From c3d335a13442a5e6cbf198150fd2389779ebccb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Tue, 6 Oct 2026 11:51:28 +0200 Subject: [PATCH] feat(mobile): add Android companion and debug validation CI Introduce an isolated Tauri HTTPS remote companion with a ten-locale launcher, strict origin/transport and launcher-only native authority. Include the shared iOS source-owned Int32 ABI fix with upstream licenses, immutable provenance and pre-link integrity guards; no linked or device-qualified iOS app is claimed. Harden preview HTTP/WebSocket credential and upstream TLS boundaries, retain no-reuse Undici containment and document residual dependencies. Add hosted mobile auth/upload, URL/authority and real loopback WebSocket/browser regressions. Apply targeted compatible npm remediation without changing desktop native sources. Validate PR source, Java XML, launcher browser and Rust policy separately from an explicit SDK-license-gated ARM64 debug APK job. Pin actions, use read-only tokens, preserve no secrets/release/store paths and upload only short-lived test APK/hash artifacts. Exclude generated API copies and the environment-bound historical Mac probe without deleting them. Integrate on current dev rather than carrying unrelated historical commits. Adapt the preview fixture to the current Config/I18n providers and normalize workflow test line endings for Windows checkouts. Local source19/19, launcher browser, ABI142/13, policy4/4 and security regressions pass; GitHub build and actual device acceptance remain distinct pending evidence. --- .github/workflows/mobile-android.yml | 167 + AGENTS.md | 1 + dev-docs/MOBILE_DELIVERY.md | 88 + package-lock.json | 1152 ++-- package.json | 21 +- packages/electron-app/package.json | 4 +- packages/mobile-app/.gitattributes | 2 + packages/mobile-app/.gitignore | 13 + packages/mobile-app/AGENTS.md | 7 + packages/mobile-app/ANDROID_TRANSPORT.md | 96 + packages/mobile-app/DEPENDENCIES.md | 57 + packages/mobile-app/IOS_ABI.md | 174 + packages/mobile-app/README.md | 142 + packages/mobile-app/index.html | 12 + packages/mobile-app/package.json | 37 + .../mobile-app/scripts/verify-ios-abi.d.mts | 4 + .../mobile-app/scripts/verify-ios-abi.mjs | 86 + packages/mobile-app/src-tauri/Cargo.lock | 4414 ++++++++++++++ packages/mobile-app/src-tauri/Cargo.toml | 30 + packages/mobile-app/src-tauri/build.rs | 20 + .../src-tauri/capabilities/launcher.json | 8 + packages/mobile-app/src-tauri/icon.svg | 4 + .../mobile-app/src-tauri/icons/128x128.png | Bin 0 -> 1176 bytes .../mobile-app/src-tauri/icons/128x128@2x.png | Bin 0 -> 1983 bytes packages/mobile-app/src-tauri/icons/32x32.png | Bin 0 -> 418 bytes packages/mobile-app/src-tauri/icons/64x64.png | Bin 0 -> 675 bytes .../src-tauri/icons/Square107x107Logo.png | Bin 0 -> 1107 bytes .../src-tauri/icons/Square142x142Logo.png | Bin 0 -> 1315 bytes .../src-tauri/icons/Square150x150Logo.png | Bin 0 -> 1433 bytes .../src-tauri/icons/Square284x284Logo.png | Bin 0 -> 2295 bytes .../src-tauri/icons/Square30x30Logo.png | Bin 0 -> 413 bytes .../src-tauri/icons/Square310x310Logo.png | Bin 0 -> 2555 bytes .../src-tauri/icons/Square44x44Logo.png | Bin 0 -> 562 bytes .../src-tauri/icons/Square71x71Logo.png | Bin 0 -> 816 bytes .../src-tauri/icons/Square89x89Logo.png | Bin 0 -> 968 bytes .../mobile-app/src-tauri/icons/StoreLogo.png | Bin 0 -> 604 bytes .../android/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../icons/android/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2150 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 1520 bytes .../android/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 2858 bytes .../icons/android/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1409 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 1024 bytes .../android/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 1836 bytes .../android/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 2581 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 1755 bytes .../mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 3626 bytes .../android/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 3109 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 2492 bytes .../mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 4603 bytes .../android/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 3485 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 3514 bytes .../mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 5966 bytes .../android/values/ic_launcher_background.xml | 4 + packages/mobile-app/src-tauri/icons/icon.icns | Bin 0 -> 29548 bytes packages/mobile-app/src-tauri/icons/icon.ico | Bin 0 -> 8124 bytes packages/mobile-app/src-tauri/icons/icon.png | Bin 0 -> 4467 bytes .../src-tauri/icons/ios/AppIcon-20x20@1x.png | Bin 0 -> 337 bytes .../icons/ios/AppIcon-20x20@2x-1.png | Bin 0 -> 501 bytes .../src-tauri/icons/ios/AppIcon-20x20@2x.png | Bin 0 -> 501 bytes .../src-tauri/icons/ios/AppIcon-20x20@3x.png | Bin 0 -> 665 bytes .../src-tauri/icons/ios/AppIcon-29x29@1x.png | Bin 0 -> 420 bytes .../icons/ios/AppIcon-29x29@2x-1.png | Bin 0 -> 643 bytes .../src-tauri/icons/ios/AppIcon-29x29@2x.png | Bin 0 -> 643 bytes .../src-tauri/icons/ios/AppIcon-29x29@3x.png | Bin 0 -> 888 bytes .../src-tauri/icons/ios/AppIcon-40x40@1x.png | Bin 0 -> 501 bytes .../icons/ios/AppIcon-40x40@2x-1.png | Bin 0 -> 857 bytes .../src-tauri/icons/ios/AppIcon-40x40@2x.png | Bin 0 -> 857 bytes .../src-tauri/icons/ios/AppIcon-40x40@3x.png | Bin 0 -> 1160 bytes .../src-tauri/icons/ios/AppIcon-512@2x.png | Bin 0 -> 11717 bytes .../src-tauri/icons/ios/AppIcon-60x60@2x.png | Bin 0 -> 1160 bytes .../src-tauri/icons/ios/AppIcon-60x60@3x.png | Bin 0 -> 1558 bytes .../src-tauri/icons/ios/AppIcon-76x76@1x.png | Bin 0 -> 820 bytes .../src-tauri/icons/ios/AppIcon-76x76@2x.png | Bin 0 -> 1368 bytes .../icons/ios/AppIcon-83.5x83.5@2x.png | Bin 0 -> 1659 bytes .../autogenerated/connect_server.toml | 11 + .../mobile-app/src-tauri/policy/Cargo.toml | 11 + .../mobile-app/src-tauri/policy/src/lib.rs | 262 + .../mobile-app/src-tauri/recovery/Cargo.toml | 13 + .../recovery/android/build.gradle.kts | 20 + .../android/src/main/AndroidManifest.xml | 4 + .../codenomad/recovery/ConnectionAuthority.kt | 64 + .../codenomad/recovery/NavigationFence.kt | 78 + .../codenomad/recovery/RecoveryPlugin.kt | 142 + .../xml/codenomad_mobile_network_security.xml | 17 + .../mobile-app/src-tauri/recovery/build.rs | 7 + .../src-tauri/recovery/ios/Package.swift | 12 + .../recovery/ios/Sources/RecoveryPlugin.swift | 94 + .../mobile-app/src-tauri/recovery/src/lib.rs | 86 + packages/mobile-app/src-tauri/src/lib.rs | 85 + packages/mobile-app/src-tauri/src/main.rs | 3 + packages/mobile-app/src-tauri/tauri.conf.json | 24 + .../vendor/tauri-2.12.1-codenomad.patch | 112 + .../src-tauri/vendor/tauri-2.12.1-local.json | 34 + .../vendor/tauri-2.12.1-upstream.json | 576 ++ .../vendor/tauri-2.12.1/.cargo_vcs_info.json | 6 + .../src-tauri/vendor/tauri-2.12.1/Cargo.lock | 5328 +++++++++++++++++ .../src-tauri/vendor/tauri-2.12.1/Cargo.toml | 433 ++ .../vendor/tauri-2.12.1/Cargo.toml.orig | 279 + .../vendor/tauri-2.12.1/LICENSE-APACHE-2.0 | 177 + .../src-tauri/vendor/tauri-2.12.1/LICENSE-MIT | 21 + .../src-tauri/vendor/tauri-2.12.1/README.md | 42 + .../src-tauri/vendor/tauri-2.12.1/build.rs | 505 ++ .../tauri-2.12.1/codenomad-ios-abi.sha256 | 13 + .../tauri-2.12.1/codenomad_ios_abi_guard.rs | 24 + .../mobile/android-codegen/TauriActivity.kt | 60 + .../tauri-2.12.1/mobile/android/.gitignore | 1 + .../mobile/android/build.gradle.kts | 48 + .../mobile/android/consumer-rules.pro | 41 + .../java/app/tauri/ExampleInstrumentedTest.kt | 28 + .../android/src/main/AndroidManifest.xml | 3 + .../src/main/java/app/tauri/AppPlugin.kt | 56 + .../src/main/java/app/tauri/FsUtils.kt | 208 + .../src/main/java/app/tauri/JniMethod.kt | 8 + .../android/src/main/java/app/tauri/Logger.kt | 85 + .../src/main/java/app/tauri/PathPlugin.kt | 132 + .../main/java/app/tauri/PermissionHelper.kt | 107 + .../main/java/app/tauri/PermissionState.kt | 21 + .../app/tauri/annotation/ActivityCallback.kt | 9 + .../java/app/tauri/annotation/InvokeArg.kt | 9 + .../java/app/tauri/annotation/Permission.kt | 19 + .../tauri/annotation/PermissionCallback.kt | 9 + .../java/app/tauri/annotation/PluginMethod.kt | 8 + .../java/app/tauri/annotation/TauriPlugin.kt | 19 + .../src/main/java/app/tauri/plugin/Channel.kt | 33 + .../plugin/InvalidPluginMethodException.kt | 11 + .../src/main/java/app/tauri/plugin/Invoke.kt | 93 + .../src/main/java/app/tauri/plugin/JSArray.kt | 45 + .../main/java/app/tauri/plugin/JSObject.kt | 152 + .../src/main/java/app/tauri/plugin/Plugin.kt | 536 ++ .../java/app/tauri/plugin/PluginHandle.kt | 178 + .../java/app/tauri/plugin/PluginManager.kt | 245 + .../java/app/tauri/plugin/PluginMethodData.kt | 16 + .../java/app/tauri/plugin/PluginResult.kt | 67 + .../test/java/app/tauri/ExampleUnitTest.kt | 21 + .../tauri-2.12.1/mobile/ios-api/.gitignore | 10 + .../tauri-2.12.1/mobile/ios-api/Package.swift | 40 + .../tauri-2.12.1/mobile/ios-api/README.md | 3 + .../ios-api/Sources/Tauri/Channel.swift | 65 + .../mobile/ios-api/Sources/Tauri/Invoke.swift | 118 + .../ios-api/Sources/Tauri/JSTypes.swift | 123 + .../ios-api/Sources/Tauri/JsonValue.swift | 58 + .../mobile/ios-api/Sources/Tauri/Logger.swift | 139 + .../ios-api/Sources/Tauri/Plugin/Plugin.swift | 82 + .../mobile/ios-api/Sources/Tauri/Tauri.swift | 157 + .../ios-api/Sources/Tauri/UiUtils.swift | 15 + .../ios-api/Tests/TauriTests/TauriTests.swift | 15 + .../tauri-2.12.1/mobile/proguard-tauri.pro | 5 + .../app/autogenerated/reference.md | 440 ++ .../event/autogenerated/reference.md | 124 + .../image/autogenerated/reference.md | 151 + .../menu/autogenerated/reference.md | 610 ++ .../path/autogenerated/reference.md | 232 + .../resources/autogenerated/reference.md | 43 + .../tray/autogenerated/reference.md | 340 ++ .../webview/autogenerated/reference.md | 488 ++ .../window/autogenerated/reference.md | 2098 +++++++ .../tauri-2.12.1/scripts/bundle.global.js | 1 + .../vendor/tauri-2.12.1/scripts/core.js | 114 + .../tauri-2.12.1/scripts/freeze_prototype.js | 5 + .../vendor/tauri-2.12.1/scripts/init.js | 15 + .../tauri-2.12.1/scripts/ipc-protocol.js | 91 + .../vendor/tauri-2.12.1/scripts/ipc.js | 210 + .../vendor/tauri-2.12.1/scripts/isolation.js | 17 + .../vendor/tauri-2.12.1/scripts/pattern.js | 21 + .../scripts/process-ipc-message-fn.js | 46 + .../src-tauri/vendor/tauri-2.12.1/src/app.rs | 2833 +++++++++ .../vendor/tauri-2.12.1/src/app/plugin.rs | 158 + .../vendor/tauri-2.12.1/src/async_runtime.rs | 363 ++ .../vendor/tauri-2.12.1/src/error.rs | 201 + .../src/event/event-system-spec.md | 34 + .../tauri-2.12.1/src/event/event_name.rs | 70 + .../vendor/tauri-2.12.1/src/event/init.js | 10 + .../vendor/tauri-2.12.1/src/event/listener.rs | 488 ++ .../vendor/tauri-2.12.1/src/event/mod.rs | 258 + .../vendor/tauri-2.12.1/src/event/plugin.rs | 82 + .../vendor/tauri-2.12.1/src/image/mod.rs | 550 ++ .../vendor/tauri-2.12.1/src/image/plugin.rs | 88 + .../src-tauri/vendor/tauri-2.12.1/src/ios.rs | 49 + .../vendor/tauri-2.12.1/src/ipc/authority.rs | 1617 +++++ .../src/ipc/capability_builder.rs | 170 + .../vendor/tauri-2.12.1/src/ipc/channel.rs | 609 ++ .../vendor/tauri-2.12.1/src/ipc/command.rs | 332 + .../tauri-2.12.1/src/ipc/format_callback.rs | 344 ++ .../vendor/tauri-2.12.1/src/ipc/mod.rs | 606 ++ .../vendor/tauri-2.12.1/src/ipc/protocol.rs | 753 +++ .../src-tauri/vendor/tauri-2.12.1/src/lib.rs | 1285 ++++ .../vendor/tauri-2.12.1/src/manager/menu.rs | 100 + .../vendor/tauri-2.12.1/src/manager/mod.rs | 1041 ++++ .../vendor/tauri-2.12.1/src/manager/tray.rs | 92 + .../tauri-2.12.1/src/manager/webview.rs | 792 +++ .../vendor/tauri-2.12.1/src/manager/window.rs | 292 + .../tauri-2.12.1/src/menu/builders/check.rs | 90 + .../tauri-2.12.1/src/menu/builders/icon.rs | 130 + .../tauri-2.12.1/src/menu/builders/menu.rs | 675 +++ .../tauri-2.12.1/src/menu/builders/mod.rs | 17 + .../tauri-2.12.1/src/menu/builders/normal.rs | 68 + .../vendor/tauri-2.12.1/src/menu/check.rs | 129 + .../vendor/tauri-2.12.1/src/menu/icon.rs | 220 + .../vendor/tauri-2.12.1/src/menu/menu.rs | 400 ++ .../vendor/tauri-2.12.1/src/menu/mod.rs | 771 +++ .../vendor/tauri-2.12.1/src/menu/normal.rs | 117 + .../vendor/tauri-2.12.1/src/menu/plugin.rs | 998 +++ .../tauri-2.12.1/src/menu/predefined.rs | 360 ++ .../vendor/tauri-2.12.1/src/menu/submenu.rs | 425 ++ .../vendor/tauri-2.12.1/src/path/android.rs | 202 + .../vendor/tauri-2.12.1/src/path/desktop.rs | 574 ++ .../vendor/tauri-2.12.1/src/path/init.js | 10 + .../vendor/tauri-2.12.1/src/path/mod.rs | 527 ++ .../vendor/tauri-2.12.1/src/path/plugin.rs | 315 + .../vendor/tauri-2.12.1/src/pattern.rs | 87 + .../vendor/tauri-2.12.1/src/plugin.rs | 1062 ++++ .../vendor/tauri-2.12.1/src/plugin/mobile.rs | 511 ++ .../vendor/tauri-2.12.1/src/process.rs | 130 + .../vendor/tauri-2.12.1/src/protocol/asset.rs | 309 + .../tauri-2.12.1/src/protocol/isolation.rs | 112 + .../vendor/tauri-2.12.1/src/protocol/mod.rs | 53 + .../vendor/tauri-2.12.1/src/protocol/tauri.rs | 267 + .../vendor/tauri-2.12.1/src/resources/mod.rs | 223 + .../tauri-2.12.1/src/resources/plugin.rs | 31 + .../vendor/tauri-2.12.1/src/scope/fs.rs | 739 +++ .../vendor/tauri-2.12.1/src/scope/mod.rs | 80 + .../vendor/tauri-2.12.1/src/state.rs | 291 + .../tauri-2.12.1/src/test/mock_runtime.rs | 1425 +++++ .../vendor/tauri-2.12.1/src/test/mod.rs | 339 ++ .../vendor/tauri-2.12.1/src/tray/mod.rs | 701 +++ .../vendor/tauri-2.12.1/src/tray/plugin.rs | 253 + .../vendor/tauri-2.12.1/src/vibrancy/macos.rs | 120 + .../vendor/tauri-2.12.1/src/vibrancy/mod.rs | 32 + .../tauri-2.12.1/src/vibrancy/windows.rs | 53 + .../vendor/tauri-2.12.1/src/webview/mod.rs | 2848 +++++++++ .../vendor/tauri-2.12.1/src/webview/plugin.rs | 255 + .../tauri-2.12.1/src/webview/scripts/print.js | 7 + .../src/webview/scripts/toggle-devtools.js | 31 + .../src/webview/scripts/zoom-hotkey.js | 43 + .../src/webview/webview_window.rs | 2874 +++++++++ .../vendor/tauri-2.12.1/src/window/mod.rs | 2620 ++++++++ .../vendor/tauri-2.12.1/src/window/plugin.rs | 351 ++ .../tauri-2.12.1/src/window/scripts/drag.js | 129 + packages/mobile-app/src/endpoint.ts | 15 + packages/mobile-app/src/launcher.css | 17 + packages/mobile-app/src/main.tsx | 48 + packages/mobile-app/src/messages.ts | 40 + .../tests/android-transport.test.ts | 37 + .../mobile-app/tests/android-workflow.test.ts | 58 + .../mobile-app/tests/browser/launcher.test.ts | 65 + packages/mobile-app/tests/bundle.test.ts | 52 + packages/mobile-app/tests/contract.test.ts | 104 + packages/mobile-app/tests/endpoint.test.ts | 11 + packages/mobile-app/tests/ios-abi.test.ts | 50 + .../tests/native/ConnectionAuthorityTest.kt | 79 + .../tests/native/TransportPolicyTest.java | 79 + .../tests/native/ios-abi-guard/Cargo.lock | 93 + .../tests/native/ios-abi-guard/Cargo.toml | 14 + .../tests/native/ios-abi-guard/integration.rs | 47 + .../tests/native/verify-embedded-assets.mjs | 48 + .../tests/native/verify-transport-apk.ps1 | 84 + packages/mobile-app/tests/urls.json | 25 + packages/mobile-app/tsconfig.json | 16 + packages/mobile-app/vite.config.ts | 8 + .../src/previews/websocket-response.test.ts | 126 + .../server/src/previews/websocket-response.ts | 94 + .../__tests__/fixtures/preview-security.ts | 423 ++ .../__tests__/fixtures/preview-ws-browser.tsx | 18 + .../server/__tests__/preview-security.test.ts | 77 + .../__tests__/preview-ws-browser.test.ts | 254 + packages/server/src/server/http-server.ts | 125 +- packages/ui/package.json | 8 +- .../ui/src/lib/runtime-env-mobile.test.ts | 65 + .../ui/tests/browser/auth-recovery.test.ts | 49 +- .../ui/tests/browser/device-upload.test.ts | 27 +- .../tests/browser/fixtures/auth-recovery.tsx | 2 + .../browser/fixtures/device-upload-server.mjs | 2 +- 272 files changed, 59869 insertions(+), 787 deletions(-) create mode 100644 .github/workflows/mobile-android.yml create mode 100644 dev-docs/MOBILE_DELIVERY.md create mode 100644 packages/mobile-app/.gitattributes create mode 100644 packages/mobile-app/.gitignore create mode 100644 packages/mobile-app/AGENTS.md create mode 100644 packages/mobile-app/ANDROID_TRANSPORT.md create mode 100644 packages/mobile-app/DEPENDENCIES.md create mode 100644 packages/mobile-app/IOS_ABI.md create mode 100644 packages/mobile-app/README.md create mode 100644 packages/mobile-app/index.html create mode 100644 packages/mobile-app/package.json create mode 100644 packages/mobile-app/scripts/verify-ios-abi.d.mts create mode 100644 packages/mobile-app/scripts/verify-ios-abi.mjs create mode 100644 packages/mobile-app/src-tauri/Cargo.lock create mode 100644 packages/mobile-app/src-tauri/Cargo.toml create mode 100644 packages/mobile-app/src-tauri/build.rs create mode 100644 packages/mobile-app/src-tauri/capabilities/launcher.json create mode 100644 packages/mobile-app/src-tauri/icon.svg create mode 100644 packages/mobile-app/src-tauri/icons/128x128.png create mode 100644 packages/mobile-app/src-tauri/icons/128x128@2x.png create mode 100644 packages/mobile-app/src-tauri/icons/32x32.png create mode 100644 packages/mobile-app/src-tauri/icons/64x64.png create mode 100644 packages/mobile-app/src-tauri/icons/Square107x107Logo.png create mode 100644 packages/mobile-app/src-tauri/icons/Square142x142Logo.png create mode 100644 packages/mobile-app/src-tauri/icons/Square150x150Logo.png create mode 100644 packages/mobile-app/src-tauri/icons/Square284x284Logo.png create mode 100644 packages/mobile-app/src-tauri/icons/Square30x30Logo.png create mode 100644 packages/mobile-app/src-tauri/icons/Square310x310Logo.png create mode 100644 packages/mobile-app/src-tauri/icons/Square44x44Logo.png create mode 100644 packages/mobile-app/src-tauri/icons/Square71x71Logo.png create mode 100644 packages/mobile-app/src-tauri/icons/Square89x89Logo.png create mode 100644 packages/mobile-app/src-tauri/icons/StoreLogo.png create mode 100644 packages/mobile-app/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 packages/mobile-app/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png create mode 100644 packages/mobile-app/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 packages/mobile-app/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png create mode 100644 packages/mobile-app/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png create mode 100644 packages/mobile-app/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 packages/mobile-app/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png create mode 100644 packages/mobile-app/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png create mode 100644 packages/mobile-app/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 packages/mobile-app/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png create mode 100644 packages/mobile-app/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png create mode 100644 packages/mobile-app/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 packages/mobile-app/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 packages/mobile-app/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png create mode 100644 packages/mobile-app/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 packages/mobile-app/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 packages/mobile-app/src-tauri/icons/android/values/ic_launcher_background.xml create mode 100644 packages/mobile-app/src-tauri/icons/icon.icns create mode 100644 packages/mobile-app/src-tauri/icons/icon.ico create mode 100644 packages/mobile-app/src-tauri/icons/icon.png create mode 100644 packages/mobile-app/src-tauri/icons/ios/AppIcon-20x20@1x.png create mode 100644 packages/mobile-app/src-tauri/icons/ios/AppIcon-20x20@2x-1.png create mode 100644 packages/mobile-app/src-tauri/icons/ios/AppIcon-20x20@2x.png create mode 100644 packages/mobile-app/src-tauri/icons/ios/AppIcon-20x20@3x.png create mode 100644 packages/mobile-app/src-tauri/icons/ios/AppIcon-29x29@1x.png create mode 100644 packages/mobile-app/src-tauri/icons/ios/AppIcon-29x29@2x-1.png create mode 100644 packages/mobile-app/src-tauri/icons/ios/AppIcon-29x29@2x.png create mode 100644 packages/mobile-app/src-tauri/icons/ios/AppIcon-29x29@3x.png create mode 100644 packages/mobile-app/src-tauri/icons/ios/AppIcon-40x40@1x.png create mode 100644 packages/mobile-app/src-tauri/icons/ios/AppIcon-40x40@2x-1.png create mode 100644 packages/mobile-app/src-tauri/icons/ios/AppIcon-40x40@2x.png create mode 100644 packages/mobile-app/src-tauri/icons/ios/AppIcon-40x40@3x.png create mode 100644 packages/mobile-app/src-tauri/icons/ios/AppIcon-512@2x.png create mode 100644 packages/mobile-app/src-tauri/icons/ios/AppIcon-60x60@2x.png create mode 100644 packages/mobile-app/src-tauri/icons/ios/AppIcon-60x60@3x.png create mode 100644 packages/mobile-app/src-tauri/icons/ios/AppIcon-76x76@1x.png create mode 100644 packages/mobile-app/src-tauri/icons/ios/AppIcon-76x76@2x.png create mode 100644 packages/mobile-app/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png create mode 100644 packages/mobile-app/src-tauri/permissions/autogenerated/connect_server.toml create mode 100644 packages/mobile-app/src-tauri/policy/Cargo.toml create mode 100644 packages/mobile-app/src-tauri/policy/src/lib.rs create mode 100644 packages/mobile-app/src-tauri/recovery/Cargo.toml create mode 100644 packages/mobile-app/src-tauri/recovery/android/build.gradle.kts create mode 100644 packages/mobile-app/src-tauri/recovery/android/src/main/AndroidManifest.xml create mode 100644 packages/mobile-app/src-tauri/recovery/android/src/main/java/ai/neuralnomads/codenomad/recovery/ConnectionAuthority.kt create mode 100644 packages/mobile-app/src-tauri/recovery/android/src/main/java/ai/neuralnomads/codenomad/recovery/NavigationFence.kt create mode 100644 packages/mobile-app/src-tauri/recovery/android/src/main/java/ai/neuralnomads/codenomad/recovery/RecoveryPlugin.kt create mode 100644 packages/mobile-app/src-tauri/recovery/android/src/main/res/xml/codenomad_mobile_network_security.xml create mode 100644 packages/mobile-app/src-tauri/recovery/build.rs create mode 100644 packages/mobile-app/src-tauri/recovery/ios/Package.swift create mode 100644 packages/mobile-app/src-tauri/recovery/ios/Sources/RecoveryPlugin.swift create mode 100644 packages/mobile-app/src-tauri/recovery/src/lib.rs create mode 100644 packages/mobile-app/src-tauri/src/lib.rs create mode 100644 packages/mobile-app/src-tauri/src/main.rs create mode 100644 packages/mobile-app/src-tauri/tauri.conf.json create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1-codenomad.patch create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1-local.json create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1-upstream.json create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/.cargo_vcs_info.json create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/Cargo.lock create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/Cargo.toml create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/Cargo.toml.orig create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/LICENSE-APACHE-2.0 create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/LICENSE-MIT create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/README.md create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/build.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/codenomad-ios-abi.sha256 create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/codenomad_ios_abi_guard.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android-codegen/TauriActivity.kt create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/.gitignore create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/build.gradle.kts create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/consumer-rules.pro create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/androidTest/java/app/tauri/ExampleInstrumentedTest.kt create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/AndroidManifest.xml create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/AppPlugin.kt create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/FsUtils.kt create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/JniMethod.kt create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/Logger.kt create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/PathPlugin.kt create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/PermissionHelper.kt create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/PermissionState.kt create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/annotation/ActivityCallback.kt create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/annotation/InvokeArg.kt create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/annotation/Permission.kt create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/annotation/PermissionCallback.kt create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/annotation/PluginMethod.kt create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/annotation/TauriPlugin.kt create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/Channel.kt create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/InvalidPluginMethodException.kt create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/Invoke.kt create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/JSArray.kt create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/JSObject.kt create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/Plugin.kt create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/PluginHandle.kt create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/PluginManager.kt create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/PluginMethodData.kt create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/PluginResult.kt create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/test/java/app/tauri/ExampleUnitTest.kt create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/.gitignore create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Package.swift create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/README.md create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/Channel.swift create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/Invoke.swift create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/JSTypes.swift create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/JsonValue.swift create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/Logger.swift create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/Plugin/Plugin.swift create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/Tauri.swift create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/UiUtils.swift create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Tests/TauriTests/TauriTests.swift create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/proguard-tauri.pro create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/app/autogenerated/reference.md create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/event/autogenerated/reference.md create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/image/autogenerated/reference.md create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/menu/autogenerated/reference.md create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/path/autogenerated/reference.md create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/resources/autogenerated/reference.md create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/tray/autogenerated/reference.md create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/webview/autogenerated/reference.md create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/window/autogenerated/reference.md create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/bundle.global.js create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/core.js create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/freeze_prototype.js create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/init.js create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/ipc-protocol.js create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/ipc.js create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/isolation.js create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/pattern.js create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/process-ipc-message-fn.js create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/app.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/app/plugin.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/async_runtime.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/error.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/event/event-system-spec.md create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/event/event_name.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/event/init.js create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/event/listener.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/event/mod.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/event/plugin.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/image/mod.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/image/plugin.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ios.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ipc/authority.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ipc/capability_builder.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ipc/channel.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ipc/command.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ipc/format_callback.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ipc/mod.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ipc/protocol.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/lib.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/manager/menu.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/manager/mod.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/manager/tray.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/manager/webview.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/manager/window.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/builders/check.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/builders/icon.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/builders/menu.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/builders/mod.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/builders/normal.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/check.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/icon.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/menu.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/mod.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/normal.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/plugin.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/predefined.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/submenu.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/path/android.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/path/desktop.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/path/init.js create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/path/mod.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/path/plugin.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/pattern.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/plugin.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/plugin/mobile.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/process.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/protocol/asset.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/protocol/isolation.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/protocol/mod.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/protocol/tauri.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/resources/mod.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/resources/plugin.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/scope/fs.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/scope/mod.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/state.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/test/mock_runtime.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/test/mod.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/tray/mod.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/tray/plugin.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/vibrancy/macos.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/vibrancy/mod.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/vibrancy/windows.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/webview/mod.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/webview/plugin.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/webview/scripts/print.js create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/webview/scripts/toggle-devtools.js create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/webview/scripts/zoom-hotkey.js create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/webview/webview_window.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/window/mod.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/window/plugin.rs create mode 100644 packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/window/scripts/drag.js create mode 100644 packages/mobile-app/src/endpoint.ts create mode 100644 packages/mobile-app/src/launcher.css create mode 100644 packages/mobile-app/src/main.tsx create mode 100644 packages/mobile-app/src/messages.ts create mode 100644 packages/mobile-app/tests/android-transport.test.ts create mode 100644 packages/mobile-app/tests/android-workflow.test.ts create mode 100644 packages/mobile-app/tests/browser/launcher.test.ts create mode 100644 packages/mobile-app/tests/bundle.test.ts create mode 100644 packages/mobile-app/tests/contract.test.ts create mode 100644 packages/mobile-app/tests/endpoint.test.ts create mode 100644 packages/mobile-app/tests/ios-abi.test.ts create mode 100644 packages/mobile-app/tests/native/ConnectionAuthorityTest.kt create mode 100644 packages/mobile-app/tests/native/TransportPolicyTest.java create mode 100644 packages/mobile-app/tests/native/ios-abi-guard/Cargo.lock create mode 100644 packages/mobile-app/tests/native/ios-abi-guard/Cargo.toml create mode 100644 packages/mobile-app/tests/native/ios-abi-guard/integration.rs create mode 100644 packages/mobile-app/tests/native/verify-embedded-assets.mjs create mode 100644 packages/mobile-app/tests/native/verify-transport-apk.ps1 create mode 100644 packages/mobile-app/tests/urls.json create mode 100644 packages/mobile-app/tsconfig.json create mode 100644 packages/mobile-app/vite.config.ts create mode 100644 packages/server/src/previews/websocket-response.test.ts create mode 100644 packages/server/src/previews/websocket-response.ts create mode 100644 packages/server/src/server/__tests__/fixtures/preview-security.ts create mode 100644 packages/server/src/server/__tests__/fixtures/preview-ws-browser.tsx create mode 100644 packages/server/src/server/__tests__/preview-security.test.ts create mode 100644 packages/server/src/server/__tests__/preview-ws-browser.test.ts create mode 100644 packages/ui/src/lib/runtime-env-mobile.test.ts diff --git a/.github/workflows/mobile-android.yml b/.github/workflows/mobile-android.yml new file mode 100644 index 000000000..58298d6c1 --- /dev/null +++ b/.github/workflows/mobile-android.yml @@ -0,0 +1,167 @@ +name: Mobile Android validation and debug APK + +on: + pull_request: + paths: + - '.github/workflows/mobile-android.yml' + - 'packages/mobile-app/**' + - 'packages/*/package.json' + - 'packages/ui/src/styles/tokens.css' + - 'packages/ui/src/lib/i18n/messages/**/remoteAccess.ts' + - 'packages/server/src/server/routes/auth-pages/login.html' + - 'package.json' + - 'package-lock.json' + - '.npmrc' + workflow_dispatch: + inputs: + accept_sdk_licenses: + description: I am authorized to accept Android SDK licenses on this disposable runner + type: boolean + required: true + default: false + +permissions: + contents: read + +concurrency: + group: mobile-android-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + source-validation: + name: Launcher, source contracts and Rust policy (not a native APK) + runs-on: windows-2022 + timeout-minutes: 25 + defaults: + run: + shell: pwsh + steps: + # github.sha is the PR merge commit, not an interpolated fork branch name. + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + ref: ${{ github.sha }} + persist-credentials: false + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: '24' + - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + with: + distribution: temurin + java-version: '17' + - uses: dtolnay/rust-toolchain@7e38f4b43b4db5c8dd498af069a4f6196df1d067 # reviewed master commit + with: + toolchain: '1.94.0' + - name: Install locked npm dependencies without desktop lifecycle scripts + run: npm ci --ignore-scripts + - name: Require production Android XML parsing (no CI skip) + working-directory: packages/mobile-app + run: java --source 17 tests/native/TransportPolicyTest.java + - name: Validate launcher and source-owned native contracts + run: | + npm run typecheck:mobile + if ($LASTEXITCODE) { exit $LASTEXITCODE } + npm run ios:verify --workspace @codenomad/mobile-app + if ($LASTEXITCODE) { exit $LASTEXITCODE } + npm run test:mobile + if ($LASTEXITCODE) { exit $LASTEXITCODE } + npx --workspace @codenomad/mobile-app playwright install chromium + if ($LASTEXITCODE) { exit $LASTEXITCODE } + npm run test:browser --workspace @codenomad/mobile-app + if ($LASTEXITCODE) { exit $LASTEXITCODE } + cargo test --locked --manifest-path packages/mobile-app/src-tauri/policy/Cargo.toml + if ($LASTEXITCODE) { exit $LASTEXITCODE } + - name: Report APK license gate separately from source validation + if: always() + env: + SDK_LICENSE_AUTHORIZED: ${{ (github.event_name == 'pull_request' && vars.CODENOMAD_ANDROID_ACCEPT_SDK_LICENSES == 'true') || (github.event_name == 'workflow_dispatch' && inputs.accept_sdk_licenses == true) }} + run: | + 'Source/browser/policy checks are not Android compilation or device qualification.' >> $env:GITHUB_STEP_SUMMARY + if ($env:SDK_LICENSE_AUTHORIZED -eq 'true') { + 'SDK license consent is enabled. See the separate ARM64 debug APK job for actual build success/failure; it also requires source validation to pass.' >> $env:GITHUB_STEP_SUMMARY + } else { + 'ARM64 debug APK job SKIPPED: no explicit SDK license consent. An authorized maintainer must set CODENOMAD_ANDROID_ACCEPT_SDK_LICENSES=true for PR builds, or confirm the manual dispatch checkbox. Source success is not APK build success.' >> $env:GITHUB_STEP_SUMMARY + } + debug-apk: + name: ARM64 debug APK (explicit SDK license consent required) + needs: source-validation + if: ${{ (github.event_name == 'pull_request' && vars.CODENOMAD_ANDROID_ACCEPT_SDK_LICENSES == 'true') || (github.event_name == 'workflow_dispatch' && inputs.accept_sdk_licenses == true) }} + runs-on: windows-2022 + timeout-minutes: 60 + defaults: + run: + shell: pwsh + env: + CI: 'true' + NDK_VERSION: '28.2.13676358' + steps: + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + ref: ${{ github.sha }} + persist-credentials: false + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: '24' + - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + with: + distribution: temurin + java-version: '17' + - uses: dtolnay/rust-toolchain@7e38f4b43b4db5c8dd498af069a4f6196df1d067 # reviewed master commit + with: + toolchain: '1.94.0' + targets: aarch64-linux-android + - name: Set up explicit Android SDK packages on the CI runner only + uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3.2.2 + with: + cmdline-tools-version: '12266719' + accept-android-sdk-licenses: 'true' + log-accepted-android-sdk-licenses: 'true' + # CLI 2.12.1 generates API 37; the recovery library uses API 36. + packages: platform-tools platforms;android-37.0 platforms;android-36 build-tools;36.0.0 ndk;28.2.13676358 + - name: Configure and verify execution-host Android paths + run: | + if (!$env:ANDROID_HOME -or !$env:JAVA_HOME) { throw 'SDK/JDK environment is missing.' } + $ndk = Join-Path $env:ANDROID_HOME "ndk\$env:NDK_VERSION" + foreach ($relative in @('platforms\android-37.0\android.jar', 'platforms\android-36\android.jar', 'build-tools\36.0.0\aapt.exe')) { + if (!(Test-Path (Join-Path $env:ANDROID_HOME $relative))) { throw "Missing SDK component: $relative" } + } + if (!(Test-Path (Join-Path $ndk 'source.properties'))) { throw 'Pinned NDK is missing.' } + "ANDROID_SDK_ROOT=$env:ANDROID_HOME" >> $env:GITHUB_ENV + "NDK_HOME=$ndk" >> $env:GITHUB_ENV + java -version + if ($LASTEXITCODE) { exit $LASTEXITCODE } + rustc --version + if ($LASTEXITCODE) { exit $LASTEXITCODE } + - name: Install locked npm dependencies without desktop lifecycle scripts + run: npm ci --ignore-scripts + - name: Initialize Android without interactive prompts or extra Rust targets + run: npm run android:init --workspace @codenomad/mobile-app -- --ci --skip-targets-install + - name: Build development-signed ARM64 debug APK only + run: npm run android:debug --workspace @codenomad/mobile-app -- --ci + - name: Require APK output and create SHA-256 manifest + run: | + $outputs = Join-Path $env:GITHUB_WORKSPACE 'packages\mobile-app\src-tauri\gen\android\app\build\outputs\apk' + if (!(Test-Path $outputs)) { throw 'Android APK output directory is missing.' } + $apks = @(Get-ChildItem $outputs -Recurse -File -Filter '*debug*.apk') + if ($apks.Count -eq 0) { throw 'Build completed without a debug APK.' } + $destination = Join-Path $env:RUNNER_TEMP 'codenomad-mobile-apk' + New-Item -ItemType Directory -Force $destination | Out-Null + $hashes = foreach ($apk in $apks) { + $target = Join-Path $destination $apk.Name + if (Test-Path $target) { throw "Duplicate APK filename: $($apk.Name)" } + Copy-Item $apk.FullName $target + $hash = (Get-FileHash $target -Algorithm SHA256).Hash.ToLowerInvariant() + "$hash $($apk.Name)" + } + $hashes | Set-Content (Join-Path $destination 'SHA256SUMS.txt') -Encoding utf8 + @("commit=$env:GITHUB_SHA", "run=$env:GITHUB_RUN_ID", 'variant=debug', 'target=aarch64', 'not-for-store-distribution=true') | + Set-Content (Join-Path $destination 'build.txt') -Encoding utf8 + '## ARM64 debug APK SHA-256' >> $env:GITHUB_STEP_SUMMARY + $hashes | ForEach-Object { "- ``$_``" >> $env:GITHUB_STEP_SUMMARY } + - name: Upload test APK and hashes, not a store/release publication + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: codenomad-android-debug-arm64-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/codenomad-mobile-apk/ + if-no-files-found: error + retention-days: 7 + compression-level: 0 diff --git a/AGENTS.md b/AGENTS.md index 24545d716..01ec0da58 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -130,6 +130,7 @@ Behavior for agents: - Run them with `npm run test:browser --workspace @codenomad/ui` after `npx playwright install chromium`. `CODENOMAD_BROWSER_PATH` optionally selects an existing Chromium executable; it does not target the installed application or user sessions. ## V2 Runtime Launch +- The Android-first companion is isolated in `packages/mobile-app/`: one webview, a trusted bundled launcher, and a top-level hosted web/remote UI. Never bundle the desktop server/Node/CLI or grant remote native capabilities. Keep endpoint validation mirrored by shared JS/Rust fixtures; native navigation restricts redirects to the selected HTTPS origin. Recovery chrome belongs to the native mobile recovery plugin, never hosted DOM/IPC or a second mobile webview. Launcher-only styles reuse UI tokens in `packages/mobile-app/src/launcher.css`; its standalone locale adapter reuses each UI locale's remote-access messages without mounting server-backed preferences. All platform recovery changes require real-device verification before claiming mobile readiness. - Native automation instrumentation starts automatically; no Developer Mode toggle or activation restart is required. Do not configure a fixed CDP port or a manual WebView2 profile. - Rebuild Electron before calling `codenomad.act({ action: "restart" })`. For Windows Tauri, stop and relaunch the release executable only when the linker cannot replace it; never stop the shared OpenCode daemon. diff --git a/dev-docs/MOBILE_DELIVERY.md b/dev-docs/MOBILE_DELIVERY.md new file mode 100644 index 000000000..e31e9b42a --- /dev/null +++ b/dev-docs/MOBILE_DELIVERY.md @@ -0,0 +1,88 @@ +# Mobile delivery gates + +## Architecture decision + +The Android-first companion is a separate Tauri v2 host, not a mobile build of +the desktop process supervisor. It displays the selected CodeNomad server's +hosted UI as a top-level page. Node, Git, OpenCode, directory authorization, +session environment writes, and all execution remain on that server. + +Same-origin hosting preserves the existing login cookie, CodeNomad API, +allowlisted OpenCode proxy, and multiplexed SSE transport. A bundled copy of +the transcript UI with cross-origin APIs is deliberately out of scope. + +Only the bundled connection screen may manage endpoints. Remote content must +have no native capabilities. A JavaScript host override selects web/remote UI +behavior; this is not a replacement for native capability/origin enforcement. +Endpoints require device-trusted HTTPS. There is no certificate bypass or +cleartext fallback. The initial deployment is a controlled pilot, not a claim +of unrestricted public-Internet or app-store readiness. + +## Validation levels + +Keep these outcomes separate in release reports: + +1. Source and deterministic policy tests pass. +2. Launcher build, shared UI typecheck, and browser fixtures pass. +3. Rust/Tauri host compiles on a development host. +4. Android APK compiles with an actual Android SDK/NDK and target. +5. Emulator/physical-device acceptance checks pass. +6. Signed distribution build and store requirements are satisfied. + +A Chromium mobile viewport is not Android WebView or WKWebView validation. +A Windows host compile is not an Android compile. A workflow definition is +not evidence that the workflow ran or produced an installable APK. + +## Android acceptance checklist + +- Install a debug APK; confirm no Node/server/CLI resources are included. +- Connect to a disposable authenticated CodeNomad fixture with trusted HTTPS. +- Reject HTTP, credentials in URLs, unsupported URL components, and launcher + origin collisions. Fence cross-origin navigation, redirects, and popups. +- Prove remote pages and their frames cannot invoke launcher/native commands. +- Prove invalid, expired, and hostname-mismatched TLS certificates fail closed. +- Exercise native return/disconnect while loading, offline, and on TLS errors. +- Log in, stream a response, settle Forms/permissions, and upload a device file. +- Suspend and resume while work finishes; reconcile authoritative state and + never replay failed prompt mutations on reconnection or reauthentication. +- Restart the test backend and recover authentication without losing an + in-memory draft or submitting it twice. +- Check IME resize, rotation, safe areas, touch/nested scroll, RTL, and upload + cancellation on the actual webview. +- Kill and relaunch the app. Record cookie behavior and document that remote + web mode does not inherit native desktop draft/tab restoration. + +Use isolated fixture backends and databases, never the shared user daemon. + +## iOS follow-up + +Tauri supports iOS, but compilation requires macOS and full Xcode, CocoaPods, +and iOS Rust targets. An installed macOS Rust target on Windows is insufficient. +Repeat the acceptance checklist on WKWebView, including safe areas, keyboard, +file selection, authentication persistence, and suspension. Signing and App +Store distribution require an Apple team, certificate, and provisioning. + +## Server exposure prerequisites + +The architecture audit of Git HEAD `9597853e` identified separate hardening +work before claiming unrestricted Internet readiness: + +- Session expiry, bounded storage, revocation on logout/password changes. +- Credentialed CORS and request-origin/CSRF policy. +- Login abuse protection. +- Explicit reverse-proxy TLS trust and Secure-cookie behavior. + +Mobile login is full backend access, not a read-only observer role. Hiding +administrative controls does not create an authorization boundary. A VPN +reduces exposure but does not turn these open hardening items into completed +security work. + +## Official references + +- https://v2.tauri.app/start/prerequisites/ +- https://v2.tauri.app/start/project-structure/ +- https://v2.tauri.app/security/capabilities/ +- https://v2.tauri.app/distribute/google-play/ +- https://v2.tauri.app/distribute/sign/android/ +- https://v2.tauri.app/distribute/sign/ios/ +- https://v2.tauri.app/distribute/app-store/ diff --git a/package-lock.json b/package-lock.json index c980bddde..9d1224b57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,22 +13,24 @@ "google-auth-library": "^10.5.0" }, "devDependencies": { - "baseline-browser-mapping": "^2.9.11" + "baseline-browser-mapping": "^2.11.0", + "rollup": "4.59.0" }, "optionalDependencies": { - "@rollup/rollup-darwin-arm64": "4.52.5", - "@rollup/rollup-darwin-x64": "4.52.5", - "@rollup/rollup-linux-arm64-gnu": "4.52.5", - "@rollup/rollup-linux-x64-gnu": "4.52.5", - "@rollup/rollup-win32-arm64-msvc": "4.52.5", - "@rollup/rollup-win32-x64-msvc": "4.52.5" + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0" }, "workspaces": { "packages": [ "packages/server", "packages/ui", "packages/electron-app", - "packages/tauri-app" + "packages/tauri-app", + "packages/mobile-app" ] } }, @@ -532,13 +534,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -547,9 +549,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -557,21 +559,21 @@ } }, "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -588,14 +590,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -618,14 +620,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -692,9 +694,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -716,29 +718,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -761,9 +763,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", "dev": true, "license": "MIT", "engines": { @@ -821,9 +823,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -831,9 +833,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -841,9 +843,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -866,27 +868,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -1479,16 +1481,16 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", - "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.8.tgz", + "integrity": "sha512-6iSnEK0zlkLKU4heofK/AdmRD4e2SHVpJMtrwnTCzhnaM98ria4rTrOXBBi45BTTYnJtO8txnPsX4fChYXkmeA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.29.0" + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.8" }, "engines": { "node": ">=6.9.0" @@ -2024,33 +2026,33 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", "debug": "^4.3.1" }, "engines": { @@ -2058,14 +2060,14 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -2078,6 +2080,10 @@ "dev": true, "license": "MIT" }, + "node_modules/@codenomad/mobile-app": { + "resolved": "packages/mobile-app", + "link": true + }, "node_modules/@codenomad/tauri-app": { "resolved": "packages/tauri-app", "link": true @@ -5515,9 +5521,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", - "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], @@ -5529,9 +5535,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", - "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], @@ -5543,9 +5549,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", - "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], @@ -5556,9 +5562,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", - "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], @@ -5569,9 +5575,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", - "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], @@ -5583,9 +5589,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", - "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], @@ -5597,9 +5603,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", - "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], @@ -5611,9 +5617,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", - "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], @@ -5625,9 +5631,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", - "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], @@ -5638,9 +5644,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", - "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], @@ -5652,9 +5658,23 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", - "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "cpu": [ "loong64" ], @@ -5666,9 +5686,23 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", - "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], @@ -5680,9 +5714,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", - "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], @@ -5694,9 +5728,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", - "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], @@ -5708,9 +5742,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", - "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], @@ -5722,9 +5756,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", - "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], @@ -5735,9 +5769,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", - "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], @@ -5748,10 +5782,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", - "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "cpu": [ "arm64" ], @@ -5763,9 +5811,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", - "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], @@ -5776,9 +5824,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", - "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], @@ -5790,9 +5838,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", - "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", "cpu": [ "x64" ], @@ -5804,9 +5852,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", - "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], @@ -6440,9 +6488,9 @@ } }, "node_modules/@tauri-apps/cli": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.9.4.tgz", - "integrity": "sha512-pvylWC9QckrOS9ATWXIXcgu7g2hKK5xTL5ZQyZU/U0n9l88SEFGcWgLQNa8WZmd+wWIOWhkxOFcOl3i6ubDNNw==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.12.1.tgz", + "integrity": "sha512-kEDEiGzG+yAc5FeLxtXpES/VN+F2C8H0r4gDVtfRLKxT9np9101d9REG/Kgo2lr0hKi0IpDsODiHnU3+naOmLg==", "dev": true, "license": "Apache-2.0 OR MIT", "bin": { @@ -6456,23 +6504,40 @@ "url": "https://opencollective.com/tauri" }, "optionalDependencies": { - "@tauri-apps/cli-darwin-arm64": "2.9.4", - "@tauri-apps/cli-darwin-x64": "2.9.4", - "@tauri-apps/cli-linux-arm-gnueabihf": "2.9.4", - "@tauri-apps/cli-linux-arm64-gnu": "2.9.4", - "@tauri-apps/cli-linux-arm64-musl": "2.9.4", - "@tauri-apps/cli-linux-riscv64-gnu": "2.9.4", - "@tauri-apps/cli-linux-x64-gnu": "2.9.4", - "@tauri-apps/cli-linux-x64-musl": "2.9.4", - "@tauri-apps/cli-win32-arm64-msvc": "2.9.4", - "@tauri-apps/cli-win32-ia32-msvc": "2.9.4", - "@tauri-apps/cli-win32-x64-msvc": "2.9.4" + "@tauri-apps/cli-darwin-arm64": "2.12.1", + "@tauri-apps/cli-darwin-x64": "2.12.1", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.12.1", + "@tauri-apps/cli-linux-arm64-gnu": "2.12.1", + "@tauri-apps/cli-linux-arm64-musl": "2.12.1", + "@tauri-apps/cli-linux-riscv64-gnu": "2.12.1", + "@tauri-apps/cli-linux-x64-gnu": "2.12.1", + "@tauri-apps/cli-linux-x64-musl": "2.12.1", + "@tauri-apps/cli-win32-arm64-msvc": "2.12.1", + "@tauri-apps/cli-win32-ia32-msvc": "2.12.1", + "@tauri-apps/cli-win32-x64-msvc": "2.12.1" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.12.1.tgz", + "integrity": "sha512-8bMnbpJ2jKO/ufrOPL65EMHC3ptw7fzeraoQkwibyfGLqIl+UCWsC0152kmNbW6r9NvVQduXqAZF5mH1QgTWhw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" } }, "node_modules/@tauri-apps/cli-darwin-x64": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.9.4.tgz", - "integrity": "sha512-VT9ymNuT06f5TLjCZW2hfSxbVtZDhORk7CDUDYiq5TiSYQdxkl8MVBy0CCFFcOk4QAkUmqmVUA9r3YZ/N/vPRQ==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.12.1.tgz", + "integrity": "sha512-G+2qte8GNqNlEZ9ftd21rbu/APVkl3r/nzNDI7LVxEoqJWVNe9GbEgQUMH1r4PHT20Z2UxR3KjCkltIb0e/Wjg==", "cpu": [ "x64" ], @@ -6487,9 +6552,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.9.4.tgz", - "integrity": "sha512-tTWkEPig+2z3Rk0zqZYfjUYcgD+aSm72wdrIhdYobxbQZOBw0zfn50YtWv+av7bm0SHvv75f0l7JuwgZM1HFow==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.12.1.tgz", + "integrity": "sha512-5b3n4DaTW+A1Zntnh4Yn44VyLrlPEpB8kMOBRfWkzNnzolBk+AYhKIfKjCLmoC8Mob3ETodxAEOMkE2YPKEWzQ==", "cpu": [ "arm" ], @@ -6504,9 +6569,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm64-gnu": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.9.4.tgz", - "integrity": "sha512-ql6vJ611qoqRYHxkKPnb2vHa27U+YRKRmIpLMMBeZnfFtZ938eao7402AQCH1mO2+/8ioUhbpy9R/ZcLTXVmkg==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.12.1.tgz", + "integrity": "sha512-APArHhDu+8pXSCkCQmvaz1nD3d8WTmlCoSS/oG475TJ6AATBvyQOAmf5qELgsQ4s2+8MOsop0Zd699RKELF1Cw==", "cpu": [ "arm64" ], @@ -6521,9 +6586,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm64-musl": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.9.4.tgz", - "integrity": "sha512-vg7yNn7ICTi6hRrcA/6ff2UpZQP7un3xe3SEld5QM0prgridbKAiXGaCKr3BnUBx/rGXegQlD/wiLcWdiiraSw==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.12.1.tgz", + "integrity": "sha512-JaCxcLoV0oGtllVkLr4UAHWG/wTfkEunnUqnsEsGYOp0VRxnUwHg63wFBD0jU2ul5eFfSLIDS4RF2gfzsSo5Jg==", "cpu": [ "arm64" ], @@ -6538,9 +6603,9 @@ } }, "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.9.4.tgz", - "integrity": "sha512-l8L+3VxNk6yv5T/Z/gv5ysngmIpsai40B9p6NQQyqYqxImqYX37pqREoEBl1YwG7szGnDibpWhidPrWKR59OJA==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.12.1.tgz", + "integrity": "sha512-K5+VXM7+SGVDwZleA2+lMgvbTVuA5e1tkiVEawu1Yxj26ZyEfO8h8xTC1rBdRX2xgJDSwCk6vPUBCS6i+Z5slQ==", "cpu": [ "riscv64" ], @@ -6555,9 +6620,9 @@ } }, "node_modules/@tauri-apps/cli-linux-x64-gnu": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.9.4.tgz", - "integrity": "sha512-PepPhCXc/xVvE3foykNho46OmCyx47E/aG676vKTVp+mqin5d+IBqDL6wDKiGNT5OTTxKEyNlCQ81Xs2BQhhqA==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.12.1.tgz", + "integrity": "sha512-Z1QGPr49HJZMktu+Spu6Q1b/f9ANadXvqZZZt2645ua1x1ev6prOgp85eiGv2jRzohFEMLo6aJyaLjLUX5KIvA==", "cpu": [ "x64" ], @@ -6572,9 +6637,9 @@ } }, "node_modules/@tauri-apps/cli-linux-x64-musl": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.9.4.tgz", - "integrity": "sha512-zcd1QVffh5tZs1u1SCKUV/V7RRynebgYUNWHuV0FsIF1MjnULUChEXhAhug7usCDq4GZReMJOoXa6rukEozWIw==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.12.1.tgz", + "integrity": "sha512-nHuEhRPOMSJ/8RRhtMRxB9B9lVr7xxpe4DapBz8x1GIau3vdzjHHJ1hAfmw+PyayPLZ6XoaVIHPgI17rX6ii8w==", "cpu": [ "x64" ], @@ -6589,9 +6654,9 @@ } }, "node_modules/@tauri-apps/cli-win32-arm64-msvc": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.9.4.tgz", - "integrity": "sha512-/7ZhnP6PY04bEob23q8MH/EoDISdmR1wuNm0k9d5HV7TDMd2GGCDa8dPXA4vJuglJKXIfXqxFmZ4L+J+MO42+w==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.12.1.tgz", + "integrity": "sha512-I0oPk2uGh86R8dAs2X5OxSP+/GqSc8jh1Px3l8EqrocSNivsRHDTh2k3PVOqdv163dlV6PIVvmhZAbH/lPfZrA==", "cpu": [ "arm64" ], @@ -6606,9 +6671,9 @@ } }, "node_modules/@tauri-apps/cli-win32-ia32-msvc": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.9.4.tgz", - "integrity": "sha512-1LmAfaC4Cq+3O1Ir1ksdhczhdtFSTIV51tbAGtbV/mr348O+M52A/xwCCXQank0OcdBxy5BctqkMtuZnQvA8uQ==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.12.1.tgz", + "integrity": "sha512-baNZRBfepAJ5qNFRMwzgBk94xQmkcmTZrNDjZjnzrq2vAaTC3NAhallb/zo3hyoCGQlMS13sdovktrg46NZz6Q==", "cpu": [ "ia32" ], @@ -6623,9 +6688,9 @@ } }, "node_modules/@tauri-apps/cli-win32-x64-msvc": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.9.4.tgz", - "integrity": "sha512-EdYd4c9wGvtPB95kqtEyY+bUR+k4kRw3IA30mAQ1jPH6z57AftT8q84qwv0RDp6kkEqOBKxeInKfqi4BESYuqg==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.12.1.tgz", + "integrity": "sha512-dIFaKslrzcbesb+bBh+E1R9km241pHyj32w6D9FPtvQeYpRUFbiCLEa/2S+S0UnIytfxepp4P+X8IerGqnXeYg==", "cpu": [ "x64" ], @@ -6639,23 +6704,6 @@ "node": ">= 10" } }, - "node_modules/@tauri-apps/cli/node_modules/@tauri-apps/cli-darwin-arm64": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.9.4.tgz", - "integrity": "sha512-9rHkMVtbMhe0AliVbrGpzMahOBg3rwV46JYRELxR9SN6iu1dvPOaMaiC4cP6M/aD1424ziXnnMdYU06RAH8oIw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@tauri-apps/plugin-dialog": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.6.0.tgz", @@ -7760,13 +7808,16 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.11", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", - "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", + "version": "2.11.27", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.27.tgz", + "integrity": "sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/bignumber.js": { @@ -7876,9 +7927,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.29.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", + "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", "dev": true, "funding": [ { @@ -7896,11 +7947,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.11.26", + "caniuse-lite": "^1.0.30001813", + "electron-to-chromium": "^1.5.439", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" }, "bin": { "browserslist": "cli.js" @@ -8300,9 +8351,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001769", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", - "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "version": "1.0.30001814", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001814.tgz", + "integrity": "sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA==", "dev": true, "funding": [ { @@ -9618,9 +9669,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.286", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", - "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "version": "1.5.444", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.444.tgz", + "integrity": "sha512-5ss/uJfoDYDHT0lfJzT6FbcskIzROIOPf0BbbFkGcvDzoJU7i//9GDrwwIHQVmIsrAGiF3ihpADBRIsrEFt1rQ==", "dev": true, "license": "ISC" }, @@ -12983,9 +13034,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", "dev": true, "funding": [ { @@ -13212,11 +13263,14 @@ } }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.57", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", + "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/nopt": { "version": "9.0.0", @@ -14048,9 +14102,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "dev": true, "funding": [ { @@ -14068,7 +14122,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -14977,9 +15031,9 @@ } }, "node_modules/rollup": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", - "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", "dependencies": { @@ -14993,28 +15047,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.5", - "@rollup/rollup-android-arm64": "4.52.5", - "@rollup/rollup-darwin-arm64": "4.52.5", - "@rollup/rollup-darwin-x64": "4.52.5", - "@rollup/rollup-freebsd-arm64": "4.52.5", - "@rollup/rollup-freebsd-x64": "4.52.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", - "@rollup/rollup-linux-arm-musleabihf": "4.52.5", - "@rollup/rollup-linux-arm64-gnu": "4.52.5", - "@rollup/rollup-linux-arm64-musl": "4.52.5", - "@rollup/rollup-linux-loong64-gnu": "4.52.5", - "@rollup/rollup-linux-ppc64-gnu": "4.52.5", - "@rollup/rollup-linux-riscv64-gnu": "4.52.5", - "@rollup/rollup-linux-riscv64-musl": "4.52.5", - "@rollup/rollup-linux-s390x-gnu": "4.52.5", - "@rollup/rollup-linux-x64-gnu": "4.52.5", - "@rollup/rollup-linux-x64-musl": "4.52.5", - "@rollup/rollup-openharmony-arm64": "4.52.5", - "@rollup/rollup-win32-arm64-msvc": "4.52.5", - "@rollup/rollup-win32-ia32-msvc": "4.52.5", - "@rollup/rollup-win32-x64-gnu": "4.52.5", - "@rollup/rollup-win32-x64-msvc": "4.52.5", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, @@ -15238,18 +15295,18 @@ } }, "node_modules/seroval": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz", - "integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==", + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.6.tgz", + "integrity": "sha512-rVQVWjjSvlINzaQPZH5JFqsqEsIWdTxY3iJZCnTL/5gQbXIRooVZKI60tVCkOVfzcRPejboxO2t0P89dg5mQaA==", "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/seroval-plugins": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.3.3.tgz", - "integrity": "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==", + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.6.tgz", + "integrity": "sha512-HXuLAX2pu/UByPpaeo/TaMfvMIi+1QqIoPJYCcAtU8QkVNwgR6MPlGuCQTErV1JwraaMbYaWVIBX7mppzGLATQ==", "license": "MIT", "engines": { "node": ">=10" @@ -15644,14 +15701,14 @@ } }, "node_modules/solid-js": { - "version": "1.9.10", - "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.10.tgz", - "integrity": "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==", + "version": "1.9.15", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.15.tgz", + "integrity": "sha512-EeiY2xfpZJqPLjXspVEKjAII4yv8NyG//NxZ3IpOFHdUNnnTyL0uJOeS9LWGvA7cFCz5y94cjFwYlmw5Luncsg==", "license": "MIT", "dependencies": { "csstype": "^3.1.0", - "seroval": "~1.3.0", - "seroval-plugins": "~1.3.0" + "seroval": "~1.5.4", + "seroval-plugins": "~1.5.4" } }, "node_modules/solid-presence": { @@ -16972,9 +17029,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", "dev": true, "funding": [ { @@ -17135,21 +17192,24 @@ } }, "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -17158,19 +17218,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", - "terser": "^5.4.0" + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -17191,6 +17257,12 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, @@ -17250,434 +17322,36 @@ } } }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">=12" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, + "peer": true, "engines": { "node": ">=12" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/vitefu": { @@ -18211,9 +17885,9 @@ } }, "node_modules/workbox-build/node_modules/rollup": { - "version": "2.79.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", - "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", + "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", "dev": true, "license": "MIT", "bin": { @@ -18661,8 +18335,8 @@ "pngjs": "^7.0.0", "tsx": "^4.20.6", "typescript": "^5.3.0", - "vite": "^5.0.0", - "vite-plugin-solid": "^2.10.0" + "vite": "^6.4.3", + "vite-plugin-solid": "^2.11.10" } }, "packages/electron-app/node_modules/app-builder-bin": { @@ -18672,6 +18346,24 @@ "dev": true, "license": "MIT" }, + "packages/mobile-app": { + "name": "@codenomad/mobile-app", + "version": "0.20.0-dev-v2", + "license": "MIT", + "dependencies": { + "@tauri-apps/api": "^2.10.1", + "solid-js": "^1.9.15" + }, + "devDependencies": { + "@tauri-apps/cli": "2.12.1", + "@types/node": "^22.0.0", + "playwright": "1.63.0", + "tsx": "^4.20.0", + "typescript": "^5.3.0", + "vite": "^6.4.3", + "vite-plugin-solid": "^2.11.10" + } + }, "packages/server": { "name": "@neuralnomads/codenomad", "version": "0.20.1", @@ -18757,7 +18449,7 @@ "monaco-editor": "^0.52.2", "qrcode": "^1.5.3", "shiki": "^3.13.0", - "solid-js": "^1.8.0", + "solid-js": "^1.9.15", "solid-toast": "^0.5.0", "virtua": "^0.51.0", "yaml": "^2.4.2" @@ -18767,12 +18459,12 @@ "@vite-pwa/assets-generator": "^1.0.2", "autoprefixer": "10.4.21", "playwright": "1.63.0", - "postcss": "8.5.6", + "postcss": "8.5.28", "tailwindcss": "3", "typescript": "^5.3.0", - "vite": "^5.0.0", + "vite": "^6.4.3", "vite-plugin-pwa": "^1.2.0", - "vite-plugin-solid": "^2.10.0" + "vite-plugin-solid": "^2.11.10" } } } diff --git a/package.json b/package.json index c3e4b863c..c57ba5bce 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "packages/server", "packages/ui", "packages/electron-app", - "packages/tauri-app" + "packages/tauri-app", + "packages/mobile-app" ] }, "scripts": { @@ -19,6 +20,9 @@ "build": "npm run build --workspace @neuralnomads/codenomad-electron-app", "build:tauri": "npm run build --workspace @codenomad/tauri-app", "build:ui": "npm run build --workspace @codenomad/ui", + "build:mobile": "npm run build --workspace @codenomad/mobile-app", + "typecheck:mobile": "npm run typecheck --workspace @codenomad/mobile-app", + "test:mobile": "npm run test --workspace @codenomad/mobile-app", "build:mac-x64": "npm run build:mac-x64 --workspace @neuralnomads/codenomad-electron-app", "build:binaries": "npm run build:binaries --workspace @neuralnomads/codenomad-electron-app", "typecheck": "npm run typecheck --workspace @codenomad/ui && npm run typecheck --workspace @neuralnomads/codenomad-electron-app", @@ -29,14 +33,15 @@ "google-auth-library": "^10.5.0" }, "devDependencies": { - "baseline-browser-mapping": "^2.9.11" + "baseline-browser-mapping": "^2.11.0", + "rollup": "4.59.0" }, "optionalDependencies": { - "@rollup/rollup-darwin-arm64": "4.52.5", - "@rollup/rollup-darwin-x64": "4.52.5", - "@rollup/rollup-linux-arm64-gnu": "4.52.5", - "@rollup/rollup-linux-x64-gnu": "4.52.5", - "@rollup/rollup-win32-arm64-msvc": "4.52.5", - "@rollup/rollup-win32-x64-msvc": "4.52.5" + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0" } } diff --git a/packages/electron-app/package.json b/packages/electron-app/package.json index 02c211552..579c86579 100644 --- a/packages/electron-app/package.json +++ b/packages/electron-app/package.json @@ -55,8 +55,8 @@ "pngjs": "^7.0.0", "tsx": "^4.20.6", "typescript": "^5.3.0", - "vite": "^5.0.0", - "vite-plugin-solid": "^2.10.0" + "vite": "^6.4.3", + "vite-plugin-solid": "^2.11.10" }, "build": { "appId": "ai.neuralnomads.codenomad.client", diff --git a/packages/mobile-app/.gitattributes b/packages/mobile-app/.gitattributes new file mode 100644 index 000000000..11d041632 --- /dev/null +++ b/packages/mobile-app/.gitattributes @@ -0,0 +1,2 @@ +# Preserve verified upstream archive bytes and the native ABI integrity receipt. +src-tauri/vendor/** -text diff --git a/packages/mobile-app/.gitignore b/packages/mobile-app/.gitignore new file mode 100644 index 000000000..a9d42c7e8 --- /dev/null +++ b/packages/mobile-app/.gitignore @@ -0,0 +1,13 @@ +/dist/ +/test-results/ +/src-tauri/target/ +/src-tauri/gen/ +/src-tauri/recovery/.tauri/ +/src-tauri/recovery/android/.tauri/ +# Historical environment-bound compiler probe: preserve locally, never publish or replay. +/tests/native/compile-ios-abi.py +/src-tauri/recovery/android/build/ +/src-tauri/recovery/ios/.build/ +/src-tauri/recovery/permissions/autogenerated/ +*.jks +keystore.properties diff --git a/packages/mobile-app/AGENTS.md b/packages/mobile-app/AGENTS.md new file mode 100644 index 000000000..619b25cd7 --- /dev/null +++ b/packages/mobile-app/AGENTS.md @@ -0,0 +1,7 @@ +# Mobile native dependency corrections + +- This package has a separate Cargo workspace. Its Tauri 2.12.1 path patch belongs only here; do not add it to desktop/root Cargo configuration or mutate shared Cargo/Swift caches. +- Preserve `src-tauri/vendor/tauri-2.12.1` upstream licenses, `Cargo.toml.orig`, archive/file receipts and exact local diff. See `IOS_ABI.md` before changing this override. +- Native iOS compilation must run the vendored pre-link ABI hash/version guard, and the app must validate `DEP_TAURI_IOS_LIBRARY_PATH` against the owned vendor path. Do not bypass these checks or silently regenerate receipts on dependency drift. +- `ios:init`/`ios:build` preflight uses `scripts/verify-ios-abi.mjs`. Preserve SwiftRs's exact reviewed Swift commit and Rust lock/checksum; a Cargo version is not a SwiftPM tag. +- SwiftRs buffer lifetime warnings are not fixed by this command ABI correction. Never silence them or claim full Rust/Swift runtime qualification from Swift component/LLVM IR compilation alone. diff --git a/packages/mobile-app/ANDROID_TRANSPORT.md b/packages/mobile-app/ANDROID_TRANSPORT.md new file mode 100644 index 000000000..ce1e159a1 --- /dev/null +++ b/packages/mobile-app/ANDROID_TRANSPORT.md @@ -0,0 +1,96 @@ +# Android transport policy + +The recovery plugin owns `android/src/main/AndroidManifest.xml` and +`res/xml/codenomad_mobile_network_security.xml`. Gradle merges its +`android:networkSecurityConfig` into the generated application's manifest. +No ignored `gen/android` file is patched. The uniquely named policy applies to +debug and release builds: cleartext denied, **system CA trust only**, no user CA, +debug CA, custom trust manager or SSL bypass. + +On supported Android versions (API 26+), a Network Security Configuration is +authoritative even if Tauri's generated debug manifest sets +`usesCleartextTraffic=true`. Do not judge the APK from that legacy flag alone; +inspect its actual application NSC reference **and packaged XML resource**. +This build does not change Tauri's generated flag or enable HTTP development. + +## API 37 localhost behavior + +[Official Android guidance](https://developer.android.com/privacy-and-security/security-config#localhost) +describes the new implicit localhost rule allowing cleartext from API 37. +`localhost`, `ip6-localhost`, and numerical loopbacks count as localhost. + +Confirmed against Android 17's official AOSP Conscrypt source at revision +`fd1a430323b5ab9945a15fce6c4a824aeffff685`: + +- [XmlConfigSource.java](https://android.googlesource.com/platform/external/conscrypt/+/fd1a430323b5ab9945a15fce6c4a824aeffff685/nsc/src/android/security/net/config/XmlConfigSource.java): + any explicit loopback domain sets `isLocalhostDefined`; the implicit permitting + configuration is created **only if no such domain exists**. +- [ApplicationConfig.java](https://android.googlesource.com/platform/external/conscrypt/+/fd1a430323b5ab9945a15fce6c4a824aeffff685/nsc/src/android/security/net/config/ApplicationConfig.java): + explicit domain rules win; without an implicit localhost configuration, + unmatched hostnames use the base configuration. + +Our explicit denying domains are `localhost` / `ip6-localhost` (including +subdomains), `127.0.0.1`, and `::1`. Other 127/8 addresses, bracketed/expanded +IPv6 loopbacks and alternate spellings fall back to the denying base, not an +implicit permitting localhost configuration. No cleartext exception is added. + +`RecoveryPlugin.load` synchronously sets `MIXED_CONTENT_NEVER_ALLOW` before +hosted navigation. Wry's `use_https_scheme(true)` continues to serve bundled +`https://tauri.localhost` assets and `https://ipc.localhost` IPC through protocol +interception; these are not real loopback HTTP servers. The adapter delegates +subresource/IPC interception to Wry, without applying selected-origin +main-frame admission to fetch/XHR. Hosted HTTPS login POST, uploads and SSE +are unchanged; top-level non-GET denial, TLS-error cancellation, native +generation/lifecycle fencing and zero remote native capabilities remain intact. + +## Regression checks + +`npm run test --workspace @codenomad/mobile-app` runs source contracts and, with +an existing JDK 17+, parses the **production XML and manifest** using Java's XML +parser (`tests/native/TransportPolicyTest.java`). With no JDK, that XML check +reports a skip; an Android artifact build must not treat a skip as qualification. +The test checks the actual manifest resource reference, explicit denying base +and domain rules, inherited system-only trust, no debug override, and structural +hostname-rule selection for named/IPv4/IPv6 loopback variants and Wry hosts. + +To check a real Gradle-merged manifest, from `packages/mobile-app`: + +```powershell +java --source 17 tests/native/TransportPolicyTest.java +``` + +APK inspection must additionally check binary manifest, resource table and +binary NSC, signature, ABI/alignment, packaged recovery classes and current +launcher assets. Reusable read-only artifact checks (writes are evidence only): + +```powershell +# Existing isolated SDK/JDK tools must already be on PATH; no tools are installed. +./tests/native/verify-transport-apk.ps1 -Apk -Evidence +node tests/native/verify-embedded-assets.mjs /packaged-libcodenomad_mobile_lib.so /embedded-assets.json +``` + +The APK check binds the binary manifest NSC ID to the actual resource table, +then checks the packaged XML. SDK `apkanalyzer resources xml` drops domain text +nodes; the verifier instead uses `aapt dump xmltree`'s strict E/A/C output, +preserving those text nodes and failing on unknown lines. It never substitutes +the source XML for the APK policy. DEX checks confirm the actual `load` method +sets mixed-content mode to 1 (`MIXED_CONTENT_NEVER_ALLOW`). + +Static/JVM tests and a successful build **do not prove absence +of WebView network traffic**. No device/emulator was used for these checks. + +## Required authorized device pilot + +Use fixture servers and traffic capture on supported Android/API 26 and API 37 +with an updated System WebView. Verify bundled launcher + IPC, hosted trusted +HTTPS sign-in/cookies/upload/SSE/reconnect and native return/reopen. From the +HTTPS fixture, attempt HTTP fetch/XHR, image/script/frame, WebSocket, navigation +and HTTPS-to-HTTP redirects to a remote host, `localhost`, `ip6-localhost`, +127/8 variants, `[::1]` and expanded IPv6: neither cleartext destination should +receive a request. Include mixed-content targets and TLS bad-chain/hostname, +user-CA-only and self-signed failures (no bypass). Verify GET redirect fencing, +307/308 top-level non-GET rejection, denied remote native invoke, competing +connections/return, renderer death/Activity close and explicit reopening. + +This is a development-signed ARM64 pilot APK, not device-qualified delivery, +a release-signed artifact, a public-server qualification, or an iOS result. diff --git a/packages/mobile-app/DEPENDENCIES.md b/packages/mobile-app/DEPENDENCIES.md new file mode 100644 index 000000000..b21690173 --- /dev/null +++ b/packages/mobile-app/DEPENDENCIES.md @@ -0,0 +1,57 @@ +# Targeted dependency remediation evidence + +Scope: mission `msn_04136161b0fb739cd5a71a2c`, isolated Windows worktree only. These checks do not qualify an Android APK, iOS build, public server deployment, or store release. + +## Resolved versions + +| Dependency | Before | Locked after | Relevant official advisory | +|---|---|---|---| +| solid-js | 1.9.10 | 1.9.15 | Seroval chain below | +| seroval / seroval-plugins | 1.3.2 / 1.3.3 | 1.5.6 / 1.5.6 | [GHSA-mv8w-475r-vwqw](https://github.com/advisories/GHSA-mv8w-475r-vwqw), patched Seroval ≥1.5.3 | +| vite | 5.4.21 | 6.4.3 | [Windows deny bypass](https://github.com/advisories/GHSA-fx2h-pf6j-xcff), [UNC credential disclosure](https://github.com/advisories/GHSA-v6wh-96g9-6wx3) | +| postcss | 8.5.6 | 8.5.28 | [GHSA-fxqj-rqcc-2cmp](https://github.com/advisories/GHSA-fxqj-rqcc-2cmp), patched ≥8.5.23 | +| nanoid | 3.3.11 | 3.3.19 | [GHSA-2v37-7h3g-55p8](https://github.com/advisories/GHSA-2v37-7h3g-55p8), patched 3.x ≥3.3.18 | +| browserslist | 4.28.1 | 4.29.3 | [memory growth](https://github.com/advisories/GHSA-c83g-rgw3-j3cx), [custom stats](https://github.com/advisories/GHSA-73wf-gq98-2v4g), patched ≥4.28.7 | +| baseline-browser-mapping | 2.9.11 | 2.11.27 | [GHSA-w5vr-8v7q-w6rv](https://github.com/advisories/GHSA-w5vr-8v7q-w6rv), patched ≥2.11.0 | +| @babel/core | 7.28.5 | 7.29.7 | [GHSA-4x5r-pxfx-6jf8](https://github.com/advisories/GHSA-4x5r-pxfx-6jf8), patched ≥7.29.6 | +| @babel/plugin-transform-modules-systemjs | 7.29.0 | 7.29.8 | [GHSA-fv7c-fp4j-7gwp](https://github.com/advisories/GHSA-fv7c-fp4j-7gwp), affects through 7.29.3 | +| rollup | 4.52.5 | 4.59.0 | [GHSA-mw96-cpmx-2vgc](https://github.com/advisories/GHSA-mw96-cpmx-2vgc) | +| workbox-build → rollup | 2.79.2 | 2.80.0 | Same advisory, separate compatible 2.x patched line | + +Published versions/dependency declarations were verified against the [official npm registry](https://registry.npmjs.org/), e.g. [Solid 1.9.15](https://registry.npmjs.org/solid-js/1.9.15), [Vite 6.4.3](https://registry.npmjs.org/vite/6.4.3), and [PostCSS 8.5.28](https://registry.npmjs.org/postcss/8.5.28). Solid 1.9.15 requires Seroval and its plugins `~1.5.4`; no override forces that dependency downward. Babel packages do not all publish the same version number: this change targets the affected core/SystemJS packages, not a fabricated blanket `7.29.6` pin. + +## Compatibility and change boundary + +- UI/mobile declare Solid `^1.9.15`; UI/mobile/Electron declare Vite `^6.4.3` and vite-plugin-solid `^2.11.10`. No Vite 8 upgrade. +- [vite-plugin-solid 2.11.10](https://registry.npmjs.org/vite-plugin-solid/2.11.10), [electron-vite 4.0.1](https://registry.npmjs.org/electron-vite/4.0.1), and [vite-plugin-pwa 1.2.0](https://registry.npmjs.org/vite-plugin-pwa/1.2.0) accept Vite 6. Electron-vite requires Node `^20.19.0 || >=22.12.0`; checks used Node 25.2.1, and Node 24 meets the declared range. +- Root Rollup is deliberately pinned as a build dependency to **4.59.0**, and all six existing root optional native Rollup packages are aligned to **4.59.0**. This prevents an old hoisted Rollup/native ABI mismatch without forcing Workbox onto an incompatible major. +- Workbox's `^2.79.2` Rollup range resolves **2.80.0**. Its plugin-babel accepts Rollup 1/2, so no blanket Rollup-major override is used. +- Only named transitive update targets (`rollup`, `browserslist`, `@babel/core`, `@babel/plugin-transform-modules-systemjs`) were refreshed, along with their required dependency/native-platform nodes. No `npm audit fix`, forced override, Electron runtime update, Fastify migration, or unrelated feature upgrade. +- Reviewed the [Vite 6 migration guide](https://v6.vite.dev/guide/migration). Existing JS PostCSS and UI/mobile/Electron Vite configs build without a config migration. Electron main/preload CommonJS compilation was explicitly exercised. +- Native mobile Rust/Kotlin/Swift security/runtime sources, capability files, endpoint policy, SDK setup and Android workflow remain unchanged by this dependency task. Existing uncommitted changes were preserved. + +## Validation and artifacts + +All commands run from the isolated repository root. Logs and machine-readable reports are under the ignored `packages/mobile-app/test-results/dependency-fix/` directory: + +- `audit-before.json`, `audit-after.json`: fresh full-workspace npm audits. +- `inventory-before.json`, `inventory-after.json`, `installed-tree-after.json`: installed graph/peer evidence; full `npm ls --all --json` completed without problems. +- `lock-before.json`, `lock-version-delta.json`: pre-task lock snapshot and targeted version-node delta (83 entries, including platform additions/removals and obsolete Vite-5 nested esbuild packages). +- `install-targeted.log`, `update-targeted-transitives.log`: installs used `--ignore-scripts --no-audit --no-fund`; no lifecycle scripts or global provisioning. +- `ci-reinstall.log`, `mobile-tests-after-ci.log`: a clean `npm ci --ignore-scripts --no-audit --no-fund` succeeded and all **11 mobile tests passed again**. The first bounded clean-install attempt timed out; no remaining writer was found before the successful longer retry. +- `mobile-typecheck.log`, `mobile-tests.log`: typecheck and **11 tests** passed, including the new real-bundle/import-graph regression. +- `mobile-browser-build.log`: production launcher build and browser test passed across **all ten locales**, including RTL. +- `ui-typecheck.log`, `ui-build.log`: shared UI typecheck and production/PWA build passed. Existing large-chunk warnings remain. +- `ui-auth-attachments.log`: **26 real auth-recovery/device-attachment browser regressions passed**, including hosted mobile login/no replay, drafts and native-vs-device attachments. +- `electron-typecheck.log`, `electron-js-build.log`: Electron typecheck and **main/preload/renderer JavaScript compilation** passed. The command was `npm exec --workspace @neuralnomads/codenomad-electron-app -- electron-vite build`, deliberately bypassing packaging/resource-provisioning lifecycle scripts. No native app packaging or application launch. +- `hosted-ui-artifact-inspection.json`: inspected **320 generated JS source maps / 1,584 distinct source paths**. Client Solid entry points were present; Seroval, seroval-plugins and Solid SSR modules were absent. Hosted HTML and PWA service worker were generated. + +`tests/bundle.test.ts` inspects the real launcher's Rollup chunk module graph with the real production config and all ten dynamic locale chunks. It rejects Seroval/SSR modules and deserialization markers, and includes negative cases proving the guard rejects those paths. This is bundle/import evidence, not proof about every hosted-server dependency or native WebView behavior. + +## Residual alerts and next gate + +Full-workspace npm audit decreased **56 → 45** findings: after remediation **2 critical, 36 high, 4 moderate, 3 low**. All named targeted dependency entries above disappeared from the fresh audit. **This is not a zero-vulnerability claim.** + +Residuals include critical `@fastify/reply-from` and `tar`; high Fastify/static/router/Undici chains, Electron/runtime/packaging chains, Sharp/PWA asset generation, XML parsing, glob/matching and other utilities; and Workbox/terser/serialize-javascript and AJV-related alerts. Their complete affected paths/ranges are in `audit-after.json`. They require separately reviewed remediation/compatibility work rather than unsafe major overrides. Do not infer public-server safety or store readiness from this targeted improvement. + +No Android/iOS/native application build, SDK/license mutation, VM/Xcode setup, Apple account/signing operation, publication, commit, or shared-daemon operation occurred. **The existing Mac source snapshot was not modified and must be refreshed only after independent approval of these manifest/lock/test changes**, followed by independent Mac/iOS validation. Android compilation/device checks likewise remain a separate authorized gate. diff --git a/packages/mobile-app/IOS_ABI.md b/packages/mobile-app/IOS_ABI.md new file mode 100644 index 000000000..54ca9156b --- /dev/null +++ b/packages/mobile-app/IOS_ABI.md @@ -0,0 +1,174 @@ +# iOS command C ABI correction — Tauri 2.12.1 + +Scope: the separate `packages/mobile-app/src-tauri` Cargo workspace in the +mission-owned Windows worktree. No desktop/root dependency override, Android +source edit, cache edit, full app build, signing, runtime/device test or publication. + +## Production mismatch and fix + +The actual Rust declaration in vendored `src/ios.rs` takes an `i32` request ID +and `unsafe extern "C" fn(c_int, c_int, *const c_char)` response callback. +`src/plugin/mobile.rs` allocates IDs through `AtomicI32`, removes pending calls +by that ID, and treats success as `success == 1`. Recovery's production +`run_mobile_plugin` calls reach this path. + +Upstream Swift `run_plugin_command` instead used `Int` and an `(Int, Bool, …)` +callback. On both reviewed 64-bit targets this is a genuine C signature mismatch. +The local Swift export now takes `Int32`, its callback takes `(Int32, Int32, …)`, +and its response closure passes `success ? 1 : 0`. Rust's existing contract is +unchanged. `Invoke` internally already uses independent `UInt64` callback/error +IDs `0`/`1`; no request-ID conversion or change to `Invoke` is necessary. + +## Owned dependency inputs and provenance + +The official cached Tauri crate was SHA256-verified before safe extraction: + +- Version: **2.12.1**; archive **324,974 bytes**, **142 files**, **1,472,118 bytes** unpacked. +- Official source: `https://static.crates.io/crates/tauri/tauri-2.12.1.crate`. +- Archive SHA256: `ed99ee9694a2deb776d91cae48ac7411ddfc89ecae2f9b5041111d8c88f2ace9`. +- `src-tauri/vendor/tauri-2.12.1-upstream.json`: exact original per-file hashes/sizes. +- Upstream `LICENSE-MIT`, `LICENSE-APACHE-2.0`, `Cargo.toml.orig`, VCS identity and copyright headers remain present. +- `src-tauri/vendor/tauri-2.12.1-local.json`: exact modified/added hashes and dependency identities. +- `src-tauri/vendor/tauri-2.12.1-codenomad.patch`: complete local diff, including the added native guard and receipt. +- Diff SHA256: `7416bbd978eb088706ad61439b5ac06a2632512c13aa7ebb4794c4138c9f300e`. +- Vendored bytes use package-local `.gitattributes` `-text` to preserve archive/receipt identity across checkout platforms. + +Only four upstream files differ: Swift command signature/response conversion, +Swift Package dependency pin, Tauri Cargo manifest (existing locked SHA-256 +crate added as a build dependency), and `build.rs` (pre-link guard hook). +Two new files implement the guard and its 13-input receipt. All original Rust +ABI/caller and Android files match the archive. The local patch is reusable and +idempotent: no build-time copy/patch of a registry cache or network acquisition. + +SwiftPM now requests the exact reviewed SwiftRs **1.0.7** commit +`f64a4514de07f450ec5b6aa297624cd3479d9579`, not an unconstrained version range. +Its `src-swift/lib.swift` SHA256 is +`d666f824f48ed780eddefc213c3def0faa8b9cf017ea0febbde3829f4efd58e3`. +Rust remains locked to **swift-rs 1.0.8**, archive checksum +`e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e`. +The reviewed call/retain/Drop semantics are compatible; this is not a claim of +a linked/runtime ABI test or a fabricated SwiftPM 1.0.8 release. + +The pin uses the tools-5.3-compatible named package requirement +`.package(name: "SwiftRs", url: "…", .revision("…"))`, retaining the original +target dependency name. Real `swift package dump-package` accepted this final +manifest and returned the exact revision requirement, without fetching packages. +The newer labeled `revision:` overloads require PackageDescription 5.5 and were +rejected by the real manifest compiler; those failed attempts are retained as +`SWIFTPM-PIN-r1.json`/`r2.json`, with final success in `r3.json`. Do not restore +those overloads while keeping `swift-tools-version:5.3`. + +The final application Cargo lock changes **one package entry only**: Tauri loses +its registry source/checksum because it is a path package, and gains the already +locked `sha2` dependency. No other versions/checksums were changed. An initial +Windows offline update attempted unrelated downgrades because of incomplete +local caches; those were discarded from this task's changes. The independently +reviewed frozen lock was retained with only the intended local Tauri delta. + +## Exact native/generated build route + +1. The app/recovery still require `tauri = "=2.12.1"`. The mobile workspace's + `[patch.crates-io] tauri = { path = "vendor/tauri-2.12.1" }` resolves both to + the source-owned crate; `exclude` prevents accidental vendor workspace membership. +2. `ios:init` and `ios:build` first run `ios:verify`. It checks all 142 original + files against original/patched identities, the new guard inputs, exact diff, + lock identities, real Rust callers/Swift export and native guard ordering. +3. On an iOS target, vendored Tauri `build.rs` runs the hash/version guard **before** + its unchanged `tauri_utils::build::link_apple_library("Tauri", &lib_path)`. + `lib_path` is its own `CARGO_MANIFEST_DIR/mobile/ios-api`; no registry location + or generated-project fallback is used. Its metadata publishes `ios_library_path`. +4. `tauri-plugin 2.7.1`'s real iOS build integration consumes + `DEP_TAURI_IOS_LIBRARY_PATH` and copies that package to + `recovery/.tauri/tauri-api` before building Recovery's Swift package. + Recovery's unchanged `../.tauri/tauri-api` dependency therefore receives the + **same corrected Swift source and exact SwiftRs commit pin**. +5. The application `build.rs` additionally canonicalizes and asserts that + `DEP_TAURI_IOS_LIBRARY_PATH` is the owned vendor package. Selecting an unpatched + dependency fails closed rather than silently linking it. + +This is normal Cargo/Tauri native build wiring, not a custom runtime shim. +Both Windows and Mac `cargo pkgid --offline --locked -p tauri` actually selected +the vendor package. `cargo metadata --offline --locked --no-deps` succeeded on +both. **No `tauri ios init`, generated Xcode app build or native linker was run**; +the future generated app/link/runtime gate remains outstanding. + +Full offline metadata/fresh application dependency checking remains blocked: +Windows lacks `android_system_properties 0.1.6`; the copied, owned Mac Cargo +cache lacks `core-foundation 0.10.1` (and inventory shows further missing archives). +No dependencies were downgraded to make that check pass and no download occurred. +Do not use `--no-deps` metadata as proof of a complete freshly resolved/buildable graph. + +## Regression and compiler evidence + +From the repository root: + +```text +npm run ios:verify --workspace @codenomad/mobile-app +npm run test --workspace @codenomad/mobile-app +npm run typecheck --workspace @codenomad/mobile-app +``` + +From `packages/mobile-app`, the native guard's production Rust module is exercised +directly by the separate, offline test fixture (no copied contract implementation): + +```text +cargo test --offline --locked --manifest-path tests/native/ios-abi-guard/Cargo.toml --target-dir test-results/ios-abi/guard-target -j 1 +``` + +- Mobile JavaScript tests: **16 passed, 1 existing JDK-dependent Android XML check skipped**. +- Native Rust guard: **3 passed**; real sources pass, version drift fails, original + Swift export and arbitrary Rust source drift fail. JavaScript checks also reject + each original ID/success/callback mismatch and a changed Cargo patch route. +- Mobile TypeScript checking passed. +- Mac probe `ios-abi-patch-20261005-8f3061-r2` compiled genuine **SwiftRs, Tauri, + Recovery modules and objects** for `x86_64-apple-ios15.0-simulator` and + `arm64-apple-ios15.0`, SDK **26.2**, Swift language mode **5**, serial backend. +- **16 compiler commands succeeded**: 12 module/object commands plus 4 LLVM IR + commands (corrected and unmodified upstream sources on each target). +- A separate final SwiftPM manifest compilation/dump also passed; this validates + the supported manifest API and exact resolver requirement, not package resolution + or an application link. The reproducible probe now includes this gate. +- Corrected LLVM IR on both targets: + `define hidden void @run_plugin_command(i32 …, ptr …)` and the actual response + closure's indirect `call void %4(i32 %5, i32 %14, ptr %46)`. +- The actual original sources compile to `@run_plugin_command(i64 …)` and do not + pass the same callback gate: a negative control with the real upstream compiler, + not merely a source regex or invented ABI model. +- Corrected IR SHA256: Intel `2aad274ba81813e6fbef7427d502d1dffced06d4b172078593801b96c3f931a6`; + ARM64 `783e584d09071a94884c662bfb786aa51bd42fa2ab09318680721c69c75a0980`. +- Original IR SHA256: Intel `3b28e57c9b77e7675fd099f24267f0189da53c6389790818df2409149c703c87`; + ARM64 `6ef42d746d53aff67478ce00678750e290b518fe1b938772003a6ad250f6c188`. + +Ignored `test-results/ios-abi/` retains transfer identity, `RESULT-r1.json` +(first driver-only multiple-output `-o` failure), successful `RESULT-r2.json`, +actual LLVM files/compiler logs under `native/`, download hashes, +`CARGO-RESULT.json` (bounded full-metadata failure), native `CARGO-ROUTE.json`, +lock delta and test logs. The corrected driver uses per-source output-file maps; +no production source changed to fix that compiler invocation. + +## Safety and remaining qualification + +Owned Mac copies only, including copied Cargo caches. Old frozen source, repo, +environment, shared Cargo archive/cache receipts, previous Swift outputs and +Xcode version hashes were unchanged. Process-only `DEVELOPER_DIR` selected the +authorized guarded Xcode; global CLT selection remains unchanged. Fresh `df` +checks retained a 13GiB reserve; aggregate added allocation remained under +512MiB (**436,461,568 bytes**, about 416MiB including both probes, copied Cargo +cache and SwiftPM manifest caches), serial compiler commands under the bounded +time watchdog. No signing, +device boot, accounts, services, sudo, installer, app link or FFI execution. + +**SwiftRs's dangling pointer warnings remain visible and unfixed.** No examined +request/response caller reads its escaped generic buffer pointers through Rust; +that narrows this path's reachability, not the safety of SwiftRs's general API. +Unchanged Any?/UIButton warnings also remain. Necessary later work: complete +the exact locked native graph in an owned cache, generated app link, real +Rust→Tauri→Recovery round trips including extreme `i32` IDs, success/reject, +async lifetime/pool-drain checks and WKWebView recovery/device tests. Release/LTO +and memory diagnostics remain untested. + +Size warnings: locally modified vendor `build.rs` is **505 lines**. Vendoring +necessarily retains upstream large modules (e.g. `src/app.rs` 2,833, +`src/webview/webview_window.rs` 2,874, `src/window/mod.rs` 2,620 lines). +The complete 24-file >500-line inventory is in `oversized-vendor.json`; these +upstream files were not refactored merely to meet local size guidelines. diff --git a/packages/mobile-app/README.md b/packages/mobile-app/README.md new file mode 100644 index 000000000..10f546fa5 --- /dev/null +++ b/packages/mobile-app/README.md @@ -0,0 +1,142 @@ +# CodeNomad mobile companion — Android-first prototype + +This is a **remote client**, not the desktop Tauri host ported to a phone. A bundled, localized Solid launcher asks for the root HTTPS origin of an existing CodeNomad server. One webview navigates to that server as its **top-level document**, preserving its same-origin `HttpOnly; SameSite=Lax` sign-in cookies, SDK requests and SSE stream. + +No Node, local CodeNomad backend, OpenCode executable, Git, desktop plugin, local workspace access, or TLS-verification bypass is bundled. The launcher shares UI design tokens and each UI locale's remote-access messages; it does not mount server-backed preference stores. Mobile-specific copy is in `src/messages.ts` for all ten UI locales. + +## Security contract + +Android transport now uses a source-owned, merged Network Security Configuration +and explicit mixed-content denial. See [Android transport policy and artifact +checks](ANDROID_TRANSPORT.md), including API 37's implicit localhost exception +and remaining device gates. Historical validation notes below are not a claim +of device-qualified delivery. + +- Accept root HTTPS origins only (optional trailing `/` and explicit port). Reject userinfo, paths, query strings, fragments, whitespace/control characters, backslashes, encoded authorities, Unicode host spelling, loopback and launcher-host collisions. Use an ASCII punycode hostname for internationalized domains. +- Validate independently in JavaScript and Rust. Native commands additionally require the trusted launcher URL and main webview label. Browser input validation alone is not authority. +- The Rust navigation policy allows top-level navigation/redirects only within the selected origin; returning to the exact bundled launcher revokes remote selection. Readiness is a **side-effect-free native generation snapshot**, never endpoint selection. Rust first commits its own generation, then a serialized platform UI-thread transaction checks the native snapshot and live launcher, selects the endpoint and starts top-level loading. Native return, same-URL reload, document/webview replacement and renderer death invalidate snapshots; duplicate/stale transactions cannot overwrite a newer selection. A failed native commit rolls back only its own Rust generation. +- Android wraps Wry's native client and **rejects every non-GET main-frame request before delegating networking**, even when its initial URL is same-origin. Top-level form POST/PUT/DELETE/HEAD/etc is unsupported. This prevents a 307/308 response from preserving a POST/body while redirecting off-origin without another interception callback; `onPageStarted` recovery is not relied on as pre-request admission. Subresource fetch/XHR API POSTs remain supported: the existing server login uses `fetch('/api/auth/login', { method: 'POST' })` then `location.replace('/')`, not top-level form POST. GET redirects still use the selected-origin navigation fence. Cross-origin OAuth, external links, downloads, popup windows, HTTP downgrade and subpath deployments are unsupported. Historical Android compilation passed, but device behavior remains unverified; source/policy tests are not proof of Android network behavior. +- Before hosted modules run, native initialization sets `__CODENOMAD_RUNTIME_HOST__ = 'web'` and `__CODENOMAD_WINDOW_CONTEXT__ = 'remote'`. The actual host override name is **RUNTIME_HOST**, not HOST. Android connections fail closed unless System WebView supports `DOCUMENT_START_SCRIPT`; update System WebView if the launcher reports unsupported recovery/runtime setup. Tauri's fallback `onPageStarted` injection is not early enough for hosted startup. +- There are **zero remote native capability grants**. Only the bundled launcher gets `connect_server`. Custom-command permissions are explicitly registered in `build.rs`; the recovery plugin has no frontend permissions. Its readiness and generation-checked connection transactions are called only from Rust. Android WebView access and iOS UIKit/WKWebView checks, navigation and result resolution run on their platform UI/main thread. +- The Tauri CSP protects bundled assets; the hosted server must supply its own CSP. The origin fence protects top-level navigation, not every remote subresource. Treat the entire selected server as trusted. +- Native recovery is an Android `Button` outside the WebView / iOS `UIButton` outside WKWebView. It calls platform navigation to a hard-coded local launcher, never remote IPC, DOM, URL parameters or history. It is not a second webview. It covers ordinary server/network/TLS and JavaScript errors **while the WebView and native UI thread remain alive**. Android `onRenderProcessGone` is explicitly handled: revoke selection, detach/destroy the unusable WebView and close the Activity. **Reopen the app** after renderer death (force-stop and reopen if the resident host does not reinitialize); there is no seamless renderer-crash recovery, dead-WebView `loadUrl`, or automatic restart. A frozen UI thread, OS termination or app-process crash likewise needs operator reopening. The compact control can cover a small bottom corner of hosted content. + +**Return/disconnect is not logout:** the native `↩ CodeNomad` control stops loading and returns to the launcher, clearing native remote selection. It intentionally does not clear cookies or website storage, log out, cancel running server work, or remove drafts. Sign out in the hosted UI to end authentication. Reconnect to the same server to resume. Unsaved browser-only state can be lost by navigation; server drafts/session data remain on the server. No endpoint/password is saved by the launcher. Per-server website storage can persist until app data is cleared. + +## Network and deployment + +The server must be reachable **from the phone** and have a valid trusted HTTPS certificate (correct hostname, full chain). `localhost` on the phone is not the development PC. Prefer a private VPN/tunnel and access-controlled HTTPS endpoint. This package deliberately does not install certificates or disable Android network security / iOS App Transport Security. + +**CodeNomad's server is not yet qualified for public Internet exposure.** Review authentication lifetime/revocation, rate limiting, credentialed CORS on non-loopback binds, CSRF/origin protection, trusted reverse-proxy headers and preview isolation before public deployment. Do not expose the OpenCode daemon directly or disable CodeNomad authentication. A reverse proxy/VPN does not replace application authorization. Existing client/runtime flags are not server authorization controls. + +## Development checks + +See [targeted dependency remediation evidence](DEPENDENCIES.md) for locked versions, compatible peer ranges, bundle/import-graph checks and remaining workspace audit alerts. Frozen Mac snapshots remain preserved; GitHub integration is not a refresh or qualification of those snapshots. + +From the repository root: + +```text +npm ci --ignore-scripts +npm run build:mobile +npm run typecheck:mobile +npm run test:mobile +npm run test:browser --workspace @codenomad/mobile-app +npm run rust:policy --workspace @codenomad/mobile-app +npm run rust:fmt --workspace @codenomad/mobile-app +npm run rust:check --workspace @codenomad/mobile-app +``` + +The policy crate runs URL/authority/redirect tests without building a webview. The full Rust check on Windows validates the shared host but **does not compile Kotlin/Swift or validate a phone**. Tauri/CLI are pinned to 2.12.1 with a separate Cargo lockfile; checks used Rust 1.94.0. The Tauri CLI is workspace-local. No global CLI installation is needed. + +Desktop compilation is for validation only; desktop connections deliberately fail readiness because this package provides recovery chrome only on mobile. Continue using the existing Electron/Tauri desktop hosts for desktop use. npm currently deduplicates the mobile CLI 2.12.1 into the existing desktop workspace's compatible `^2.9.4` range; this changes the root npm lockfile's CLI resolution, not desktop source or its Cargo lockfile. + +The isolated launcher browser test needs Playwright Chromium (`CODENOMAD_BROWSER_PATH` may select an existing executable). It exercises the real production Solid bundle with only native invoke mocked, checks all ten locales/RTL at phone width, and captures `test-results/`. It is not an Android/iOS webview test. Rebuild icons after changing `src-tauri/icon.svg` with `npm run tauri --workspace @codenomad/mobile-app -- icon src-tauri/icon.svg --output src-tauri/icons`. + +`tests/native/ConnectionAuthorityTest.kt` exercises the **production pure-JVM Android authority** (not a copied policy): side-effect-free readiness, competing selections, immediate return, same-URL document replacement, different WebView identities, renderer revocation and initial non-GET denial for 307/308 chains, while preserving fetch POST plus GET login navigation. With an already installed Kotlin compiler/JDK, from `packages/mobile-app`: + +```text +kotlinc src-tauri/recovery/android/src/main/java/ai/neuralnomads/codenomad/recovery/ConnectionAuthority.kt tests/native/ConnectionAuthorityTest.kt -include-runtime -d test-results/authority-tests.jar +java -jar test-results/authority-tests.jar +``` + +Create `test-results` if absent. No compiler/SDK is auto-installed by these checks. These JVM tests still do not exercise real WebView callbacks or networking; JavaScript contract tests check that the native adapter consults this production gate before delegating requests. Windows Rust tests cover generation rollback, duplicate admission and authority/redirect fencing. The standalone Kotlin test has not yet been compiled/run here. + +## Android prerequisites and artifacts + +Install/configure these explicitly outside this package (no automatic SDK installation or license acceptance): + +- Rust and an appropriate Android Rust target. ARM64 scripts require `rustup target add aarch64-linux-android`. +- Android Studio or equivalent command-line SDK setup; suitable JDK (Android Studio's bundled JBR is the official Tauri recommendation). The CI workflow uses Temurin **JDK 17**, the documented minimum for the generated Android Gradle Plugin 9.3. +- Android SDK Platform **37.0** (CLI 2.12.1's generated app targets API 37) and **36** (the recovery library compile SDK), Platform-Tools, Build-Tools, Command-line Tools, and side-by-side NDK. CI explicitly selects Build-Tools **36.0.0**, command-line tools **16.0 / build 12266719**, and NDK **28.2.13676358**. Prefer **NDK 28+** for Android 16 KiB page alignment. +- `JAVA_HOME`, `ANDROID_HOME`, and `NDK_HOME` pointing to real installations; accepted SDK licenses under the operator's control. +- `adb` and USB debugging / authorized device, or a configured emulator, for installation and actual device testing. `adb` is not required merely to produce a build artifact. + +Minimum Android API is **26 (Android 8.0)** because the recovery/navigation adapter uses `WebView.getWebViewClient()` to preserve Wry's existing client; core Tauri alone supports API 24. A current System WebView with document-start-script support is also required. Initial scripts intentionally build only ARM64; add matching Rust targets and change CLI targets to test x86_64 emulators or other architectures. + +```text +npm run android:init --workspace @codenomad/mobile-app +npm run android:debug --workspace @codenomad/mobile-app +npm run android:release --workspace @codenomad/mobile-app +npm run android:bundle --workspace @codenomad/mobile-app +``` + +Init creates `src-tauri/gen/android`; Gradle downloads/builds may require Internet access. The launcher is built automatically by Tauri's `beforeBuildCommand`. Inspect `src-tauri/gen/android/app/build/outputs/apk/` for the debug APK. Typical AAB location is `src-tauri/gen/android/app/build/outputs/bundle/universalRelease/app-universal-release.aab`; exact APK variant paths depend on generated Gradle configuration. + +Debug APK uses development signing. **Release scripts do not provision signing secrets:** configure the generated Gradle release signing config and private `keystore.properties` per the official guide before distributing. Never commit keystores/passwords. Use the same signing identity for updates. Google Play requires a developer account and signed AAB; this prototype has not passed store review. + +Verify on a real device: launcher localization/RTL and large text; fetch-based sign-in/cookie persistence; SSE/reconnect; network/TLS failures; same-origin and cross-origin GET redirects; same-origin form POST returning 307/308 off-origin (neither initial nor target server may receive that top-level POST); remote native-invoke denial; competing native connection requests interleaved with return/reload; native return while loading/offline; renderer termination followed by Activity close and explicit reopening (never reuse the destroyed WebView); keyboard, gesture navigation, rotation/insets; process restart; and drafts before/after returning. Verify multiwindow/popup behavior because Tauri's Rust `on_new_window` callback is **not supported on Android/iOS**; Android settings disable automatic/multiple windows, but these are not a substitute for device verification. + +### Pull-request validation and consent-gated GitHub Actions debug APK + +Workflow source: [`.github/workflows/mobile-android.yml`](../../.github/workflows/mobile-android.yml). Relevant PRs (including forks) run **Launcher, source contracts and Rust policy (not a native APK)** without accepting SDK licenses. Paths cover the companion/workflow, root lock/configuration, workspace manifests, shared launcher tokens/locales and the login-page source contract. It installs locked npm dependencies with `npm ci --ignore-scripts`, runs launcher typecheck/unit tests, verifies the source-owned Tauri ABI override/receipts, builds/tests the production launcher across all ten locales, and runs locked Rust URL/authority policy tests. Java 17 enables the production Android XML check; these are source/browser/policy results, **not Kotlin/Swift compilation or phone behavior**. + +The separate **ARM64 debug APK (explicit SDK license consent required)** job runs only after source validation passes and an authorized maintainer explicitly consents: + +- **PR builds:** set the repository Actions variable `CODENOMAD_ANDROID_ACCEPT_SDK_LICENSES` to `true`, only with authority to accept Android SDK licenses on disposable CI runners (GitHub string comparisons are case-insensitive). This authorizes SDK setup for every relevant PR, including fork code; standard GitHub fork-workflow approvals still apply. Missing/false/other values leave the job **skipped**, and the run summary explicitly says no APK was built. Maintainers must inspect this job, not infer APK success from a green source check. This change does not set the variable or alter repository security settings. +- **Manual builds:** once the workflow is on the default branch, an operator with write access can open [Actions → Mobile Android validation and debug APK](https://github.com/NeuralNomadsAI/CodeNomad/actions/workflows/mobile-android.yml), select **Run workflow**, choose the companion branch, and confirm the SDK-license checkbox **only if authorized**. An unchecked box skips the APK job even if the PR consent variable is enabled; source validation still runs. + +Both jobs use disposable `windows-2022` runners with Node 24, Java 17 and Rust 1.94.0. The APK job additionally installs `aarch64-linux-android` and the explicit SDK/NDK packages above. Windows avoids Linux GTK/WebKit desktop prerequisites. SDK setup accepts licenses **on that CI runner only** after the gate; nothing here installs or accepts licenses on a developer machine. External actions are pinned to official upstream commit SHAs. Checkout uses `github.sha` (the PR merge commit for PRs) without persisted credentials. The workflow uses only `contents: read`, no secrets, privileged PR trigger, shared build cache, release/upload-to-store or production signing credentials. Its actual native build commands are: + +```text +npm run android:init --workspace @codenomad/mobile-app -- --ci --skip-targets-install +npm run android:debug --workspace @codenomad/mobile-app -- --ci +``` + +The existing debug script supplies `--debug --apk --target aarch64`; `--ci` disables prompting and `--skip-targets-install` avoids initializing unrelated Rust architectures. Tauri builds the launcher again through `beforeBuildCommand`. The workflow fails if it cannot find a debug APK under the generated Gradle APK output directory, creates `SHA256SUMS.txt` and commit/build metadata, then uploads **only APK(s) and those text files** as `codenomad-android-debug-arm64--` with seven-day retention. Download the artifact from the successful run's **Artifacts** section and extract its ZIP. For a downloaded APK, `Get-FileHash .\.apk -Algorithm SHA256` should match its entry in `SHA256SUMS.txt`. + +This is a development-signed, debuggable **ARM64 test artifact**, not a GitHub Release, Play Store upload, AAB, or production-signed application. No signing secrets are used; ephemeral debug signing keys may differ between runs, so an existing installation may need removal before installing another run's APK (removal clears app-local data). Only install artifacts from a trusted repository/run. On an authorized ARM64 device with USB debugging, use `adb install .\.apk` and complete the real-device checklist above. Android 8/API 26 plus updated System WebView is required; this APK is not intended for x86_64 emulators. + +**The PR workflow and consent-gated build have not been executed on GitHub during local validation.** The runnable workflow contract check (`npm run test:mobile`) covers immutable action refs, read-only credentials, source/native separation and the real consent expression (including false/missing values and manual-vs-PR isolation). Local YAML/PowerShell checks do not verify runner package availability, generated SDK/Gradle compatibility, Kotlin compilation, native linking or actual APK output; those remain unverified until a successful remote APK job. The mobile-only Cargo patch and native pre-link ABI guards are preserved, not bypassed by CI. Even a successful build does not verify on-device authentication, navigation fencing or recovery. Review workflow/toolchain changes when updating the pinned CLI; no generated Gradle/source edits or compatibility bypasses are applied by this workflow. + +References: [manual dispatch prerequisites](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/manually-run-a-workflow), [downloading artifacts](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/download-workflow-artifacts), [SDK setup action and license behavior](https://github.com/android-actions/setup-android/tree/v3), [AGP 9.3 compatibility/JDK 17](https://developer.android.com/build/releases/agp-9-3-0-release-notes), and [pinned CLI Android template](https://github.com/tauri-apps/tauri/blob/tauri-cli-v2.12.1/crates/tauri-cli/templates/mobile/android/app/build.gradle.kts). + +## iOS — native component checks and future app build + +The app and Swift recovery package require **iOS 15.0 or newer**. Real SwiftRs/Tauri/Recovery modules and objects were compiled for Intel simulator and ARM64 device targets with SDK 26.2; this is **not an iOS application link or native FFI execution pass**. See [the source-owned Tauri C ABI correction, dependency pins and evidence](IOS_ABI.md). Local native iOS experimentation is stopped. The following commands describe future tooling, not an instruction to resume: agree a supported unsigned build contract first. The current `ios:build` requests distribution export and must not be used for default validation. A supported Mac with full Xcode (not just Command Line Tools), CocoaPods, Rust, Node/npm, and iOS Rust targets is required: + +```text +rustup target add aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim +npm run ios:init --workspace @codenomad/mobile-app +npm run ios:build --workspace @codenomad/mobile-app +``` + +The Swift package's `../.tauri/tauri-api` dependency is wired by Tauri's mobile tooling. Distribution requires Apple Developer enrollment, matching bundle ID, signing certificate and provisioning profile (automatic Xcode signing or protected CI secrets). The script requests `app-store-connect` export; configure signing first. Expected IPA output: `src-tauri/gen/apple/build/arm64/CodeNomad Mobile.ipa` (confirm generated product name/path). Simulator builds are not distributable device IPAs. Test safe-area overlay attachment, accessibility, navigation fencing and recovery on the Mac/device before claiming iOS readiness. No Windows cross-compilation workaround is supplied. + +## Official references + +- https://v2.tauri.app/start/prerequisites/ +- https://v2.tauri.app/start/project-structure/ +- https://v2.tauri.app/develop/plugins/develop-mobile/ +- https://v2.tauri.app/security/capabilities/ +- https://v2.tauri.app/security/csp/ +- https://v2.tauri.app/distribute/google-play/ +- https://v2.tauri.app/distribute/sign/android/ +- https://v2.tauri.app/distribute/app-store/ +- https://v2.tauri.app/distribute/sign/ios/ +- https://developer.android.com/reference/android/webkit/WebView#getWebViewClient() +- https://developer.android.com/reference/android/webkit/WebViewClient#shouldOverrideUrlLoading(android.webkit.WebView,%20android.webkit.WebResourceRequest) +- https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E) +- https://developer.android.com/reference/android/webkit/WebViewClient#shouldInterceptRequest(android.webkit.WebView,android.webkit.WebResourceRequest) +- https://developer.android.com/reference/android/webkit/WebViewClient#onRenderProcessGone(android.webkit.WebView,android.webkit.RenderProcessGoneDetail) + +Historical isolated Windows builds produced an ARM64 debug APK with source-owned transport hardening (SHA256 `c316a46a1ee42c2106dc85384f7e9b68ae54cfe80517bc1e9bea57201ad82bdd`). It remains uninstalled and device-unvalidated; no emulator was booted. No iOS app/IPA or store-ready distribution exists. These retained local artifacts are not GitHub CI build evidence. diff --git a/packages/mobile-app/index.html b/packages/mobile-app/index.html new file mode 100644 index 000000000..a5f126876 --- /dev/null +++ b/packages/mobile-app/index.html @@ -0,0 +1,12 @@ + + + + + + CodeNomad + + +
+ + + diff --git a/packages/mobile-app/package.json b/packages/mobile-app/package.json new file mode 100644 index 000000000..d2c7b67aa --- /dev/null +++ b/packages/mobile-app/package.json @@ -0,0 +1,37 @@ +{ + "name": "@codenomad/mobile-app", + "version": "0.20.0-dev-v2", + "private": true, + "license": "MIT", + "type": "module", + "scripts": { + "build": "vite build", + "typecheck": "tsc --noEmit", + "test": "node --import tsx --test tests/*.test.ts", + "test:browser": "npm run build && node --import tsx --test tests/browser/*.test.ts", + "tauri": "tauri", + "android:init": "tauri android init", + "android:debug": "tauri android build --debug --apk --target aarch64", + "android:release": "tauri android build --apk --target aarch64", + "android:bundle": "tauri android build --aab --target aarch64", + "ios:verify": "node scripts/verify-ios-abi.mjs", + "ios:init": "npm run ios:verify && tauri ios init", + "ios:build": "npm run ios:verify && tauri ios build --export-method app-store-connect", + "rust:policy": "cargo test --manifest-path src-tauri/policy/Cargo.toml", + "rust:check": "cargo check --manifest-path src-tauri/Cargo.toml", + "rust:fmt": "cargo fmt --manifest-path src-tauri/Cargo.toml --all -- --check" + }, + "dependencies": { + "@tauri-apps/api": "^2.10.1", + "solid-js": "^1.9.15" + }, + "devDependencies": { + "@tauri-apps/cli": "2.12.1", + "@types/node": "^22.0.0", + "playwright": "1.63.0", + "tsx": "^4.20.0", + "typescript": "^5.3.0", + "vite": "^6.4.3", + "vite-plugin-solid": "^2.11.10" + } +} diff --git a/packages/mobile-app/scripts/verify-ios-abi.d.mts b/packages/mobile-app/scripts/verify-ios-abi.d.mts new file mode 100644 index 000000000..5908a0fcf --- /dev/null +++ b/packages/mobile-app/scripts/verify-ios-abi.d.mts @@ -0,0 +1,4 @@ +export const mobileRoot: string +export const vendorRoot: string +export function assertCommandABI(swift: string): void +export function verifyIOSABI(root?: string): { upstreamFiles: number; nativeGuardInputs: number } diff --git a/packages/mobile-app/scripts/verify-ios-abi.mjs b/packages/mobile-app/scripts/verify-ios-abi.mjs new file mode 100644 index 000000000..8f18e5ae1 --- /dev/null +++ b/packages/mobile-app/scripts/verify-ios-abi.mjs @@ -0,0 +1,86 @@ +import { createHash } from "node:crypto" +import { readFileSync } from "node:fs" +import { dirname, join } from "node:path" +import { fileURLToPath } from "node:url" + +export const mobileRoot = dirname(dirname(fileURLToPath(import.meta.url))) +export const vendorRoot = join(mobileRoot, "src-tauri/vendor/tauri-2.12.1") +const hash = (bytes) => createHash("sha256").update(bytes).digest("hex") +const read = (path) => readFileSync(path, "utf8").replace(/\r\n/g, "\n") + +export function assertCommandABI(swift) { + const command = swift.slice(swift.indexOf('@_cdecl("run_plugin_command")')) + if (!/id: Int32,/.test(command) || + !/callback: @escaping @convention\(c\) \(Int32, Int32, UnsafePointer\) -> Void/.test(command) || + !/callback\(id, success \? 1 : 0, payload \?\? "null"\)/.test(command)) { + throw new Error("Rust i32/C-int command ABI mismatch") + } +} + +export function verifyIOSABI(root = mobileRoot) { + const vendor = join(root, "src-tauri/vendor/tauri-2.12.1") + const receipt = new Map(read(join(vendor, "codenomad-ios-abi.sha256")).trim().split("\n") + .map((line) => line.split(" ").reverse())) + const upstream = JSON.parse(read(join(vendor, "../tauri-2.12.1-upstream.json"))) + const local = JSON.parse(read(join(vendor, "../tauri-2.12.1-local.json"))) + if (upstream.version !== "2.12.1" || upstream.archiveSHA256 !== + "ed99ee9694a2deb776d91cae48ac7411ddfc89ecae2f9b5041111d8c88f2ace9") { + throw new Error("Upstream dependency provenance drift") + } + if (local.upstreamArchiveSHA256 !== upstream.archiveSHA256 || + hash(readFileSync(join(vendor, "../tauri-2.12.1-codenomad.patch"))) !== local.patchSHA256) { + throw new Error("Local dependency patch provenance drift") + } + for (const [name, expected] of Object.entries(local.added)) { + if (hash(readFileSync(join(vendor, name))) !== expected) { + throw new Error(`Vendored Tauri guard input drift: ${name}`) + } + } + for (const [name, entry] of Object.entries(upstream.files)) { + const patch = local.modified[name] + if (patch && patch.upstreamSHA256 !== entry.sha256) { + throw new Error(`Patch base input drift: ${name}`) + } + const expected = patch?.patchedSHA256 ?? entry.sha256 + if (hash(readFileSync(join(vendor, name))) !== expected) { + throw new Error(`Vendored Tauri input drift: ${name}`) + } + if (receipt.has(name) && receipt.get(name) !== expected) { + throw new Error(`Native ABI hash receipt drift: ${name}`) + } + } + const cargo = read(join(root, "src-tauri/Cargo.toml")) + if (!/\[patch\.crates-io\]\s+tauri = \{ path = "vendor\/tauri-2\.12\.1" \}/.test(cargo) || + !/tauri = \{ version = "=2\.12\.1"/.test(cargo)) { + throw new Error("Mobile Cargo patch/version wiring drift") + } + const lock = read(join(root, "src-tauri/Cargo.lock")) + const tauri = lock.match(/\[\[package\]\]\nname = "tauri"\n([\s\S]*?)(?=\n\[\[package\]\])/)?.[1] + if (!tauri?.startsWith('version = "2.12.1"\n') || /source =|checksum =/.test(tauri)) { + throw new Error("Cargo.lock does not select the local Tauri patch") + } + if (!/name = "swift-rs"\nversion = "1\.0\.8"\nsource = [^\n]+\nchecksum = "e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e"/.test(lock)) { + throw new Error("Rust SwiftRs lock identity drift") + } + assertCommandABI(read(join(vendor, "mobile/ios-api/Sources/Tauri/Tauri.swift"))) + const swiftPackage = read(join(vendor, "mobile/ios-api/Package.swift")) + if (!swiftPackage.includes('.revision("f64a4514de07f450ec5b6aa297624cd3479d9579")')) { + throw new Error("SwiftRs commit pin drift") + } + const build = read(join(vendor, "build.rs")) + if (build.indexOf("codenomad_ios_abi_guard::verify()") < 0 || + build.indexOf("codenomad_ios_abi_guard::verify()") > build.indexOf("link_apple_library(")) { + throw new Error("Native integrity guard must precede Swift compilation/linking") + } + const rust = read(join(vendor, "src/ios.rs")) + const caller = read(join(vendor, "src/plugin/mobile.rs")) + if (!/run_plugin_command\(\s+id: i32,/.test(rust) || + !rust.includes('unsafe extern "C" fn(c_int, c_int, *const c_char)') || + !caller.includes("let id: i32 = PENDING_PLUGIN_CALLS_ID.fetch_add") || + !caller.includes("success == 1")) throw new Error("Rust production ABI drift") + return { upstreamFiles: Object.keys(upstream.files).length, nativeGuardInputs: receipt.size } +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { + console.log(JSON.stringify(verifyIOSABI())) +} diff --git a/packages/mobile-app/src-tauri/Cargo.lock b/packages/mobile-app/src-tauri/Cargo.lock new file mode 100644 index 000000000..c5c8ad9ab --- /dev/null +++ b/packages/mobile-app/src-tauri/Cargo.lock @@ -0,0 +1,4414 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2fb6cfd47bf496ff64095c20eaba0c201404ee38714d4142fcfa1dc334fcc7a" + +[[package]] +name = "alloc-stdlib" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5c1865780388bfa186411ab5f247819487fc4864c6e9c3106611fa347586e1" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b851b75c23ca7873623d612fe49bd1989aeb03d08fb9432187eb253d3d4c6b" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941cd9bd4ddab83cb46fa5a2d428f1c857b24ac78cb876cf7beb710840934bd7" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.2", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbad30e4b4c14a39e3cc8aed085a12a327257c316619c93581e017bc52be591" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.21", +] + +[[package]] +name = "cargo_toml" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f4b26e751e711a5302649417f2da046dce6391b2ea30a4820f37462314f0b9" +dependencies = [ + "semver", + "serde", + "toml 1.1.6+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "codenomad-mobile" +version = "0.1.0" +dependencies = [ + "codenomad-mobile-policy", + "tauri", + "tauri-build", + "tauri-plugin-mobile-recovery", +] + +[[package]] +name = "codenomad-mobile-policy" +version = "0.1.0" +dependencies = [ + "serde_json", + "url", +] + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.2", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d045de693cb712d0b22c6a64be5b953f67b3ce00ab5ad3dd5d8b441886ab8e1a" +dependencies = [ + "quote", + "syn 3.0.6", +] + +[[package]] +name = "ctor" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d" + +[[package]] +name = "darling" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 3.0.6", +] + +[[package]] +name = "darling_macro" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" +dependencies = [ + "darling_core", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.21", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.2", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dom_query" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac5fca71e65e94cc718a6e2af65d6e0f9c6027751c2aa562fbb5087fda639bc" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.6+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.2", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4200d433cbd5178df7797c9c2e75b348b728e39631cf14520d1e2fc424201f4" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jiff" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" +dependencies = [ + "defmt", + "log", +] + +[[package]] +name = "jiff-static" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7421438de105a0827e44fadd05377727847d717c80ce29a229f85fd04c427b72" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 2.0.21", +] + +[[package]] +name = "jsonptr" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d" +dependencies = [ + "bitflags 2.13.2", + "serde", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cca139e57da4383727f189e43e66e84fa372347027fc996f9f1a007ec0a37996" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.21", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.2", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plist" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896bade328c13f7042a297ea5ac5b0951f6cf989dea5f32c2fd98da398195cb" +dependencies = [ + "base64 0.23.1", + "indexmap 2.14.2", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.15+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_users" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" +dependencies = [ + "libredox", + "thiserror 2.0.21", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" +dependencies = [ + "base64 0.23.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", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.119", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8adfa1c298912827b8a28b223b3b874357397ae706e6190acd9bf28cee99114d" +dependencies = [ + "bitflags 2.13.2", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9adc193c780ef8f159aee8b61e2d5801aaa555e6eb0947fe45530ec506296f" +dependencies = [ + "base64 0.23.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.2", + "jiff", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e17bbc68e28663bbbb90df47e058aa7eda4fb445b89fe70457bb94fbccf6e49" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37f381f4e048e6cdf038b5705f8cf14ad108279d46eb968140a7b291aba9400" +dependencies = [ + "bitflags 2.13.2", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.12.1" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "sha2", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.21", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59563ca5b331fd97f27ee9672ec8d36d33bb3c9e2c9f6710a1a5687256052e45" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e50213e463086ecc283d7e9f51c30cc0fd304ad457425158018be26dbf0d0a80" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.18.1", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.119", + "tauri-utils", + "thiserror 2.0.21", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8129fb43bb6e0d6b73ad3315cd172beaab17cd28899c594b1b32c80edbbfd99c" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1140cf34a3b3b836a13103dcab17f18831d5cc3534cbd435dc01a5c6daa65aa2" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-mobile-recovery" +version = "0.1.0" +dependencies = [ + "serde_json", + "tauri", + "tauri-plugin", +] + +[[package]] +name = "tauri-runtime" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a57a2a9b8c8b7fc00d35b510fe808ded7f9f1b44fc1cdda6aee38632ea5413" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.21", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e7e0c9f80a5130773cfabb6df4dc924518fb1b2c923726966804634b57d22" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ff55a614843b9a3f010f211df637c94df2607175b8d0f6e0f4a7c19e18621ee" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "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.21", + "toml 1.1.6+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.6+spec-1.1.0", +] + +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "1.1.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" +dependencies = [ + "indexmap 2.14.2", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.2", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.2", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.15+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" +dependencies = [ + "indexmap 2.14.2", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.2", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b9c52859a94554803ccd4a24b98f74148ebc73b90676d783f3490b1bff9d72" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.21", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df16f50ef4cc145211879a3867ba757076b25dfee812040dcb0658bd9ae7904b" +dependencies = [ + "icu_properties", + "regex", + "serde", + "url", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "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 = "web_atoms" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "webview2-com-sys" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a07132775117d6065853d9d1178157b8c90e228de47129d6bce2c7edebedfb" +dependencies = [ + "thiserror 2.0.21", + "windows", + "windows-core", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "111e51caca442cafd9bab396628ac4d814880eaea4e63dfd76a12e9f342b5580" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "windows-sys 0.61.2", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "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", +] + +[[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", + "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", + "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 2.0.119", +] + +[[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 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +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", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "wry" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a819957a01b3119af85e638a38d242af76dbc87d130dca67bfd0441072e21ff0" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.21", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/packages/mobile-app/src-tauri/Cargo.toml b/packages/mobile-app/src-tauri/Cargo.toml new file mode 100644 index 000000000..e07a51de3 --- /dev/null +++ b/packages/mobile-app/src-tauri/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "codenomad-mobile" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[lib] +name = "codenomad_mobile_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[workspace] +members = ["policy", "recovery"] +exclude = ["vendor/tauri-2.12.1"] +resolver = "2" + +# Source-owned iOS C ABI correction; scoped to this separate mobile workspace. +[patch.crates-io] +tauri = { path = "vendor/tauri-2.12.1" } + +[build-dependencies] +tauri-build = { version = "=2.7.1", features = [] } + +[dependencies] +tauri = { version = "=2.12.1", features = [] } +codenomad-mobile-policy = { path = "policy" } +tauri-plugin-mobile-recovery = { path = "recovery" } + +[features] +default = ["custom-protocol"] +custom-protocol = ["tauri/custom-protocol"] diff --git a/packages/mobile-app/src-tauri/build.rs b/packages/mobile-app/src-tauri/build.rs new file mode 100644 index 000000000..4794eca97 --- /dev/null +++ b/packages/mobile-app/src-tauri/build.rs @@ -0,0 +1,20 @@ +fn main() { + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("ios") { + let expected = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("vendor/tauri-2.12.1/mobile/ios-api") + .canonicalize() + .expect("source-owned Tauri iOS package"); + let actual = std::path::PathBuf::from( + std::env::var_os("DEP_TAURI_IOS_LIBRARY_PATH") + .expect("Tauri iOS library metadata missing"), + ) + .canonicalize() + .expect("resolved Tauri iOS package"); + assert_eq!(actual, expected, "unpatched Tauri iOS dependency selected"); + } + tauri_build::try_build( + tauri_build::Attributes::new() + .app_manifest(tauri_build::AppManifest::new().commands(&["connect_server"])), + ) + .expect("mobile application manifest"); +} diff --git a/packages/mobile-app/src-tauri/capabilities/launcher.json b/packages/mobile-app/src-tauri/capabilities/launcher.json new file mode 100644 index 000000000..bdf598e7c --- /dev/null +++ b/packages/mobile-app/src-tauri/capabilities/launcher.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://schema.tauri.app/capabilities.json", + "identifier": "launcher", + "description": "Only the bundled launcher may select a server. No remote native grants.", + "local": true, + "webviews": ["main"], + "permissions": ["allow-connect-server"] +} diff --git a/packages/mobile-app/src-tauri/icon.svg b/packages/mobile-app/src-tauri/icon.svg new file mode 100644 index 000000000..19df7d948 --- /dev/null +++ b/packages/mobile-app/src-tauri/icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/mobile-app/src-tauri/icons/128x128.png b/packages/mobile-app/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..9b1aa643ffb97f6adc9cae3b83d2413f9eb52fc0 GIT binary patch literal 1176 zcma)+Yc$(;7{~u1EJtN>OxMeba9mo3wl1Aph-U6V&Da?lmok@G9H(8iWPid*Yg@NW zS5ib4tyWt^DXAj=#_gc8QborktiJb0Rch(C_~90s3_HH6y)S_a6;-Q*hm01UAPr)-&MeHle$_f+y1sYQdfOn}ke# zlPsOQ-m{*maN;)@+G8LhTM@i4-E9{1P3#H=n#_k!m|K^Vx($G zBfNC@!(i=(T=l@ZVz^YHh8kS96d>X}T)%<&qgo~SekJ6outm0+&?#T25l_JtbqrR% zorH!oIeFeyZhbd+4Kx{xW0tkOgOurS;(bO?*iltd&44%~s3e>Ju+Y@;&btEzUaRQa9SLVn?b3BkW`i{xM$KR# zFxDzc{$T|b>gH1%;mfEHyUQ9>sHGvYz70!A`*<{N6rpXMI{&3LY*kN9NJPI|?O-*7qx zc$MJXvtV;0kHC3Ee&7quq*yo599sI*X6s6^iq4NTm+mpCUXum#gWIDz0%wtGn>*R| zjFLV$-Ez&G{(*sxct}N;pjC_7`WP`=MIT20u`Qfs#c57W6aDDS%pxC+*ok(bNo?X> ziTlk#UAZ8Q`&eYw#|0*XQNr*P9$|J6c97qTv3+QO%aSA3%F+6drF)q%DupC>bX(_a z{oh#)R;U=ng}> zoHKMb@%DvYK%LZxR_;j$N51IosQi~!jhYa)6g_C|C~!Egaf`O&jZXn>Ow?oCO@L^Qy)&tse5@eJ|+>5GtNiSm|wthPZHH{-uNtHLb`%LX6 zZ>N|~kk{Bg{NtH`i(BaQ($e(iNBjk&NHx>#U@w#zvgg%|G_}y-Ys@K6OPerumo@a4 kX}iIDNBIA^8&SO=mTz2!(!uVu=HoyCpNCNDed3t^0f_=N@&Et; literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/icons/128x128@2x.png b/packages/mobile-app/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b84c91649867e15b75161caf97158ab8d38bdcda GIT binary patch literal 1983 zcmcgte^8Tk7=O0C#1xfb*;J6NiD5SZe-LJf8#V&u#|TLC!7QYFgT+ zmp3#T-_E_a<2O55vL5^J>Pd6nquZiIH&3_Y86dIkT?uTrF&_W}B>)G231ax~$bTs) zZb+_UI(gAN?b@ajvbj4IxKL4;pFJhmdH2Y^J&8t>XzR?Og%5Y1T^$v!aXGBM{)cMY ztvtUXkPp06M^3?BuSN`wAk$4>yitmC?XIE*_l?b>IkYGZ6 zT;+Xsh!wZ_iFaY?b`j|##+E;=P`gi;HR7?n&QUEGpqEgx4=!Lfy4U$jHk%nX&Mjg# zKJ-+#aa7NH_k6WXTtPoFlh2$LSAW(eEo5+1Q)Um{xD|NKE4eL5)p#u_#S1c$)dW>7 z14+G&{kR>4o{4aKWh9s#guXYFfHE;lgY54+`?wJB&iyASypopY1gc2n=v+)hk`!wC z*v-sP_4?wO`eXjXBntO1TvfF{bxVYBQ*S9q;_mbn6ywPTB>I6y z>jgvSeNk5vA_izA5DXpmMboXQ`6`WG=c%|cqn8R0aWh!7>cUSiO-`Xn= zmK3P{K0{F_N6+jymAzW_aEB^T7F@HYtu!?R<1b)mid@$*C4@Y)ZrKY_%UUfVu`soG yJ|4X8-pKAyA12i2Py6!}{olvw=s#b)$WHUY33H6$#kZS*{u1L-B~8oaMSlQ;JWnJ5 literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/icons/32x32.png b/packages/mobile-app/src-tauri/icons/32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..d7c9d3ad27f5f4495a57415e8ab3d8b6337a9689 GIT binary patch literal 418 zcmV;T0bTxyP)=K+!2BF}Fh&Lr z+PrXfyrVFNJR!hBiscE8B4GBxt6vxv?0*Y&0K(TWc@RjP@{oEVfNc3fpv5rDA@L6h zfr(pQg5_WiAvPpfNVPm+$^(@6H&EmPvkw46Wy7ft@Th?4ArulU;J^rx)VjO|t07f8$ zf&UD+n+n+C9~(e61KDhx4j?w@aK%5Vp~*sS(BX@JQlb)PF2LsqLh(R7r@<5g(y(jvoJ{T26Fjgv}A?@t=SsN37)})rK$wLGkZzCV&zDpb82WHN;vD z$(khg3qaX#Kd=D;0)lZDaArZ8IRKsu6gY8~fHZf&XsdYC0mI4x03%G~qZhv*0RR91 M07*qoM6N<$g4VdI=l}o! literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/icons/64x64.png b/packages/mobile-app/src-tauri/icons/64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..a258bd1f2d1d7ebd841dfde9df7934c55de5e370 GIT binary patch literal 675 zcmV;U0$lxxP)(b+62>_Gd$9f5*fw+D!ByZt^|Oh^+ff*$tM(SS6x&7H)T(-U3E?Q-&Ab^+f~hjb^&zodZf+=bQ3>V&uaC5O5s)yNoCUGZO@yS|BF#L z9}qSb#%_Q>Y@Gjz9RR`S1Q`GWU;qq&0WbgtzyKHk17H9#iC>O==~5x#kGcQ=002ov JPDHLkV1n;jJv9IT literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/icons/Square107x107Logo.png b/packages/mobile-app/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..8ed66f7dc8f49394b8f1036a9bc2b35ce95fa747 GIT binary patch literal 1107 zcmeAS@N?(olHy`uVBq!ia0vp^*&xip1|$pi*YGefut<5jIEGZ*dVAM5Ti8*8{lmx1 z4HD5I6S~+9cYe9lWm{=_Z|{qv*|uCwbxil|8k-(${J|@-XM0?Cd)K!G(_L&L1%q;w zMZ2GI=J=T2eRKMZ@%c0V{tIMCpV;*I^|{TPt*uwie4Sq>t21F zfds_^OkzwrED;BsKteznfdodN4AUB|rD3geYYHB(u?(DVls)_Y+1F=fFW_wf%E*xa5Y@+{W$uzQ24u?`6(knVPyyx@QxXNM@~! zeE&a2o9EcqtS?6%?O*)r?>T`kpuWpIZnGBt`?pr&#uB{^yMJeMOm#7q$ZXeQHsgGK z?y52Sf-TK~90C8E3crVKi}USrGvH#?I(ho*+k>8tH>OnQ%bEZN-|$+W+7zKaT_v<&bF~uVy$gF21p#D~_Lsu_vintN3G>wZu`jsmtS9 z+1xfdK3yBG*cj*&VD>&flV$OY1v&R~uCTcoI!;|1e(RuynuhdOFJp zv3eyH_2dKTQ?7e20ku2qTopd+fCig}{AcFchCrTxeQL#C%|JTn>nU@IqYSE%>uZ`s z4^`ECj!|22?%KQ)OX_()->kH+tl!*F_`5UlW6I?1bK~0n|H#~2^t(oNLxuh2zIQ5j z^6u0nMf|Y|etqw_Pr~VNKWp~Sq3hiiR@iTLDBLYtE4StYXPcP8{jXkJtG=w<&1Nt^ zc7e*2sg4`-4dNp%1zgiJ|H<+>aD9zKf8@Fm-pJc(etV{Z-AfrnH_ACSA|C{nB(tS%&B&fHLFQz=98CPXH2J=X>5FXS?@s1 zHSOMjMf_?vSc)UpZ*l6h|CG#qCRBWd zT?a2LxT3>8Wx2Oc^QoxyJGA$fy$lHmSS5Dr6f-a(7-qcw*M8;e>@AKfqYvrv_DFv_ zxmy3)tE+Cmk1tG+S{S0oXUW$iJw@7VEw_%*J}LHPqF!$|tzMkKDPq2+`*q{ugjowi z^hM`2d_2iuzT))jMox+3?6?W>Uw6H`Q*dX3&~}$eZzXgNW%i0L!KpkM-bZQWd`^Ru% Xg9*>t5BlqYg$0ABtDnm{r-UW|`iB0J literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/icons/Square142x142Logo.png b/packages/mobile-app/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a840938ff83668518a117af188768618bca84cc9 GIT binary patch literal 1315 zcmai!dsLEV9LLGDC^I87Q&8t|GfS5>JDQX78q5~4lGIsCODAtyqUGG%MKot~n)vRl zVFl)8G|5UiE{#eJ6p^Eaf|!%MpqQ8PI3c+RNbYUR_UF#$JkN8!=lNZpbAHeH7JPL8 zZ?oEIH3EUKiTEP?5PTNDTgVmgK55>LL?A3*M}&uvz!x(^jme~g8`{+~`+}P+^M0aN z_{9R1#GgC7rj~EIU|r51uqG zolxYT^wd06YGp@)IM_u@RG**g-ITbTum{~E<@BVq<4GC-wLZa}hz}8@uC_?{>udl5omQ#HTk}wpo&5Q9XXw)$83ZUP!rC^(TnePSAZVa~@MOf|A@C$hIqI z*<&pj877MIY<(_1_^NK6(A%Rn3 zy)1XL+>^UpkezkE4wc((5HgH}PJOD+fai3f9eKbewHaA{z82$czO&`|+?5nP zlNViFt?&|!9?N#%5(aVA6YD&tca^APZp7WQBD|p-j#9@=`7{tW4c09U-PON|*9$T&epBnw$}ikRR?O=$uR ztkeanb8$nx(nDaf22BJ=T>)J*HjbzM z5jT^lEd}@K(GWmVJauy@`^DVu<)r-{>bS%v!82@cGv7I+-13~##M*H-zlj9y(=azm zu0P&PUX6VOPZfAJWCilTGdgC8XRhbFJHCm zK4ucPnzgEULzo?~U-0S&_VkLO*|*GXAhu$ME!(>6q1&|*g+t(;20IrY;3!|dia3~) z9#*nvPlK}y5g6pl*LH6r+|GC_{@8^AgmxZGWvIN@BnfCj|4N-fi0X#FYHLsD%4nb! z@E#l&pgw^v$;Zu((PfrBBZUcBPh8VTHM{&KOb3hU<2nI%w2Q@Uto3Y|bdaU5 z$tasw+eiP_(X*d7%1Zvz`64sWFnl`X^DN~@>toW9&iM$K4|X!LZplt8F#l^P-@LK5 Y2<699ZeKYHn;#-#?}6~z(D>ZH0Sjk}JOBUy literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/icons/Square150x150Logo.png b/packages/mobile-app/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..080164a756027c0cf54352e22b0406f9be6a52e9 GIT binary patch literal 1433 zcmb7^X;4#V6ovr<3aJZ3mZGTGLE4*;v4X@3MzKN!%3??ec0iUwMB;)ApezZp4U&RI z#3(32MGV)N#0Yp1@JiL<1Q3m;Y*CbqMvSrr5+GdC8~?OF`ls{E_nq&Z_q^x(cg} zgwjhru3YN+L3OH1gS1CsY>r);-Kc4CSCQRu!9xRS34;Tri@i0Z@a!{|Hn*OnBNaqq;Q-0wBN(3=_o8;Xj}1c<67OcR@Y z&@cJ8 zJGo+8=3GUX^8qWCaAIuPnD=740~e00T7CQV!-4n5a(Q2sWUXeC^$8NTZD2;({Op@| zd={b~Rn1flXDAC3R?3UH9UV@t>$hag#%kHqqlqbckpUt_S?0<+~cl2=xvtCcX1=ae|zK2Uj2UD8Udh4R) zzXm4xHm1+|OtMo(=9Ci#(3+PGe4f5J11CLlsSe<9#D0-xpto)@>)}7LRV_+tcd3@b z3!nw+P9flzCIs|El|((*Hf$4G9z8T_oW+4bQ(iXs6FgZGxZ}E7pSv&Fl0TKjdA5jX zN}pxFLwf=!@MHmMW{`8~k!Ap*`I>+Z7*R9Upsgpgcr}EfHNB7Ci+(8~8Kh=J)A9v& z0veS1!4fo>ka3?Mjt2D_-_2;SU(K*Xg9o+v&k%DitLyRUV9s4!>cyegVfPMq3iZTU zYkaUFip(K-t0e?d`*^>j>h74Qs=*E&dSyTqe*^(isQK3V{9; zLMJ4c#uxL??K^e1sFMoIih$X+oh*njjqm151c$|WodK|{1Q<*w;32{k9?O>q*EMDb zX26mhKwpv84K?WSf&8M(KnkANjYu8AxFP(@!ax=*DFi=|Y8v0l`b2`?0dk8EmRO8P zHvzUUm>zFre~ARwsP1cTdJuyU=^}uRLjs0nGEr+am6Y3nA-W+_98exsc3dfYCTgvr zda1U#3T24&8$cdX_ay&jJUm$xfZRDjRRaAY?`xYTrbOqVfNA^~zsp;+(N<`KfEKj6 zDshDwW0XzD0>^vuMsda`4k!z`J%(jGWz$!I&vc&(6i|UZY(?>-R5d{xyxb@-XiazM z$=Q;{a&-v0Tp}}P3}&V?+g{X*!)Y5r^0ThkRn}=Viy3yHZ^D}JWsy;O`9NYYeQ+;* zUubi7xnnPTj$Y}LSQXlQT5iEmJXhY+jrj0k_4e{`H$ewtXuV^2@ z_QpRwP_NHzc{^DobrHNu4qD#h*1VA9$lFhA|7b4rgu;3Dem7u>Pv@MrkfnZHDfUz( zbAbqWhvQbkk)hVDi3*X$cEo83Mertt?InKsP~rtUMHJy~Kf1AzaP=aVdzBNc257uo15o-pXYgh zZ*ueA{12n)L?=%t3%<2> z5J9Dmil~2&_nc{+oVQ&M+=ah7^Zn#--}Tmzq|}j-vtLv=9!kew7Wz0+FXKs(>Pzbl zyX*=b>APMUKF9{J_Bq3spBvfqC2@zX#dYkmeo)RD3wWHa# z4(dN11O^EH5*#3~FtgLG$=u#K%|BgFZHPYlnPGL%M`8TbAd1;$wIv;{=Py~ot9Y3) z`nqvuPV(F~X~F*Muz#Y~JM71K=4UF+S#9&M@kM?S22V;n!uy-MJn&9We!>1a@Ar5W z0<-xNZuji;R=g=!wvj+pUm8fYXcU|YmKW<7{_3)}@4}cY zq=~N|>dMSj1k}t9-hMha-e12UnDjGCSz3GQh@Eui%-Tc?o1yHV%A(js?}w&%%Ex^v zS)pFosx90@sso(y;D+mBp?(8pg0|IT~H@nZuH7Q|59DF-?e*UQ#~b z4cFB(by#Qak8$MA=X;1@4kiq}Cf~1-|MAS`Z4T5N&57#*oVS5Mk?n{C!% z3*%gvp#s<~m0QVQa7_~`%AuAw^E?yAfwDwdJUMHGRWBZQA%}&LKqrjfat~$lsj#gb z)N-#MXxI2%(3GN`u%T~T(5rB$wgycK8M{&Fd#JV!fg(R=1l>okE;6doTED8d&_;-RTpBssiV`kQGlxHlPF|B4R|q^{h-Eg!Vn!>vu3cl9))X|uFTz=As; z4Tj@Qw#ySU!aJ@Kfgv+VQnXJm)Ziu*Awo5f5kszzD!UQ&$Eb%LB_<6ax}Ylg)o8Oy zq$@`YV{U)2&TrYG-9wo_&N7_o!nu`XCQ^W2Jp`947_TDo-jR)ULSO*)I6viks?pW5G|et4cn} zz1uL!vduUI4@hr+DzRQ0(k4G1b_tw|O;ivige4fH@<8TVgwe79`dT4^%gofx|-;>Po4$NmK}E zR*t^IcVRnBYg7)8lf_5@xD|@_oDxPuDRJNy=?yJ%q5)dZa_v_+h1Nd)S%6~jt&qC%1gmpr4=4kCcJT!JV+bEGx=2Mn-~7G5<3A1@}s z6j22aO)+4Ky4PQtorD8mdRk>xI(eSTl)>Ya4H%-*De%;Vb92H7b$p>W?u*R5R+5SRIlWnP_5(z2|P0RyECWj^ZKbP@>u-VYA-B( zFn!{wdAz<1Y^3qVfi7#^x^3#VYq!6>QN5hBqtzX2(oBE->Fy1Nli9-a);|x*lAQmf zZkxdn?{K5}^9^TL^48}D_I>$3_VwFa^uPQ1SFGN*Nxy3OgC_R~m-7=u>Kyw3 literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/icons/Square30x30Logo.png b/packages/mobile-app/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..dd7204671ef4909b4a6131c0784e7be45dbfd024 GIT binary patch literal 413 zcmV;O0b>4%P)kjKxTr>#v8}PSdPzPVl2g(stH++D}IT!6mLGp zWjU_+CB;&LB^Nf!VKou5Z%MV3sInT}@<1~|Op6t`NU{`bcEV>lC{fMNk;kxj!Tz^k zH6Sr!EG61vnB^NzePB>n#Rhf&dj18;kpdV;+ts5M4?Bwi7TGbH`SH0v#BGw29orxL(drnUD>aK1d_^=PT&hzAX z{=ffy-zPceIZHX10D1U(U@#ac@xZsZQ%@oW1^^Q~J>yM6xP)6ajT*}RU)Rx3RXeK|v$NskWOUveuQ>-^%6eD}cY z$AVWvwq*zF28Rc>%t~VvVaD6njDMK^V%-|8r;YunQ9U^LegE(R{Xk0d(V{iMRpgI) z@QJhw!Q#aAnib64^M6{mqSxTz~pzr|_+!y{qB+ z?y#o8o4t!>^D=vRKBmaj-y&bI#SP^q-1>TW;nEF0m-=ZXAw9H0ZffEjyn7|LI3Q~A zp0qNW#e@%*za?98kEkMVrMWhKR!1)uzPmX7Q~{rb`H{=t8Hf)9_w-Es#%pWsT{Zr! z_8zPF!qO>ITG|8CuHx{Li)}8vkFnCo%IJEXb*c1sN<3tI{fzPcv_T8${5603=89+! z+m1;&6@Vy+oxOL7pGS^pVR0JjXdYxt$d|;!pE_daO1w>Ynq2|&B zO{7_!eF0jm^JZ`C*!CZ5+GX5z9lbr7BD%JWYscv=4Vx@g9Y2)Okv zV-XDYY@Uq-0pyfGQkT&LRB^twH=|VQ*TUQEneXRWuUGr)d)oSw^B`GtmiaZLws~wl zgFTZ6pdQU*E(it;>zd&LeSfLHhm6^hSnnz+TiVqB7qMstl67tm)z9Br3rkLGIX7=l z-oKN?}!t*VWN><@V{W8E+hr&%r8)gAO~WYlKP#5s1g*+eDI zq0}Y970{|z zgRDq|oemd#-&d_jK9r=tJG=b1Vm=nF#u5c+xxiVEQ#k<;-ASNur~?RsC$}{`zsQ2Q0V)+Q`zLL-79raYq?sCj##zca-%PF&ZM0EGN{lTAE8Q9uS!~F=daAw}{Hijq zibzmCbFHm`&$UFr@;hp9*Sk+*!fpO4K6il?KLI>}TQ_Bh8h4KDn(_BD!M#ekOY&nl z7ENJZ&lgiKJs+KjpFp=oy9c7%0+2ok_7zxO!-nXl7J7HV&yFbRzekgQUxKXQIc>2%IX2BA}AUsBOM{R;`GCgwwiIS zC*q+8WD893v9P@L27nO7NwmRVifgIFTz+w6I1>`6p)vcAi2@o5q()mSO?(gQohx(sXV z+|ftuBfkF_wu6@n4hY>}q}wYzrF`e4LK(~iA*ezayyOnc;*fnDg}^Po@l8QpekehA zPH?Ea;{H+R*6neGx~Q1q?Q#$beS=I$>5z(Me8WLh^^;N``XQ^N(WylmCtOfiD&-`C zUV30=a;%Zgc~NF-78z$SYOR1n49K}VY|O#F>y_W0p+8h zAXi`F*IL?SJkljAy#HD4rM~yGb5D+DQLa5od)yc+D|^tDnp{Ob$v0ahHQNh!?{(oV z>{UyOBHjR}lU`Hc&(8=hJoLPRo*z~;t0IGQ&WpO0%)o)?Cw2dO1`Tz$&B;pT^Py4T PP=`s3Ke(?mmR9|591+OT literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/icons/Square44x44Logo.png b/packages/mobile-app/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b001b551ba006fc0d99119dbbe215ffbc3487d35 GIT binary patch literal 562 zcmV-20?qx2P)^3ar2i{LjGgC||y_ zszZJ;31+hkr&k@Y+8=)#TwF>)K9hvvatbsJpnBH+-ALr-CZKhYwVz&vvF-}$jU0>z zu2ts2(8uj32`OZzhW;+E&-nI64tB7q#80B|GiOW=gR_I#eR}yE>J>pFQ7o>~z6D3M zM`DHt;OhCk-Q$!98byI6^J_M`c8?4+iULdK*K9u9PZ?+=8%yT*qPGUSOB9V{W6AuQ z1XvV}WMjSZ>s7pHBpd6OUs-4*6C0La8ED_Lgx34;R9Jp#Drjua8Lqe9pisWCcPOV0 zw++}_Nrw(blV5k?cj9-ucSSrlHZWzF2hEaS9&G3;!HQsz0=X6 zY4R(AWjafQp=t6fien+a07*qoM6N<$f@+)s A9smFU literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/icons/Square71x71Logo.png b/packages/mobile-app/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6efbfbc9775a5133f6371bd34b11e66cb372e7bf GIT binary patch literal 816 zcmV-01JC@4P)tVV)t};gVYo$NNsoncmhm|A_XKMWiBmB zBqKu*C{gfSZj}q{oo{bv=GHKaERc0RZhzeV*G{M{k-a~%@#U-Ik|KG!#O^WUk~Sj z^WBSK1s&SSImBw*Fk8dFbbBLm${RR-E8`BK1kxeFWaSOabGG;U zf3wE|K{dp5C@@)h1M{1HGOWz&xqx&iFr(%-=^@4iq(gzpo8Q-f+O6Zm_!omCYM2 z81%PhiUO6*8%`LKH}#4D?d+d9=M6UuSs}qFpw-2?Q{GeoLt15#2Lu&cE_stxct8n5 zUc3YsetWxPCwb4?C2wqn1k^Arm0f8d=%B4bMdmkYAPyL|f=TEI?8?E=`OtY)dVbT6 zQNDX^X#`nKYC7b*p5lV3q|~fFWGV^gFZDvW$E uL~sO0a0Ewi1V?ZLM{uMGj^GFmdAk8e$xc&W(P!2G0000 z3Kpcf2?e2&{y z$UsH`8OSIg0~yFDAOji5C?EqF1!N$Xu6pepJ&LB{-kq1&^QNB+Ou!;YSFo8j>l@nm@4=OW*VSCmQA$*N z?(81GR^!*)zHewRAAEzreZ6pFml~~g+S>V>*~b36Te;qIYv)hg?#jmA$?wToz)6pO za;H`}kJHL$k2acsx!x@OpoO>J3+FK(uMOI018%qslUes&N;=0V7)^)(Z^*|=mC+C?582Ilk60_N+_I~be$gU>azt2~wh zE_@5;652%`O9GerGU5taI*+A+OMk7FhL+A_7r<5?5J^Hy=CLbaYmYjmpr!KIC9u_p z_7cz%dF&e4`tXxYXo)=b0W30*Y75$S9{U0on-X9H+GZa61Qwm#QWn})9{UCspQcq5 z+EyNG0CqCrun4q`Jk|p2Z0c-Y=(2gN37A~2<4d61i#O;(%Ho>@rc54d19rZgVbmU7 zG>;b^UQq|^Wns=J&_(n3<%i99iJ2x~Z|nHdpcyC)X_P$XWof*odaw$UsH`8OSIg0~yGuJoOJl!%FZ#?QrA(0000;Y%LVyT~aOU(;17~;I@f?;6yGC^KK4i12LG7T!wj2S!XO3ZcBklQz3Y@t)8=N z!j=`*?c@8ddg=*c3%t!h7$r($DVY(6id^Fv&!N=}dNwfZx8K8Ek|D}!V4s@Z2rX=%m9sN_Qx zA+)qqF)H~`MF?j^1tXh}I0&h*%V1>l5f>qS)e^>lYF|pw`9;gr`T@;)^&HZlFw%a`3#zIg&y6$gw z97}~D>onp-aDw>hcg{ + + + + \ No newline at end of file diff --git a/packages/mobile-app/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/packages/mobile-app/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..267bd286f78c609534e34216951c711a6e7f346c GIT binary patch literal 2150 zcmV-s2$}bZP)f8lTjd zctJ!SeDFoVAe2iK5z0j_Eu~sOkhZX8+p@bee*ZIbrZY>ow6HUw?Fy)5Ksu{<~<^VZt>Xf=8&KD<^|e7Z`DQ&l>I9wuj#Q{tWGENRb}bDHk9m5u|spppYi)vsU1qtU*=dN+`)7vN@s| z6XNvHT@}=hkYioS6=*x80YPxh&KxbDTTVOQnl8`yx5yhEAw>zD7EVoA&SpEx?qioJ z77_H@`?a)uc2c%gB0l5+VItX7mg?qL&{u0G(V}Td>pZW5jI`l+gYvM^}VsKt>cA3c#`l14riPt_f*iPS6y(}ew}Byf#IzF_!M<_vq+a{ju5Mu zx_?fEr@w87J1LdQ(Hd?8u;>H#DM%OtV_{4`APK)bXVbdLPFNvSB7nnQu==A@l;+UV z*;_m-Vkt;fAsN@+9F89B58du6lVY9?$9O zV$X?cp2eP(!HB~v>0~u%6};q7OScs72m?i1^s&MAB>X`)7;BSH;U@ePWViv=ItYz5 zE;#vnDwX2zooVTh^g2FoKf>X~of4JWssR zlFMf)K-TqLM?0w_Wv!rbpe_19U#s1ad=zdGgdi(8eeI#3xw(u%Q)&`4epJM>g5GmH zO_cVon$d?F(gvS}TLdM@g*KR2+fc@&B{RxsbeRRHghT0Udxo0N_sG<5<~8*|@>#e= zQ3YAMwjk#pIxOJ(>Uet*W39Q8weCG|Doritt}q}h1+H!{7oD~yd4yXlu5gQD2(qCB znQhou-`=kv=Zmup$p$~Yf9@O?b;1irLrd3yY*LUg zCdP&hu*I~!3^u}6*bLisp#`Hs#Ym#k)^3C5OiWNU*MOw9@I`1(1BVzDse-5lCDNEc zgzGgs_Fkk%-)W@NZJFK$8-J6}E~}(3Ua6);sZK`5ysF+)=$KPwg;!6;qVf0n4DI7R zn1}&T|Aa6G#=@8w8#cfewLpc9uoX7L_Q6&o2kk-7hzM^9cI~@FPrlbgXE@tP5Y|$L zo~j!|pT0DaO0NM4-jm_=o^kB|8RJ9Senrv9|Z?vVN8r|*n2vB2HRjG zY=zBA-+T`-1!?ELi_!q3WK0;@2H()ix=Q-Y1*xyNSLwZKe2f8E`!AKn>Un`krMk0V zETo4D4#vWmQV-b%40qOs0~@aw&JAghf>h52JYjnhjK(*P8qUh44C&>Glz>~ar{qJD z+Y7!9?YWgxvI?(`%URm_*X4rgUBN-0=o@2TtgM7%eFj@#Q~eFXx#5+xgp_1Re=wwX zn`FGpFtPVX%9HwnlwW^Vo78g`vvh*>M=y}xJ#bu}gE28SY=AAW3AWt`oTBW-^25!6 zWT)V#^B_5zlvjV&?d#hPrKCWaeLPD=TQ?l^t(FoBPBFr&=)3uRNGjZjC_ zIxU}>^t9dDaLMy%8k~$Clj8AU1Uay$8vP14&*2$(60dsiuJP8NxA3DNq<-Z_flxPhhJjA8GQu-r~gVokzWIrFGm@c z2+iR1zW;++&fW{>6>m4v55N3Jkx|jBCNA6J89v|VoXuhOroBwd2p^vO7|u99?Y~4v zTe~UB*AQAQs6R(UpA(nY;8_Osxrn6U@`jnLBlBBhyAGu2k%K9!;WF#Osb#D!M2%ct zrI^-#{YN7j_U|Y{dqk;_1+6CbpeEj!N=4>urHNp@G z@4KAP6obt0O?wJ|iVy+{0fon40S)4()1e22fWjmE-MlZqn-Blz1qy{Y)SwVh2q**; c0vfu<{~AA5i26^gHvj+t07*qoM6N<$g757r&j0`b literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/packages/mobile-app/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..1ed0a1406f578714d41fe5f6614ce826623241d9 GIT binary patch literal 1520 zcmb8vYfw^Y7zc19t)pcJO$)6S&@y&Ylhn$LlX3A@xs;bJ?@F1il*Apga*GNranyvgyn-59u*dH^JF_3&%RDo``9Jf{yw3$jNPxAK zqZJB;vJMKQ?MKdvg#|1|_SUJDt5GP6h9KImu#-Kby)RNj53YGUdF3SIYbY@+#xiNk zAY~OFjL)^mMZJ5twA`;G%Pi(zS}y9UG-}o=<|Ab*dl%-2D>0H@rz}e@^J41NAJiFK z-lI4o(a!Pmk7?Xz4`<7;Qb-Nb&V8D)!(c9)4)j}xb}}mjMCLRnyzT!l^o&j!R_w5} zzqrFNuRR=R38duV>v3{+)7&t7@&}z5bRB? z@x{G)wprm0fQb)OzC~=vT(VDe2fxRy@7>6YL|;O?RydwH`#9MqF-3p`Hi2;<*9Cj_ zy646!n~-@!|AbQAk*`)_o5@Bto`3AF?y0neau%&ugMVNryTRvdTG#9_058aUfDq17y?9-&=|Ibjb% zdk&fyNHXQ2PP`tW_qEiG2xUtPY8Pnlw9Ff!^`5v?geJ2fMX}-`bE2Evt2Xpf2RmqURoI`lgLHx4&Au@6-}*MhSQ=Ha#<8M3OMVHE|A> zCd9PYcvjHil{0UzP8^hhlM zy`5dTad#HOjvDGHK(Eo`9VF)sU1$o4)fd=C%!TUt3}tG(Eh+d0=ti(>fGWJ;{aPvu zpi8kEEhI`qmlH+CO5XjLn2tOiLoY>Fk%F&*Zq9biFM@;M%Npt@ApG-=aI~b-(6xx- zeQI(@K|BoU`Ha_Nl|H24VvtM-YlzEmhpV*IG$4E|zHf`-rA{0m_d^DN{%{ z)ni;sUHhM=qnT2&86n__rU2$(o^QtSCrq5PvX@Jb@07D|-su#sj>5^CddZl`^TkUC zU3LA)N2@zIpT&kFY)a1^Sj9xkzKv^Z%5iVnk{!e;qHvN6VELpwG~hOmd%#}85oid; z<%5|1uQF=e^G&AZk|hFnd`Rmy+P2e-FLUxB>_hzVMd@n>iLLk#bMkcf24*p<+&QA9 z7^;tr=`)=10PgQREJ{yFC%li&8Xm5iv>?J_!#R4KLq49RL$O?(wWJZhl^M z&EH|JAI{>D@kNl6I(w2Cu_x`-FyJ#>aFv5clj^xPa^ZMd(=C_QL(sD50%F36{Tk{e zk<+DOyKQ$U|2hP1FqR^k-#WalS>fqGa~j&|zY3i*_A@GLW7yJWAu`EPLG%!sloHSX E2N>wOD*ylh literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/packages/mobile-app/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..56b83c33f079c86a7e0ac6b6c3327db3fd69e77b GIT binary patch literal 2858 zcmV+_3)S?AP)43q6+ZXQW_vtNQzvnp*jemsG-RnLs6{Q5hN`Nl1SF0q2oVqnK@F|Q!XG4l(0`CL zN)rV9U@1ri+9WL!6+$6}Dn+RZRW{lt&Q8*}&ekLz+hcp?JH(0WW3D0nGDX1 z+Ss1uobP_`?DxKrRDF#4kW~t*1Whu6R{z_sBsk*#l7QX!h{;7zDXpT!WQj@RE+ryL zOu9G{Fi~RC2H1dz5|cIi1VxgVl*9x`l$iA76iJe}EyyW?Byro2Qy@vAQppj*0jHGN z`sXbmNmQ)m3|U0{AB7oWNRZ^N(GHTRagrn|jvUqkGUiN=QhiN`x<44ED>u?asSHs} zl3=oIyL=>t%jy1My||2J=e=5X#-@h)F#W1?E`5A)J?%MkgASY?pndQ5(*9$8)DO!d z3XY^eET~B9QSpTTw|dzMqLRWjlG15AM?2nQ4_egR6r-*kOQ|jvBB|f==7~Of9qg}~@OmWL6ibYyai2cLr&(L>P&!jEuTGZd4xGO=w zdul$-VUmXVwU87(6RL?wdf-F#KL5YWLHfzh%%G0@rmDXo3o@;dpD%$GmCS+wJd;e* zch@jEUu|WVax7t(-R8li33~eRStPV|xUOP3f&8Im4a$WS!Thu7jtI@29&-acWI-lm zLq{MaO_-n}tj8s%VRjEV;)8K3L(WNFSjSJ8oZ}8TmX#}~hAZWJ7uEX;L45AenSKg2gy}6N7BWtH zNHXpQozM*omafJtVqZ*=gT->a()qb2dd5qRZM&AUh8K1SceNHW!Wk42WLbl3umWL$904%^%5J@A}UogkGdd6~|_-qqx zRpdwo6csQQ*wvk)wH%OD%*jWN;b+LEhnF|_-0S?6Av(yhha%Dq=7uE5gly=5F6eYf zk_rnjxt1i#>|(-n3EHh(ax8}&T*+gc${*iynhqXI=8ZXq94u@~V}usXs?j7kY;<28 zro(J4=-n(&L|2RE=O7z8pbI(`NmiC5<8C$;p50`j?Ssu3qx17{tJbXLSehKpJYRg~ zG#wa&978s7{<@U*_tKCw#KqX6iznXzocHZ8OD8kx!2myG7x_5}uol{u$YRW>$p$3nLDS3ZHw8=xvn_=yFNY?&b~L#k3`ILgd_} zLPnYm>-NFxR`xiCS}>krcCT4;YpH39e|C3;M-f$5KDm?afM7N_@mHUBXe!VJhf1@`Du zTHA3q=mrL00Vc@uR@}gjHDE1R)5HlX^LCC3%IseE^LPVuAM?PF?Dcn(Y6fjtM3r;G zEYJ86K`&GnqHR07Y5i}`(@@%Ww`CAB7B<(=&W&w!H_wnWJX{xYd}-CZNOYLy~Z^25Ok! zub-~~S}Dr~Hk$Pe@#u8X;y5)X{PCyfdYUer9Z(a;@x(DzOR-Mq1_odOCSWt<05h;- z4f!?=iq2w+B;Ik@zKcoHmV}Ujx&^;421(!3B(dul@Ok48SI@AXX$@gt{Q3Kl8;*O~ zV_Trw3>ehpux?;bxLB4nwkSNOOo`Eqx4l3ObN$M&JMS5d9LRko2b>za{DJ8kyup-s)MoD6Zm!+iL+ayV6SDBWr1l zW_vPz>^XcxRnMc6!?GX~vZ2FF54@HGOu%M_dADR>26s?_H8#SMbl`^1FKCX@i<{bM z#lkE}QpcaW+u{`G&#H{0WxmG4`Fs_kAqTP`6S7rI9N|{Va#*f2D++pGHo@~V4#2Ft zc>_1nEUq4QN_QEOICG_W2Rb|>%2z*@BX94Kq%tvk6mlR7GA-ND;SI>ft?>Rrg7V1q z2Qs-eLn%=!X~%AV31H?u?Yy9!ys(e3Yh<_)T`{jtJ?C|=H@cFP8np7q93}@c+6_6{ zT}>*7ZeVbSd6jgphPha#1n?D;`gLVV-*2Z@jy-LBKw-)3n(WAfAFm$i%{fw`>YS90 zIU7Be9jGP)$9d9 zp1)Nlvv8mX@~G(yo7A zRWi$QptIy>6wOJn5RXn@_{$}_V^)m5@#$&mwRnxwu_u`*?39w7J(1>Z*$3IBB0Lf~ z%6nu-vB#DO>2Yr*+>#l<4I$=SkDMA-apnilzei7VaDC;i>uezw2j?UW4C!LZzWH1O z0XoFhafDsdvZ!n)UCVk5Sm?BkF)Q)>7;~VRQzPtZ8FjQp6?dYtPTiOF7_f3TWU@ME zul+KgfxgVdC_9eITX6?%<({+``XAxh-=Ye7%Q6n- zdp|Rgu*o0UB+w*K-=kL?apWg^-BF%?Sd{xpY-r$B5D}XOd>ImVU<_wbtz*~_;i`;kw1m*?XzV$ueZ;5!19^MYS?+?P~zb4`A`6OgA zHVCa?+kjF#H{8Z!jQI?_b*vA@M0=4y4a(CYzj<`nouECPd1 z_wxHp)`rnB17wE=7H-47Xd47k_Y}O34RjV%Pyz7?gKcQKs5!W0t+Fvu2BaARAA7df z`)=lH8iUmVguo6y81e{z^xYyXE}Q(B=FnWSLAGk$2w4kwS2l@wG5D9>nT4^_e{le? zjBxBzdz9C2Ea%{j53a({q~VQc67js8Y9S%(0)MC&{HaeDK(=dM0QYqVV4yF^-~W6m z1q(|#`0&eX@XGjXp2VU0NXS|Owj6j7l;9~Dci>5sPe4?%n}-vL_!yoR}+ zfV+jswp9Q>@<2O(p1YR89Y}E{HZ3)d=9DDiGq7c)3hT;d{HAR4@>m``WgNROPSAt9 zqV2Hb?tn+&m)R7z5tgmS(j1or*(wa|@mO9?LexQkJ&G2ljz=JlhTUBK;@g+tWC8dh zgTx?X!4tbWp{qlcnVHKe$Sr1}1$U*eeQ-&TEwbtUjn<+yIh)xHC}hGrxD~s@E+aY8 z`lBeH)_{OjeE@H!U+afs*xy(wvnbeT zdd`fN)&tcB&ruk^Ve*v>kK6)0)+^M^)7U z4mO?j1hIrsl7xl8*;}afy-_8f9A=TP&L!zc#@WY}gO#>QgHIT|hM(rH>TZ+_(w0(c zQwsqMIW>WRQ1&_T@jP^IZ-spkmH)Fi_L~by&rPBtcqJP@ovE*O+Hzu|6p8nSqp*as zIA^5cfU^U?UrEE28(B_GTcPXNgVDaY5=q&-Zn=;K{243z!Ad)IOkgIRCe5Fgo=o7B|Voj!t7P z_bjNi0vqKK$ENJQdxG%8n*(0=c|D2ODU{2>krD33tBL%9#&5u`$+Zft&*0FL z^Z#bCyENdLheB}SavG8tv~B=%7Aok4gcQ(-J1)`CyHT2n(xGBT?KEJcw$``PjlG$p zY8=$~SV^AFnfB(p8zoKCp{02{y>$uP<{xU!9aw|s4*f4>Q$kGwTZh2^dRD3HJJIB3 P00000NkvXXu0mjf`|+n3 literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/packages/mobile-app/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..4e172f11a9e1c630f0397817f649b9c607420ff4 GIT binary patch literal 1024 zcmeAS@N?(olHy`uVBq!ia0vp^IUvlz1|<8_!p|}=Fdz4HaSW-r^>*&*ED=YMHtosH z+-DnsIp3)@dG^pkw>g77fX&$ z??kG|8vsw3nIIZZ(nr%b_di?u zgImWXx>-)>bl>eg`(td^y^r5#`Qq*RZ4V7XqMQG=m|Ab}`?=@z?)%Ty7fJA34eQb= zu&!Rp8)hthe)+lg|1QPPk6&9>r)!tXTja6j;3wHlIsZ1bKK|e_DWUb-be9a}%ndbd znzQ%I<ICM9Z`L%lQ0xO7+$^AO7GWmALcA z>I{)uRY&FC4KaU0O}v68Etu#Qq4)3Cr?=PZ?WZ&J0`2)29=*d^Id?iRe3-=mbCB; z5ZgIfq(0f9`>lT8hDl+sot?X1>aW`{Nj{E~Q+L{ai^HdiY;;(*MErZir+u68%)Sdr zKfIaNtZ|?7+kyM7zUGEa`hqXGbeHT`IheF@*ZKXt?s(S?X7DvZ%!`X^?!5KpMNK(?u>tV{_fp( zr5;OfPCK~n*4?1;gs#W&cM?0TBoCDdHi*1_&Ui-o>Az3h+wX^ZziumF+rFm>(Jo%-IZ{ Lu6{1-oD!M#s`frMiW9@pY;W` zVvHzJgai>ppdti=EJBb%K+pg!-M5*!*YEr9(&@^b=|Zji$%UEWp6`6;+_U}v#gPB_ zGHw`ft+n!?HJI?~x)R+qRb5{L2GYA38l#3?Sfpu)8g4P2t`Tb3kx$bIHSCC|X^0vM z@&<~vp_EuaGVzCH4V)3dp>U$vW{MdH>!8w?GGNS*^nrA|Z}ouY{PxVkB3U&%Cfk0i zm+ueN$?-GIVuTGmjs8D{V1LLfhofc!uj;=MDxM*Tm2k+A<i%CW~;5yj^fqByk*saQKvR;)WMmA^NL5i!I{ij)>- z$*8=bPTaAvkUWiW3c$j$2|2O`xv~3Tofx?R4QN>dG0a`V`${^<&z)Kz6)#TI&gHV4WQW1eFKu?KaW4hEDY8xvY#MGE;Q zkbfM+wp3n~U?gZ~C9X)eAv5lX=zFdr$_YpkmB?}`(ETM5DTvr1Cd}(2YIhM;(K{nNBkc+)$FzgeJ^DwOAKPUiA^2U)$Y@XJ`D(^kl&C(z8zx$ z6Lw2wtpwoBWD0q=ujQb0-hE4s{$Aglls$hm!W(w?<-L@lEHQ{hOxK7FDMo}sDl-F^ zO!0bzO&9WURFLKC{*Mm~Y6Q9iv;K3i*(r!p#7{MISi_G>N37(F%~#h}N1^HDytLN8zwl}>pNfys6lq9ClK)^OaCooFl92k&~zQA?lT#K}zIi*es znmoHYWQjp6H(xrDhiYy&a2R=qRM;NRG0aS#a z4%X^n%rznkB44~zA`f7qESr!mv8YXKKgI_=usOqi0-j%jnCA!`O(9P#Vg~xCHofso zdp}^JlyC>dom8q3Ayg20l^gg@@$Xi0gv9cBG5HD!o7;uFyDKwIYw(ZPol(D&5pGU! zXErt#_8QjCWA%D4;<*r;+ojrs)MDeJr_(F#oyc2$H}}4dRs3%1`9#p>7a{)FF1bn0 zUT&AA2&3XrA*sRUq~zJ7QioYa1?>uDB7_#8VZFJ!Sl-xpQGUP%$cQ@Rt;~*BGCtVy z6vBJk4>!oRT{ZIhQ{yywuF4Zl(!w#>kPd0rv4@tw|Q_49~vz)t4$CX8N9I2Y&mkTK|oyaRf; zOXqSP((#U`Vw!`0LOq~|yNslyWyw&k|H1o#5%Gp*#JnRL5pO9+#Czfq^QK;I{BMMNHX2SZ%dZ6$4DEzW72RjZ9!LGhx*z|Kh96gZ-%hr} z-+L8epi zi;jh;O;!Q_COsnwD|$Nlvpl+^QPczSKL4TR_n|8iXC+`-XQFO>Q6r`pMaT3RovYY*Vj%BA&+Il>f&O3BBRC=g*t&ZFjt>-I^K%!$XHQ>Hx4tP$00B}M zaUp|l;*%#YfGaOf^NWr%8Jo`~L>17`+83UKZf8ac3TXYriHt_!#DD{DJv<+-x+DXu zm#F#@T%Rr^#7+PKtH9rg09~^z0}lERdeQO6YZt+^vutoiT`1?kB|p+007Pp)^9q@by1xnG!bL{alJ7loN0F?bP!x+`D&(mKe~?hzNi7dWNRiyM=KU| z2BNIW-^WJb;6KCcdh9Nn>!Lb6c@3R}S?D0RY7~%_-uK3SAAAJPtpYxSjSHWv7pI{k zB}zZXvUS&BFdEqXND`=SH4+rDaSOB1L2#AN4=kU+r#1Z=OAsI}w^G@>Bg@OuN;jyk zL}(<0uNk*63+)6~T7IoXGhXS(|5h*QDhjeJIRuWDARtlvoc`~sk$~6GNtlI>g6q}O zl7b8KG5kDlW~wwQ!m_P*2#(_NbcrZPeP+;Gt-fPgHW-yvO(uNPGB04`VT!tk?RWjYD7&|PrL-4CtjFU90b&#Y8119oKv-(mQ9 zVo)aL>l9stS+pvXbvgvMlC}Wud{~awzu=sn^V(u&&9XEg zfD-6r6`wzQVUSEwLsWHy8h`g|`{7;epq*(B{BZtvxgTSu~42X1)bPuQ|E8w}SVYb7o8{U;W{JvYpP0{ZBt!?_CiUj^2VgE<&D zmIpi8-2NYKH@1!1NNu&HjwrB|TctiyU#ZWQ(7^>!bd$mm5EJ0Hy}FkhsD^8p1j2m% zqdjmpcF<{pgi85e`X?z}&W-~)zL3@A+=bdiZKF0S5mw+Uw@Q7ZK2l$)&#l;F)Djnj z^B%C<1Y3n7Aso)BnI^f6b&r&iU^TG)usr{(`oI6s2y7<;+7i%+yjW%6Q`>}Ww^bHC zEAXk0)K|yVY#u;Ew1kA;vOydj4(G^{*3n3~5lI*9VUOvBv=tf zLWTUVC7C1t4+Kuc_}`KQ(zU4#8$#fhCZg^5^!Yg~n2(xlc zWPR%&TTmpSh9=a?{|;kh^!;v3z@TNiZq$im5qxTcayttAu;5Z3s4vu~X2R!KRztZn zsZvb{3$ticm}}&JPvO!7um4AMKh*?RR1sWd`K~v?@&VkDRta;2{BOdp4bu1o*PECb zu~rCk)ckL)RV7^nHga-1zIty;B`+}Mg<^lpokxird zUtI+^_MLH!3iJIp&Bk4}dAJJi6`0;&{#QrA)nadGqr!asni;(Q-xe|dt8^m(Un9ZQ zW_NTHVOEnWy7Td&Yi9=FM#=xuy3qr-%nrbJH4$8`?hmLZ%#`vYKITkM!VFB5c!7(m zp8xglu=Jp#Jps30KApj*n>>Ujf~(yfGEszi5Ocu1_7nOJRLTFcz~cMdkOTL=-V2+t zKDx<6-SQN-;A(k~ST$j$^5G9B;Dc|E2Xh4v>gRtIsi!bHy8YF^g8Fn2Ty5`?3=8wE z=o-Gm*!~+7*hk+!2;FHrux>A6{#TLOpDl2GTXzlci;m+IT+sPWd0&`)O}P1`z0jX? zVDa2`UVK92j-rgggN*S z4)QVtd{PSTwgW?v^1rIS?2yYZiv<_JJSzff=a@OJ?+|>S7i}A)=RhnwzY{LS)zEoJ z%I>}q9uij69S&A&c4kbOr=>6nXz@qzHq#BC9^!DQ3xQuWrwwM|vNY#^ab{d_J@dq@ z(@Fxm92@5W1oL1H?|j@d$Sb3i{nGXEGn49hK?HDK?J>foAYmsa+#|^*%>$bVaA&fh zZl0QG2r$JEUW~3>n6rGY93EW+3=j`l{cAj(ZeO|uL=Q;QM&Ut>v zHpNDJdiZ$&0N@$(S>zYk_o3r)bH(1rD8VKGaJ?K8x%$fkk4#Urg2ZHBWcc1lAG)R?*;l~>$p z?#a1eG#9Q~>b9bMXr3`V%visps`k3=>Y2sssIj>&=UmPOA#V*g*J17p92O&fYfc*+ zHWi!-o(9f1%pre&!@qJz1NCvuqH_6>uM#pmM$%uj8EffJp}NO4=IAzGDE<2_Km!{g6x+`telCWsto_msixWJ~5!j(-C+pdM>n>w=wf}F~(9PGbA zP$r3cOLFYlR}3PZrGGvcO!fhV8C!?ys$bhjM$>j|Js#1*{gKQv#(kuDLybn;AT7FO z<%qSdRfKb8jgPMon}ZuK^{LuV5*BE#LtQ(U*?8nsd#XtT6vV1N&dk5 zayHT$lZ~o+(l?k?#y+lZ;(Gbp+Xpbvg8_-C!y>Tt8fD8v5JM_&VaPHlV|~s(KL(Q0 z7&^u)ndZwEhvxPhc)wn&Z*Q{Vr-QqMqlpuL4PKnJlap!BBQDUa&CWW|b7m;OnW+5f zFj|#m?&=QsYW%6#k?n$<8OZG(zwM}}4#*eW zuz3YIR|Y9R8ee&YsNSb3>p~|pkaf@e4q;=|u%@pQ@@F8eJ+eb+mJV2dLQ!Bd1~}7# zl}q~QI)Iw1s4_yU<0i9HIKkm40bYZ^J*lG>uGAZd!?CDlAO1}-6Y+j#(M#vJ#Nk=r z8tD;d-Pm(Dr!HuT@lIfyuxdH9`Ye$h%9(-zPXmgh)Z12kybxrHqGVbM!K0F)+=*ct z#LPzT40|5UP5n|6*us^DP9iE_V}%n=`P3GyV>@EF9R@L@tDTB01o$}?t%R6S@1m=S zp#rN#yo>mVfdMgBzYBui2LDus6XYXT<&P#l?^7))KIj>niSLKty&N=UcuS^kZ~f0* zbvc&rLiDZ`g+^8WC|YLInxvxw6*qrNQtv!@17#7!yb-FeJt{q~tuV+;;~?gIY>4E${Rg*@hDfWjSnOP~E=4^i=EHkq*IU92>*D&C(%`7~p*>r2OcYKva z;h~(nzp&zHWvi9x&iZ*GFfJl4*y@{Zc}`o=?J=BQIG}@D zxP_vEtZ;S;|GZK4QnO~oGsbMV$?U_ymEJA!jBmFPlot^=K>sP!;7w-F<#+VaHKzf+ znD*F;XEDILr;=q_!#WQd1Eyh3w{oMR&Tg~kDcsaMb&Mvi(A$~h9{&o1Tt+TaB+H=& zCYkNR-)y970D*t}eG4H3SwoRva6H?We-~o|gaTTt74HLHl41vHaH5gn`50JS5J|$g z1hrb1Q0hq2;C2UuaH6u;0l6_-fSoo${h=Udp=GP6ue0bWhTfr}rK13uzSt z8|BNNom7`O81DB|4Y{5NI6@rBzr#08NEBi2ClkB@v~=jOwd&SWDEN>g)6eVV z9-%Acd&kDUPd%+IYc19XqHorVn_*wH&TY0yD=O)k z3Z9`i{zlq%&E{1)s&RRERWhuO^36bhE1y|(@}0XD9&D(>0XNB zN#MpJlSS`Tc^+N?#fw9kWk$Nf9KsWr2Ael=O*yonH|C;Vk9Iz`)#&wil0?9u{aqGBVJYrwp}0Yzg= Apa1{> literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/packages/mobile-app/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..2cf9af9b5e84c6faa9792e8a8303bf20844e4102 GIT binary patch literal 3626 zcmV+_4%P9AP)ysQ+6+hk6v+rz@&4WM^1DhqlX2U~>Nkk$ADxw%g5iP#@4~Pg6Nm+#mMo{siAN(v< zA&4MqMJqf+0;wPoHsO)Y2J?O_o9rX|nn(Nl-RV2i-8;KGJv-Aq8@j6|nVspm=iGD7 zJ&)hH-EB&IM0%L92x0}q3W$$b0p%ax++)vW_Ki=BaG$-wh!s#v)?6bBF+TBbBw|Gv zvG0yn8AdGlXcb|^f{#`aMlAejm0-kzk5&;zEc|GdV8p_YRtZKd_-GYj#KMnO2}UgZ zXq8~Z!jD!7MlAejm0(>c^EWX4WFjYnY!)xu#_K}v2h@=DQ}}iU?3~Zw4L)cGh_l~7X=&$S>lHENc(syD!cT!U#Jk%?o5{$X-_f5lf@mJE0-`~?SD(4Uz8s=Gn#tkbwG;2ZlrsK!wLrDMl$>r+B_x#K>W>6YZ zoz_NEZgLcmz=v)D@A&VaSdjZm53ebC+%T08xeKx&i7W)NV7?`PeW*j$U*4=EcJZR5JomtAx$&x&oCO=X7mKOS;j=$&T`emvZjekmJK1gE`~>p- zo7c`OxFlC_Zoqe!v?cMmk^i1FNQ*Q{n>?s`p=MV=6|7$gAF1P#MRoGb58LIY&Uuo_ zdi`?N$yrt{sh1TH6tft^A3gD#I*fJm+7>647?bjAe>F@Rq(z#fO&-Wg)s!%U{6b#= zT=J<=2!C;%JPiSChV?RIal05=KmGIZ{t5Z+uY2W2xiW0m?-Tev3$fq*vrq1M@{pV!NU1U1`Xfz_ zNgl`xc_MFN2WB%Y_rrOm5ft0nEP3|+g5|rdj#kX`PMu51*SGiNgpah5#Z}P?48Ogl zYo6ETI8uJ|z=(`a*s^c`ux#0WP>pFiHG(Gza8?3&B5≤yRc{j%HYRjZQV6-`bJ) z`EJX%o%7DXTJLzQM|O7)i`6`Z@Lk8vUETn!t$goCBXacAxL6C4^5#23YD|u87~Vpg zJm{D}-gJs;7*Vf)3Ryqrp^7ZHkr#j5keTQQ-( zZFBN`myoo@YRkXmabM` zR+{_az9VCDkP4{dSaxBRIZ7Zc9TVKyk>-uOl4rdVsu`EfFyI9(eD|U9eka@6^7De* zvF*|^^v#{klPhB!`;Uyu8TkK1(z}vWkaye>X_7X1$P`&_z0t1hXRm5b{=!qq{2|Isnv(m4eefsPa{(j;y2py~o2 zaqW&O3R@JBs~HNs;1fQr=@=${*DrhXsb;#w2Cf8w{@s}PE>yVn^JY#py|D!Yrm`rYT0Fwg{=XLr z=!Oe&`~PAGX_7X1xZ9-!w>t_0wlKql7qr{?HYWT!g^wUNp0)!w%me};v@E`KlE;VW z0}nz8$)qu(OCxRaKwij`c1K~t7G@Oig0}qelpQ#;A}V_2rA=}X3M{|>&w%bbAB_eY zlRW5^K;Cp<4hyz0qkizugZ2KW7wBi5nU4vj9^{F%K#$c%Oa0oCwBxrZCJAn&L3O7O!LPJ2@~`pez( zV$P-rn`!Ck{%$2uEu~;6U<2^~#jRGs{W#zEVKdg72hOYXvm$#!4Hy5jkU?+-l(MS} zXAEGc*%ux@AiF*oo&;=Zi(#g+0~T^UKr?%Eb^%LG6#jO7N0T%-5QBU2rw7vV_QCV2 zN1UC729$M!SMp5WIS1QEOVj4uDFe!)Tq7$#2qPCCfYp5!zVhGi59f;(j}q|z()me+ zsoa3VDLYzT>ZXz(W>;viuUX#UtS-B-GH`Sgejd)KZkRnA3=2GxSMp5WZJ(9v;*xXb z+$jTN7KK^)L72W5{`M>^Zwpel*WMe-7cHJ%fSpsq3kX>2yILeYk`7!+GX1pSe%Sk6 z-j>V>Ctby#%dnLH{G$%pu&z~7ShDp2l%Ud26h7Bo)+8$z)wy~Cx$lSZ-M^>+ zRs9B+26-V*()DD`5p}q44QL6XdQZa!Sxei)ROOHrK&= z=7DzEbWN*_QwhmnE8*%(8SfKv^+hv~ z-OPTQ1Tq||aN5#-kR+35J>}Oyd#8h-l$oh&Hr3AQ< zr|&DQgy~{}Dzf0?yH++z`?C5;8?Z?C73Ay^u=*uUZTaMdJY}*z0MRKfITy}}bK@LS zX}dN%VoC`O2`o|IKEF7a&yMV=lmJX+!_|lh*uyhbcBJ74Hs3E>T0bczoB++2aYc*- zxcEW=h@eT@oEleD$l@o)asc|PaBxo}R^ zxYo?9#>K}9Q*lyM?m~Fu!{JHiXxyDa@A(G{b%I;CsGOWQc!{iQ!xuE>We6RRK+}2 zP=faVtKs|g3NU>d*!k{YZWbj#1{|9-NK2=h?trU#Bd_FH6fOKK5b2qMqWwxDz3dUohnDth$9n0#ZqGp<=^FNOgx1d(5)7iv!jTkPx}l!tzQ zTx~zmF~W8NF*X3zc_4RlIwwAGX+ozAl^V-}zOq0^dCD z#R;UlJ~%G}yf@G^?ahpT+@AC5RdIM>mU63!J7(o!3#ZXPk;-K#U%aN(<9agq$Ar&e z_iJ!8r;X|Ib^o|L_`cfkLUn?0mG7Z!J6Ao@vPU7L1-Spi4INGS9chMZ56bIsBN=2m zMk3g_YSkpF4=)6fmsyi{6Gs+v4v_JaFPxGcdk5r0?0Dl%m~`W&_tZqr{|6no2Jf0> z)#Ck{cxS6YSnJl`9#)?%Mk{gywxe$bp2L8A5)g%8Z_tHs%f_0%XPnw)?ev5ROS3bk zIEH&N!g3`wch9&8?j6TuhkXED%`$bo5QHD!xU~ju+!{YgZ_Yo)DqaN<-a9&0LJizK zJ$`;b`~;2o2^#S;Wa6hN#ZOU+pCcAONiTkqUi>V{_-U%~(^TW<3CB;=j-RLaB>(^b literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/packages/mobile-app/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..799a35550115cb5c6d989ecdcb32f9dc7d789992 GIT binary patch literal 3109 zcmZuzS5ytnK|bs+gO?M^NRDbv9a-+L5=KK zbH?9;hns~ollgaSYs%pwzfKbb|6vZDs00Es*5hF^PAA*C|-vz|++DyYN%kr8s1(FRJ zq#&E{y&KW}3o(Vix9_InSJl3}QGZTu-Nk5a&3>U5gg>CAp5x(_PE>pdm*(M){{_;b zJn`s%;1}h$qd2uIv?~>EGb~AZrR&(*L5cT-S5A6Li^}JkyoH1EDjT`u5x0ZS@lnz7 zc?0o?m(t)KO~DEFB$I7J;cLg@dF;gmPad;vqxN*^3!W1^#%ZYXU+6YqE&Ku&WkkKW zdfC!AEDQd#K~YFb;`nKvfb)>SPw;r56H!*9GQL&NkLRSg$hv0UnQsNr(o%At#dh7$ z;0HQZz{gqJ{hc9W*etF5wl)KDIM@4bQ?$T}Sw>FtZ9pdc}%m%ed_sy%M=}y^Xfw(o)~Bi1{&{&Z4p*Ddp!kU_!6q(n-^ZsEl^6@#}jh$82u( z7{4m{!|zmC*4a33q={6CFAACXV5imp#skH&VJ^-{#NyqQGIDj~r<)8I7UsM0lWk{D z;ZXMm?!wY$j!TtF?cB23anCvM#v2+JRLLypQ%n|p(!7a=$18EU@AN5pkv8heI zGmu~)6!)&>N|cjSaEF?^Y8o@z?Jy>}-XpKHDbRi!zmdkJD8jw+62Ut&yJjjT~-Ol6~Yv$6e4qaWiIHY(m4C75Y5X1^}=7)Y)JAgP$I zz`cPuv9~y-^H17}D&Jv}eYk^uZe9KUeY~r{x48;rkS)d3uzwjo{&ag^pv7{^8?!68?PR*q5F5CtS>pCB$6gFFRrwv!sakNFANf~K{MI*&;1L`c*u zu*wq?+T6AExgQjKti~$sOhk0Y<8FGv;>@B%JJgE{b)cGdk zSo&XAbUo~A)u5#){J2o7={_OhNiI`ZEbgxEM$xnNiJ;rM087zhB!Vbh`HJffQC4GB zpXpw5c&V`BPDPlPT;K?7*${d}WYe%j;`}!?@z&(eI$!S7ISOePQcb|sK_4yH0=>mm zG*m~_f{GS=eJ^x##_^jb(Zd7x0oxyt-1q7D1BU)b>YoEY#P<6yjjXD!UNcAM3?s$O?>P%f|w)gsufszkaiD8zMT-cv{N)k><_!q5&f?sv z8wf9&TLRX^=nZ-Jrzb8n=h&rx`T5M(jeT=@)m%JZJ>j01U2gW2f)Iv{M2LXbt6SUC zwIt#n=b=Cnz`qJR?%r4@PcvpUe1b7spUTI}J$y>!O1oV?aOGhyKZEc^Pf1CvlEIrx zt9JPV(?o}-FswuTKvk;MSiuQEyBEY+i7h-WO_tsYn!W6h^{g#iGt%4RYI%ONEM0cA2DS1|vQ%!eOib%WDG*uJ zzt~1fHs?DzYVK8bvHIroWq2JMv4y4&V3N&`w(jyD{jgd(Zz@eVtAMHPf$UlLkj%plqUss2lYiBVLcv~6&gIGQy7^PYVX8h- z4mvgNbXFlJq#mcvUwg9vjmu%syq5dYEr*-iJn-jPJ9ASVH;awc!XfRw7q2gqh9F$O zbW;ppYmF%b(-9)X0{WEGx#k^5cm{)J28t`{#E{9SBPRV@5}Hz!R9c=h*XYZkSukD1 z%A-Gga9CEPGkLDVsI!N3FRS8+Jm1kecwLEGO~=YZTq1+^EQb9R5uM445_s9rrA=u1 zsaWl<+SarJI4;iFh`+9Ci|gd?k~q(#*Nwwmh*v#zRoa!EhnmbKV`jz$YXwqs$n&nb zhTPA8b6o6Pu-5-<=MmzjG!d&4fmS>0IGPteY-QOsnNuOfXvJ=qdiNEBm7ay7_weCJ zrO1ns>%oC{ggIe_QhU4wOa{rHY*4e~Qlj_9FJDDjZU~0Fbug$EyVV|~wMGgg>*J>f z2hU#E+p3N6oC^unXkx{07HHVK@^>7lyZXE_J$0gP;`fQBGK+woMee%-@-G_wzg#Nc zPON|J-ieIMKlMX1%g-;-h3l3&tCJh7(rw)KCzXIIlm3&tZlb}e@J zsLX+Zd(!S=IpBGAtx%oPj^x-}i~^B*F_l64WI}s^si=5`crE_=l+rL@mu7(3D2cU!*LGn~LY*7i6lgR$Z?e zGSZISiU?A+a5AXwGtw^(;2{mkj*2=(Xu>6$ zlGrrzdUUK7V3Kpy+d^2#xkZ+VyUCIU^yy4?tr-_th4{^l-j{Zlkvz5b-OHqB%P zDXK)TgOXn*L;|;pExD=nb7fZ?8ba)k8kf6fb`Ff5u#|8h-eMX1ZKcETz2H6;IW7z0BcLT0|OGbmlu~+g#(R zlZ0}AM3Hp0L8EiML+DfOa6J{^UR#4>_kOy?Sc`W6ggIpnMTUIg7XMbJp{jdZRRu`z z>+$Jw;Xc_8T$}g|e}!TAx|>+XcN}7ewqIna?R!Q&Sjy)h_Uoxd%E9ZHkKaUXZt4uH zG&;*H>ATMK#f7!MZ1Mh~L_QCP>{ku%bWxBgo2$$=v*Wi88i-nx-DNop-*&&Z6;U`a zd*hFhv^QL&FbUmW;2FlB7t>JhU$9eVmt|`iwAiKmW5j-7rFd=eJotL5EIvygJEry* zazoOYJcjiPaXmNB;;YvV5GT(1+d|m4unfsPLnS`q41+pHX8>}8u&F!P=Xr}5+IiA~c zq3pV={&)=;HA^}pzZcp=K4kg@LcyV-aur*}i2U9^ z=ImN07$sj1@$Z{`xWbQB^4&f)jmuf>rQXOsmpRz>_M9Z;jtqzGdbi0N|F?ZAa^nN8 z&9j@G!ubuFE08|)J(Ucef7|B&E1(mLLBFy1f9fbEBNIAXjNwVdw_Ct@RGm+yow~)-g?fHKTwW=HQ_@aZI VD*@r$z^eajX2w=V_1EC>{{hcd3!nf1 literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/packages/mobile-app/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..c93a6dd67590e2c95e72598bbb96e7933d920d0a GIT binary patch literal 2492 zcmcJRX;c$e6vv}RjUf$_nnJW_7SzV7L8Mhdn;0@_u!|;YTWCQf7OCf;2MHC*RvQJz z#6mePu?te7MLmiFBD-ld2CaZtQ9%?DZ~UcvN1vNxP%DSz6n2)^7td3O#s#Hj($%}2` z^Ra@-=M^6_J!msldm6mzxIPm;$dB#IslM>QH5%_9|GIR>>F$(cu1$C9PV#?<`7-;` zh5HZlYTd22%12D^;XPA+n$%J|*4A#DsN6jR^*f=lj7;=fTG*v*Va8 z58S=9v}&=)Nub4e;s-@dj>`>05@rCxx615%tzT!#bi+uA?L&|3@(8|7X&H>xusT z#QyB4J&0Qp{p}-Xy5l6BYgfOx)kr$UKEEb;p@=0{>(n@y`1EYw@_{gnrwf_sTH1r> zdKiV+8pNCz)V6B7P^a~q>FHLR)UA=Q5()9_ftp|!NUeC&%qS;i0TfiX%AneOa*3#_C?r`&kjd~B zQl;XhrNJ5Ymf5plTaLZ?IA-MOvN+k-aV2}aieZ68T8smZsSuXAe)Iy1fHhwMC#5K3 z{fh+|2*Uy4H!6&v*|;C_mraKk=d00sp}A=^7a4Wyd$Lg# zB!>YZB+ugM)Q}t|2yzbHD1_uN5dOA~%s>@Flue4tRlq$^Y7~;)RnOzQa3#YXXv4zr zk2O-Z+@8f5=>i@SK@29 z@B|}HYB3Ux1^d$laQaaT)TR=x!8aVtuYR#WSr!TH6;goU|ei!zfUGRvk%=~?64tV3v z?)8rjAe$p9WP6}+fd{gE_my_7M{BOi}&g!xT#ag*YzKvQmEYfT=v+X0H-4v|o$GpsTM zRfEm>M4A@E5o&t9GkD=##`Hs09(>GH~k|4Opm?tCita0unPSO?eD*~l7ecTE z)bd4<+Y<#Lf@Q@dSOp;gLpB$J;W;@g8 z^6T&RJLmf(*=Abu|a3n1GpfNZk>vduONpm^JNcQ|$u{F|_#h`IgsW?=zj z!D2zx)&NSg+Q}H0gawTSQ~LraO9rl?U=kKM7D(*_pmuVp4FQs{z_CEeE`V$hFZDnY z7CaV2*#eLSj!{4o7CaV2<^#wA$5%=_7c?@=_26SaKY4 zN<>Z@OCe#w6Ae7hBoA+-$g-P1bV|y1NRkp@;urr@a@q?-!h$EHt_Mg)N_4}uJ#^pA z7tqcRM`-WIV>EbZjA*Q+z(fqDjt-ViJPQ*=r!7E~w^}?TfLQ#LCQ9^;8~W+j_b;TQ zXC~<2@o^eFdY*POh~6F?p*=hBQiz1j@SVVy)HLDd=|(FQV0V%~jx# zp4mHbc9IUC9H(PvCh4Uer|I#(9i@DCjwaKLq8Ox*wBQN(luefC(m5S;)8*ajyCMSv zFQ_W0thxPp`ux&v`sR)Obm#SbMB^o;_9)|r7P76vO7T1*0wQR_p@kO}7$mcl^noXa zuX9AMM^=_dV+@>ayU!_ms91`x582HRlrrgqMa637cw}>D=hZttiw1->+;z$ex^`ig z0@Gx1`tJ#kEiin(L~rh80Of=NR9t;Xs}z!10}s}kcoBbm$6PwJ@p|=+&-lKc1uWY? z4t2AN`qEWBQy}$eXGe>)f;{h++?f}|Ukl)W#t_&(Q;ahz!RWnJ`( z`vzj|L-r(4ilh%zM2V?|Rrh$n<54NzCmnysb)})n@a{Y8pPn4uc5QDZBJJ0DXXWVU z_YSB&d8JI%K5(CNQW!6VjCL>!?CBJTCuuc5@lUeKsJ37;!L)w$V*2Ix7jj%DgjeKx zbiHi1eC@j4DVN?))A6V`wi4loILF!EW^gOOiEgZ){*=RZ0

_V;t)b88JH~*qm@0*g zlt7s9ai$K|erJH5Sk0aeKgFdQhAE6+JnheHJxM=%>X3@-)bfO|RRh=Ea&=EAb@%-C z)9N0R0YobLh`yrF=sU)Mv7`z_i2*1D(gzEq!2@0@?}A6x08icp&rdfWqH{yVa4aAr ztb-%UE0^_E2vxuK;V0*5)e{Hl53iq8_X+}$bU}pcFa`~x)Tpxv>ui(+g3&o~j@8z; z2k3F0_(DD4IaegvAwsJ!NQRZmdwrQ_Eye!v-VmKSHBOJQbbEGdco1O>7z@UPv8B$i z&Vn0{2Ora_4d(S8wdHG*IAsa~3;FCDQ{WK=5ow#n(t%D|a8XB1puYC*Ipyv{x;@JG zLWh62a*i^rA-SpvFRvuRn9%3VFCHbyEWSf#1RG`A)t5FrOQR)Pb?+s#acwy8IIhHc zj=}Tjldj|`h-iz#H($A|H*nvJ{<&=|f#C8(y5U~9C+?kdx4-(`S9)wBj16PNSX&-! z47uLY7?rSACkM}TM3K{_|g0lSWf;L!#< z+dsEn*HhE}d4soaoS&TPn35=vM0{`b0e#W^^6Q6}A4@&$5g9-&P+w}!T*^f52C#VcVAA_#y_}XaTi^3L4g9dd2oCE-65LfeL&vX#659u^kJO_k=s8f z07SZB%vutO6ss|rR&Ohap767IwB)IPCoh!|Ti2c<$D_U|c)@q=A5r$t%>Lot=)*J+ z2@14{jG;Z!3ZSs`#7s$`ZJtKK1%U=IE57YfDac^G2DJ<^i8|?=<_pM&8IzN zX6T86h~*%k0s^1FKmgG{*5s&tLECxn^Kst3%4mwTK}~}QeSgMq+9ThN^hBV_;2-Mb z?W@;aHp@5eq|^$(|LeaabQ}T1oQRZRL8)dBh=jqkhirub-BNm@7Jyj#eD#`M>Yr8H z{`og=pod|b%Ztd{QypizI!iU}5r6~PR`f&|`-cO9*nQaV8qn3GPbU_BtVsL$Tzn%f z3$%x9H+mu%K;;`^1DW(WdgHiXzRkhIBc~=51Mf5-0!^nqWLwb_HNZX2?)z-szJV1F_HF=?Ltq4@rnS58<$R{ zZxKH9#qPsnya#$*W#DjP_Mc6oJ!Cu36JdD ziz7)MoT*y3skz`0+BN6x8_Qk#BFO%M?X&wZOQ6nj=8dGA7ettwsOQeG23U*9guFAE z6pbN7s;kmZ^^<3y$nN{nOS|Zbd7ZxAp_VrLIQo3_$QTt^0vVBoRU{i+lyPV9R0#%{ z2H5oXqqJ`0LG`4_fT*fgdAROJcg#^7PcKsBPB_$zaE6ZmEMw_&<-D%y_K&L)%JH8O zmG@9IExmA=pTnGBZZJn$Vh2hsQI_mj1FQwsB$4=85`$Lf(0lXrBzGL=(4&9|tTeoc zW`Rg=-&n>H=t91y?rXKPUq?8N?|%kIRZ{U%skz|6oM3LEfCqDrHNaW~sk~HrVJZ^_ z8>Abbj}9W_7DReD4WgV(4m`*zMi=Cj1R@INzrcO%;0*N&KnYvW7_$Zs<^ppP6+D=G ztU;1l^An8&fm_lb!kO`+n;yjpDTy|?ASzBci5P%o=~JRB=XWXz;mH*tK!k~S03c#7qL(v- z6*FSiqcOZe>E= zRx0)bsbO)x!KYt7&dHb#RG#)A5IOGun+BqZQp1L|x;6jKqPZQa#)Y5#qZe;4$9_sI zh4M0f6^5~3Ow+(aQ{V`aJeWhwCFT@!>tuOHVud0Ah&a{ls`Upxv%}7B3#XPu6`N?h zF&AIgzgXa@e^E_U2si)pTYJtWqP~>j%VW_tkCr?^TYSA#3+51WNi9lX-t;}u(?G;e z&FYN@Du#8?K49&p!YpbQZ^^kZ4I;_>)0P*HDUL_G_@x>)k@n8KaiXZ9`l$6U&M=R(XR+I5NFaqDZWKSJ&=W@H2Wl4W5w-pW z&c46LCon{=F;!s_Jwc|X`SsEsc=E7)0j_n>CVVgLfr}ogb%G;e{fjsE43*1P#z{}G zw-uG1fX&k^dLmv4l%OHqGTI|#{R_0MuzzHG(GyYDKb1KWlcYVO)W1M`^_?O04`b{CAV-iTDB3WHpv3?Gd5=#a8ydoKpMI6H(SZ)tTy( zqCLFzFM@>2>!^PbMK6fdiru| z3&)+;_eH9Ikx6tA5OXV$-{)gaw^ z`x>Vw(h_0LY}s!w?LiSigfmqO=M-q5M*Ry`AXD`(!W;?adp81}G)9<{v4L8s#waG* zG8r+B88&R9^L?55YZZ?{>8N4=D5;t{&a-Xu+;8DD(HzA*VkAH6qfeTPd%=C z>eUl8JXVUW{>8M3WJ$8+W&b{yL{B6SpcF}=Alk#bfBC`R4yweMT|O0?>#ctgwhu5`j8rWA~lidv`HcF0={&N)^b#L zjNNd&Z+hVvb@JAYpacq9Sk+tqqE;X9Io05)b3OBvp{5tm;bY3bf9(W4zx}j2QGv(g zR^ENsSF8R-&Qt%Qx4u2c)%x%$pK3)$k1R=^cvM)*q){u_SGKO z_Ry~+>R(jrgG!+%MS8*0z&grS-n^e*D9~j{jE{0@aO@Zl=r#5L?qDTgk?3HW> z<&@_&$U(zF1`RTGK-IMccv7jtOi^Vs1}|iJW4+#9(H7u|(eV+jC{}Mhfxjs;OB!9z_g$Yep3Iq*=^D|%TFWePmh zl9F&1M415(wXCL~1yLrzLoF{#X@O)caMA%F3nHVzL#Zi?Ye8h}j?i>%N3}pQZYy#6 z_A^@$8MO~Ry&K{!kc>FMAW}VH7DTB({UVH*ymkgsnvYEx1E6*UQp$m2CV<)jNQMB% zYyf2uNQMC?8vrsLOooCcn*cHeOc?-8_5qYOFl7uh?FFD_gDS&du>i7-?71-8EP!mY l0J6^I%mt6Sq#-M_q%j$6htiPL>%ndf z`fuIB)EjCLVUdA-q?JDWJQ+4ga$xvB6q_9+J6Wd)(@(gGe-z)d_QYi4Ue_eJGkeh4z?Yt+*xBx<+|b)cznv27MTV z)c?83LHR_it3A;tHKKrEOuE(wZ2Q(IWxR`z>b&khD9UHmIUD*@D-7*uaQXK#ZrxFB zd*Xuub)wSX>O)wU<5-wvW?>c=4U7h!NSskH+$uEc(dVJ6qAOT5-}Hy&k(p9M7$1cZ*&R z!!9^#)7>X3KPjY(9y`C zC~c~@nr`hj(I?oVVJ5C;^HJ^WeWJuu<)DtMw=8`1_u7;yBX-er#>{O}3=rCIY&Bfi zFyXW|*r=0U^M;z~T#&Rpn+ z9kK2WLV}UtCEYB_+6rxa((7ce5L`|$v7mMv$@x>t3Vc`mHRRzep1(j5JWq%9n)%G{ zS&Xuym!W4(fH(=o<9wJUQ>gKaXztym?$txRCv!eefYXbAPQ4cycqrl-w1hks+(e|LGeDA}N07H>iq^Q;K3FWyH7ptyDJOObHA1-M8OM*v2PYoLw8mJvgY~NMi_h++FHlL6}A@9;K5~%y<4(X(he|lxD8GI7EOd-7R@)B^?>)mZ25r33MJQXOZ|@Z@ z()Ub$#Rw}Bwrn*i4Q`%AuH#=djc0e9$UPk|wue@NT9=m_1`4=c%>`#$6&a~Q9*^FxHIdF8rWiX@rF?8o@)mXy(bo;&+!J>Ab0hxd1a| zehM_IEQ060Pd5)nHwJqiPXI-(edBV?8jatNnY}<`PB&Pi{_p`I3>ORUJ;{ZX7mHOM4)1N+^A9@}aw(L6)ZZDAIBk;a@3Fv6N^rq<=sOZX%fzAnjHr@FuI6IaxG z*6JZJMbp*+TiyhFM9LAgR&O1~hy!R+erTpK`!++Jdq+0gdOyBBXyE_qu@oR%5 zpZ=`uwFpvIELZ9ush#iZRdnTQ)|O-ID4Zr4Gswu|%Q@R4H{O)veX*3Nt4~kV^R=-*r_TIVV^<$5oUTa`(A`esiq?vy zPgRFSFJLOj_-oI?P7SholU(e5?{H9&N^xz0s6K?g=2-PKw|U%}TE(^1^}Hu*2eL?+ z)Bk9ITGq~`+k@-xC*}+Vgv!NZEA;J9ZPZeINclDXo65EtzU$BBbHJ=mxx61XJyDrJ zu(`gZf*3+y*$G#p-V7v(}^&_>N8*?)eJ zj5fa=$ZDAz%C5+vI|W4ADF;%@WI`8Zm}-2DMX`xuR=BDkcP&b2gx_ncox9RMPq++I zyGqj?4E7JhHJ87*;}>2rM4!?z-Fi$#1UV}9_Lvc%Pm=RP^ZkDN5Ab2zELacnFy*y0f<^(7rI= zDMq6{*9ues@2mV}y=)#aed^h*d<7ceOMzwkab)T3ypLFl5jv6N=>DF%^I~+I z1RvDC;a~Yr`s)f%Es~ezdq2D8JM|k+rS}#S=<8&W+hpz*=aZ!-3w$5H$RSmUAQud| z)XXi3&@HfWP-18(6C!`T^xf&d7j0|Pid$Y2?%{?PerA$avcE$;n> zhFADF&R1;xsOlcHpxvWmUT9-4acE}Wpu3>BeXsP_U z>#sxrG55Vx89*Ko^JC0I08Zq=Y=jLfl6AU{qI3y}2PB+4U#y#1Fei3*x-DH{rR=Q# hUD3h^pu1D*oaxS7q& z=KbF1d7t-rzihsh#YAsfvUu5I5{a}V>TP~3iA1i(Kh91d@?A97AH;K`_@UdyJ!5_C zX~H;m!QfhsdzF*_=JVe&y%MTpen>7}5i?4@u)pX`Y>_&2ShxLQ`M~-|wfva3U-k-& zb`Qv_h*Nm;f-IanYtC(*9J5qqB}NqH!S_~;j&~IM(Xx{FDoNfqR0t^h5^-%*lZcDK zB{?oCokUyQ-frU$O2CZDF#xJy_PQr6C`Ng{vj+nB*-pW|w6Yr@{ukjdSEbh+AvvJWUU_- z@RF^WLT9!<(a=8WNap?GoY3`Q0qm#sDnoS zXhl7JSEKv+8u(3!Na&CNqKkH=mVkrAat$13*PLoS+K2LSJKM05 z27Pc>#7*f!m4;?!5z39gB&p1XC;?9;qM>{X&2AvjJI5wG4D?A8R=S>KL55~5 zUy8SdSO|4W#VsBDL+_m;>d9Rp)u3)>g=*>fUfGxGg0hU={TJjra!x-(hh;2Q+R#5n zvy4w^`|@ykne+)|i+TKU@cOKl#C5$gieHM)pKwnRu}dkChtmU_K^QbI&(ZzZ9@?4dm7H|uMffd5O>y>7SjBSK8 z0yXHe4FekiWOBjq%`8W`dBAym93iDZ+Ksk77XSz*8wsEAXjo5tj8^9wYN;xwzr_i)X2^d<-J25GnnWP!;2`9ml0iueT~6wXml@A z(UFeVXQo7<_$q?v_91610i&vo>WAa8hkH`@;4C1s!M*IDM-N)lL8BIa^G0jq8{y{o zJBM;5x7#bGVFyGP+k0~sasqWX?Vp7_fLblW0;9a@359(2k-+f%Wm67C3HzRAd6p1o zL+|e&l3>lt>~;29GTKbO!>} z_+mGz7kMWoSt;k<>lbXy<9N5kH`PC#Dq4N-tX_mOc`9Ds78bH0$A5XS;=QhG$0l1o zF@mn7^b;0Z8|z8_3B$gF?9h|u$GaduG3BGGm21o_XExud9Y0Hc9;$>xyVd2CwQEp} zwmvuXpUh=3Ih@rbXEurQz}Nml@N1m9?Jo}tj{5)OH;4a}Zk?Nzq(Rm!Z-RF+ OQdC4V|C_L+ynh31{0I*K literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/packages/mobile-app/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..441140ea57421709b2528bcf6ef9270a10da3640 GIT binary patch literal 5966 zcmZ8_XH*ky*YzX>Oz6_9gccAG1QeubBHcz%5iCe=3Ic+3A)!l?SST8#C!S5mWsf5a=}_>xxT=JX3An^7pMkH|bh z1GPt1d{0?TRzm0R`!zOJyO}0_@>}ou`8nl`Y2}x4J&?T zV|G^83W(o@78XW=YbjeYz4f=ZzfLr%DW0;lzmk#KBxaBoz)BcF|FBo=VI*|v?}NK< z^cKjg2M?&pIw7g&D4Q>Wb;G?P%x~M|@e2zTQMvhL3QkD-zIq&xsw8-W^{FY7ihZ}v z5o&vt|3EdVCUxz{cfXHb?Lg#>AJ?HV@S_xaOXjmt+`{+z` zmuARf17Fx0nT_^+ed2t>Z5^dl_3CF?zw~~r7D<+k#Aq}Ggh!0OIyNxOSk!NzES=DK zWdPMVWcHE6h?Cl%>&3;8!SP%XpcYr)CCjSWC+Dfn9^nqR)auTAY==XLuK9TjSPeL` z0_RQ<79~qAbDYrq;s0xxd`XF#M_vN-=)W@q;&|0JzjHkGocPFx^q4HI5va7Aa%Z!2 z-^VBh%j6ymvM93u_m3C1qsTCEZ!r)Hy47R_h>UeM3_LYPtA= z2q8ILNDuH!`-CRZmOmaXp(>bfE=))r$?aX%d#8G`vAvI?SI<^@@5O$L=AX$}ZWJaE2~N~d+k}49ZCmh27`+rGPw^)v4=pRg*m69M<{u5Ex z?8EG`Xw(xTCI8&Thuekj-Pq_Unh>c50w1r|hCb@s@m$X@kh00zvt|L$TTGn>>2nUW zxs8*vS|QZdb@7^9%$QcNmo=}A5>_-fo)^|px%#DI`o+dd)uA|vTW+|dI)uMWh~q;B za%WPqBX?B8VGaL z{rTaI0&4Usd`h^kQU5dX57iF01U=~0OlLW@KJ0^|YSKb{AI$&Lh}pnxRSU+j;8wH9 zMsbvt1(xlY_dr??<61CF_EX0EaFzAUZwI=AA3w)>@bqH!LhgEI0c+zIK`}xjt%9Uy zDz$GvQI2vKWC_|x07K?ZhfwwAOYUL|qt1YtKp4K!tkUx}LPED{k8U!Kk1$=}B_n1I zS=;#eB`$QXHyH2YLc9EoHxmr(E^OP8VrD>M5D&1McO;(kvfcEB<0%KfZqSNxb$Js} zrN?jawv+6V&K#HP$n-RT`-BBYK}hU~a25g@WEj)Bs^4xOQj8O;eO5ftqB_XvXV@=c zL2vrQDx)~_-pz4oQyf_ibL(CD;xB^q^=w#G7fM3>XZCWZxEL#FN1v4W)lN37`GzmN z%qpQ;yCSu^1u24dW=~|P4y4#QK`DY}H5c7EMh6uU8q!2}-jv6^o3Y0={f|*e+tqIV zFZt46_6)bIuR0>OUNP8tpA+WCZC`UdguEqK(~fiAJ4?KKZZh^koM-B#Q#Y~>qB_JQ zYZiYyOx+csR4WN0X8V>y{KgXmfWk6iyX-%g8#9Hz5Xd!#m8;xKGH-3Q`TtVP)FRXc`Di05i@~e9HP?xzq<7cR!56<<07jkAvgfHiwyX@$@+qr~8ep+3q z%=cbFEnVUzF0PfB!U4}9VnC=BY-=~=&t(y|p3TKozh|0x>_-llviH)$1x6FJh&N6r zxmaoSYe40G9^5e*FXN+U9F_uA<8;BPOof|PAA0*ohH~Arr9kVmYn)}T9(p_}v9Wz^ zvv%MxJXe_Z?Vicl250h9WNLXlf%>34Ag=mGaLaiLnwm2I;9HT2;j7`(Ni#hA6rWO| zikOe{3I%`SxBiU2K8L?Nr}bvFmw@yukAKgfb8R!WxQSg7arkJrR4q<~R!>Ve>HV?g8J05*H)cGb3FaFds1UqROXQcJyFjUTJ+(OuF zoRwO4oq@kPd3>j*cwqd|=P7H6hGrcZVh0Sucg!1}0qmCa`!GPx_GuhDB=ZWF3wUB4 zCPQ?84j?wb0i^f!lMtb5|J62bu_=ih0leq$VKvI)dh^qc45CCE=HeLX(=+%wmeuY!r5GV(C9#CmCX{AIvN(+$_cra?(o-<;(+ z#p?CPJ>FBsJ-w|4VxC-|>$P_^e+vZp>JZ}lEVbxxmRv^b(c{Jz48?)+c%a7LX#8uV zApXZm%ucm`!yZPPd{JB)2L!)X0$nORn?I_C8X9fJB8xbm0D&plAjziSFbcBvB{1n~A$PTOofAEq4+k3(>9|YyBR3N{D5*f+4i2|4 zm^rR@>Yl_=qVwNpy)Ffdm-l>?w))SO0w$l$qm&v!9Bo>rA!f5cy@tE0110f9+i0CZ z1|=Kvi4|snuwrm-UkvP?OHsw9)1f~E#1Z8xL?5GZ-UD`E*G<71Cp9uCssbsQSj+Zd zWGhRs-c?+8PyChpo;h-!Xn%?Tewql7*5)`UfA}U}{|d%gKK9+wR$^1j5dPD4DzO}4 zI?L4`DYY5L%^mi5lb!dI{SntMPFJ^1#862AxM^GtvrguTE^+&}ia)ipA7jUP73rN~ zS*!04q{NrluYrNnoba+dp1!A>7nNQ37ebTH7OlLkGBe#rSZtUFWe3kz#m;isie4^N zlyJ>GG`ZC-=M7&1-$e7-Ahq$J_cX}=5E3414M?_K`sz2-h5EWH!XnuLAK9xcN7$e3 zMmdUL6jY@%?>4yFi4l3(G-D^?1++VfNkc`pB`I#yW@r{fQ!P5O2T6aw; zSQMDSoXmSfyG%$Q`JE$MF zgvBn{>?TJKZ8F$aBKkBC;Z7B6_2*ykX*w+h2%JU-KJ*Aw1i^(7R^9s!{e8eHDq(QJ zNgqy=2uo3ii}z_ecqE-Z4yY8?m%H;Pp?l&Tc(OC|Lyzd4bJH0q)=@#!z4#_qGfcpv z>z;D*Ex0tmpaN9RoU!bU$djJFcO%k*Tvg=US>79V3-YLo(RaBm-}pH6NccMTsP8b@ zPL2j}=V+5`W|joctkQe5&A$=Rx8AkIi_167kUoFeI{%A$F2~J_jq<~ynsmRQmVwT4 zN#lt_9R~a4Iq4nZF=i zzfHs=NvEw*!f!z};22>XbN|)P^qPgSCWSQX$g)sqJ5O=go+fdjQ;lW#lvYQ#nXv354EJGJL4Z}C=x>$erJ)T;FtEZmZ0^Kag#yZ&w$aeA-*+o6v~ zqnPc3d{aY;+B%}Shx^^{=D{#hRXk%Qkj-BJrOF2!WwF^097C-?uRG%E8!mevdQk=6 z-M#`9rM%(kPhS{FJ&<^is(l9Crgd%-I~WESMk0jpVYFq1;{IF{RR?n8_M!{lr!mQI zaW^ulch`OThC0Kn^c?1%)HE+*2@tV0?3d(;RrN_zyYIG|T$O!WZ%&I`O}h7#oLwXC zupm;Q&KNSqTJ<5CZJP__o1T$+*DUhG#GRh)lT!HYetoV`v!GAtOXcyXeS|@0cGm3`l75O|Zs8waBR=hOZd0!!@Uq@lx3_A6uC9)ZfnK-MRMkCG+>V zl6Ae6%8#qR7->?@pOL&VLpoX5w3sG)#%Fu1=0EDm0?Y7xpk8J@o-q*TrbOFcA1=Bz zc~~UOMXNs9b%vbbuyQ=s*7$-)iy6EZ}L@S8mRGAH9)E_b1}&wr`-6^=d# zZXh{XRRy|Tps{7lGg=kBC(mf)nh)TTJH$DuGqt^GuLd7o>OxueHVn@3&%j;yliV%C zN6@3yHoUT!&9UwQ7n;=Ld;3!;2%$ci4Q?);30Uv}~U2bSK^%~5j>m3Lp z7v@<`&F{f#zM2F|7c(VQ%_ZZ2g-@L*B+hntS#qNodGzWJ+gaY+`A3QMXKiv$bfZLC z1NZ;nqJA@Q%LU`m)A2y= zGk7meJG!aJP_(?png8Y*S$*TbTH@_8s5|(L<@nefm(^#?Il`q3SWRGZ>=x)oh0?Lu zUaN{5%+A0Gx+Bp6M5@1scEHyT%=|U>z7PfSf8>MmW~}95WbQtmS(#>8xv(@aF>bCf zh@e!!akYa_mr|Ijf-{@jWpGrU_*LBb@!a}Yzo|!vZ|(`qDCi9-Gy40g56cmAd$*-F zwm?g04VpYxCqdihn@p&e4uhFRh!1j)!0^`2=??bppXqmU#GPh4rF__b_6Rt?s+We^ zxQ4Jkx~Y|$O5_l1q>p!>D}&ae8B#Gc{sMAx7CydS+Dll`xENzu9#3+f%mvzPg~-3Y z5_znEF&ui9C&=5dS&zAKa;8|l2QGJ@D1W|$dcBQnEQ?vA%<=reJMm50&UU5DTY!m* z4`3a$l7hieo{>krMqn%bc2arRl^ZkHtV)UCz`i(ISIcm~M#0*x$G^qx)xuH$b^RvF z+e{8}q!JE=V zpRn0OYhhqaocVpI%3!;{Shs#4H91(fMgDp1h#k*>Epz#-%3~6&BV3*NuMNxE8`Bcj z!8bd?^fePrzoX$3Tm04we%LJbq4AzB@t3XHhH1BC%j2J}oAmg?w_m-ul4BW*8WAif z-Tu8jpG^B1gw2vQ=PzaT_7*DFmJ8+2oW6#9@7XL6(_)-PVoc@FwN`uG9=<(>r4km| zO(QHDH}uF3W5gT5h8#X_2kg8iS`M>N9|s@Dkj3-o6v*$=0bxv^-dpHwy*Ykaaj0r% z6y^OW4mfJZy3~nEvYFJ6?zbqQ;If*Xe>qdg+>F1XFR;duL1uBGbL^v33lN5>r>WE>Eeb`C-CFTHB>`N^k+u zZWR5fq7!!g2%jlO%-5<>;~=1cg^z`S!7^5^2#6v}Mr<$4ac%GcEBZklz0W-(d9pgt zr&T{je@-bTC3&wREWr!=U=mffe(#GY=J_9-b(QwB%~+vO(Q+v48d+5OX1_A7)}v6F zkao%2#DCi@!rbQiwkD?T-ks9xHpI!K71o6?C79Ylr-&VXB&@g^I(yK)MOE6rBUtM4 z%;q30UOPV~^1Kmi(2IjNkD)FE7)^tNh59Y^bz|TDw?&;ZT&m00^%_5kSq3k1pMCT+ z`w672#TFh>Fn?n0WH?u+Ek4j#%o_#lu;8mafJMUjAq2zkY1QjOnO-ts?6AN5hG;x1|WoKud<+DlQ&6x84$VGTPyDFY3vIMeIU3k8>acSu1x1njzB&Te43K6FZM!tRU**ZeS1sDaU&O{{R)< z@R)#6G5K{}1Lk1>g^-wrD<7QtK><&|kiAbsqG8bR<-b_UEi51%fu9J0!Vxg~cYe$x zrVIOTZ1DHK9UN)IbZ!5|OaIMPgYn8Rty$o2P2T^44>K)ZKsplm|6n-N;>Gix#WTSg z@C7Cq25ICa2mBjb9X0^eU}Xus&>(62$voaN4yLngw#4#(E#{02Ud+Y64s_Ma1GK%8 W+=RWTE7KqcfOE#?MkNOJk^c`CVQy>y literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/icons/android/values/ic_launcher_background.xml b/packages/mobile-app/src-tauri/icons/android/values/ic_launcher_background.xml new file mode 100644 index 000000000..16cdd5aab --- /dev/null +++ b/packages/mobile-app/src-tauri/icons/android/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/packages/mobile-app/src-tauri/icons/icon.icns b/packages/mobile-app/src-tauri/icons/icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..a67159d286132c1d273245f4998e43ce3d6a3388 GIT binary patch literal 29548 zcmeHv30PCt_U}#z5XDNcTD6La;M7_Lt93$6uqfJU@ltQKbpWf@T5WM^!2!+z6u|+w zYFlg7&_S)ORn%*7Bv=Q;2GLpv90-atV3;IehW*ys=OhHLx25<0df)rLKk8@BUVH7e z*ZQqt?|n{AW={QRJ|XjG&z#z;4-ln^OgN{QSJKB}UA zdkjniM+OCsjk;2Ly;sWLmt~&35qa#mEVh5EWl2qz9{>3T{`8vNKXrU_{5v(SvlBmA zu_eh>8~ba*$%3!C&KL#j zx_!0|6Hi}xluO9po7Z{~9_E!1qJj?zA#y@Aa0NOk$RBn6N%p!)8YI1ry-ug6(f(LZ z7*!{tLF{#4C`gR#Kfy-ORDd!(t`%2rU@#(zl6MfFOT~DoCDq*(}u*6n=&%(HxvClP{9+j=dy_lB<1cB z$#!bdMsIA^ldE{6XLQw1?k?pfLR=;3=c~Gdq5Qaemeai?5C9|ctu&3^(OtJZzL#WH z%ZX|Tb&*0?fV7)7PbpD7q=3`SO6Y>fk&;BS4lh>DsJ(Z?^62gV%kl!ND6D?N!k)iW!2iByhh}} zPa_I`g(tzObQ%v2gn!DjcmYY^*jS=VJnpbxz=q-Kq z2sIoqA7Y9qeN#8*(RPtBl}zWUveVK+%SByRCfp!jcb)eB?%5={hG?$Wq(sXSqG)Rg znmSw+t)+ueLC#ktXv+C2M5PS!{URd6!7&0h?%^*dnRRl7L{8p+o+>kJ|2%lX@1xtc zi^>V@Oq`brf+pN@oeqO^@VAxMhRJx6Rnv}>q|Ttv5~Qxum4{ri(w{G>^I}f=Kp>qo z)O>!8Zi}E0nr9rOZAF#z@oM+Ibbz((N{EOyTv6C_MC9Lg*QV=Df&^)P2WZ5#7>kx3 zkrVJ`KimBpi=R{jYOK}~1+(&`x-;ko@lzCGdEH3uQcRUPafnnS=tZ(~t5(Tibt;}|2Op#5y0>m<7xg|DZjJY*92DEQYZZpvU zo6Dz(l8l@!O1YP&Yc(KT%!Rl8i8!Es=Wa@=EFL5K&T%% zZRY`vH`(Vh+O)}kMxRI;k_`Yc71Tp$q|UlhFw%psaMF2(-S$ItTM@v`!a=E}hYYr1 zf>-O|qxKQhM;w{%&uZx~vOsP{Im>E)lQ!r6UgP8W({nS^y zRGK(YiHXX-%SAYtTN5e5EyENB*PzS!a;Ao$8&n{3umBY%A>fVfvTKYEeMp&01>Kg| zOprT4Szo{XfsMP9Gp#)x(6^uU-4-xZH~*b}S*=Ou;;pSroJ6x^z-qs3V7CGRnkrO{ zf-umb z>uH}l6a1x1Z1yQviLN1DFwmjvbSD$qNQOAUV=tZER@LdcM_y;bDx$@x65tV62mnW9 zAa#NPQxUv6jRAhqq;4zu3qO`e3dq2yIgcvO7uc9!%>-Gq(o(J&gj68-rW=G4Y#qHc z2SIz-Qu^?FGB|;9|1QnxB+#~!HpeP4MlnK+ix6+(lXaEL7m3oel3K(JO5>Pn3{@7) zhc=@nyu7SBOdvK54u5RCafM`mvP0s3{4R#^n7-1hI!oThK|W-K7TGoD?o>yz$WSTeC#HjW!$*r#b&=4n~J1Oli94lgpmbm*e;Iom*zdb&BfeaPr6}e z_$}YE9bvW^A>MN-<3_^aFdMAt$nJmBt40@Qlct$r`_2C&IqM@GY!VM?Vb|Q|hP%l*~>p8Vo&Xq@_)o#!CP!6^%P~4eoJpIMdP2|j^!}n!#Gf$(lNGlh?rAL9Qt7GtSWK~w~fYH%#UQwm!!;Jm`>oyI=w zs{>V~etVB@O313(m0`2ZzvW64)L%8?g5lY8ZsFZYiu-%6XR0Qwi?*Jx~iiRQr@ZtR5AG+slaTeL(E*ch!k#5WA=+jQW%8 z8qh?r4)<@QuE|5k@2jW+4heyS29DyPG0@T^qFLlV?K_dbL5qwW_D0aoq2X~e=l6aY zI7jV+d>oTVT(zkr-Kb3t;z}Ao0*7{>Z3C1z+EZ$i^t z5~pv1P51|mrF!=g7>}0;de;(*QGc%_5hvNFpWpw(S%9US{xAOnxAS@v;-3AlxAPz( zAu8GJyovL^dk>fr?6>nmX0@MAvdd>opQ)Ij=+i1>wBO|Ck4A z^LvhPV@XiOiZ?M4Bv>$Hm<$R`D-Fwz|2xD63e;>FwMlAG1}HNp?c5R8ZE+;mJu)V0 zV)G7`8{e*4Ix@dNJ@5Ilmmfy&d%JJXT~@Qv-E;G2M~q63I1*kmdt#=)N7f-fYlQ5m zENevm!B6hD{Kxq2$~-!eds87JGbQ~;D>4fLUJA%C27D#uKZ~MQPBee-_&fRI!{)#2 zN`5S*YeQ_Cme+RTVbRmsJa|CBb}Y|RyU1q@N!?FdbLPCs&+w$MZNZ_(4{mA=1?Iu? z=%pnVDPM3KmbFbMOqU{TV>ij7jr2;2uEt;S_%r(G($<__>*0>dN(vgt?F2R))s)G|?H#!xyfVdywx_xo%AkHq*3DZP(trJR?7RP^ftqrzk|-m;~*`~Ek+j$u{DZYqcPTe)A545y%sztHgQ@K-SngkPa;kC;% z1>)<~RvM%sr@~&-+t?(XO-I{(z=m?SOu`f=DlVndVZfx!{4ai}29aACbeO8ofA=H& zvV7F?A^B_b$7$Q=X13T(i@B02>*Zaxeq`6NEA)hU%pR?aLSoihZtRjK^41VrnMopi zMoL$(cAiKNaS__ZVt;pO66>EO^8FxP$=YU-Kfp!kUo4J0iTX|Cl~YvT3( zy+^cX3W!#;i>;;R6a933YHf69GE&;c7P)id^1kyducbX1rFHCDPuoQ`j{%K4m5G+^9=b{R_0-^(i~-PA~7Ix)Nyl_>_I^PTLr1h8|kOm9eb{ z*JLlEy7L{>Zyo#Xg*I=+6&YX8veM56#K<*4ve!<*^tN@ai>7iBHo|tFy(G_bXQj?NsDUbSc?AY#cA8` zuXue<7&bQPoZmD4Y6gKnj;-O;UcF$|{Qh5;aa>4=iY?>w7cHs+rkTPrK6Q$GV5N2n zkvw~h#Q)8G)jiR*P)F&|I(1N-<~Op`G+Ll?$hRIym`mIoyL#YH)zUfmvuG$#@v$b^(3!OKepYo>^uH`$CfQ#y7f+S2kO2v zCZVRo_X*>J(^F>+m}d!FQnvcWu}iwde`?}<$Di8Qet^w$V(qGq!8=%Mux+>G-S)9J z$Z*xtSlVp=mrXCg`q9}NE4k3GTg>R0byq&LJY>gh z*)Nib$yu%Gx8vU!J2ElXOZnAYnEl&#-dHewX|C73axLE}c*pHd7ZUtk0?UmNU3f2= z*0kw!uN;4;dCrDT)gGGs2z9zdDvg`ldd_=+8wQZg9xeON{fzGunf3k;QNKx!orcro zs9WE()~O`*J>}mIwl--`@e~hyJoD7CAya+jzI7JaW~cjDP^6*5LB` zPx5}(hUl)(c(35<7bmLb82fyc-l8XNs5BVa64+zOVBdvj&p#sUKYRMeA7)N{Gy@(}O&NOL!u78G zY)dp*f8LTg>isn|qHEaReV)WyZ#=fNODz{M{e=l;a`v2MXO})Xw96j1*r=Cv7k?hQ z*z2Mn^ugXEj2=LZ&q$MI z+XhwA0#)j%I~kiONC@hy8*K~gW<^#*l>ZE-w&8amU#fvUgd8aL3% zs?_i9+-D=j z(|v(w7e@0xMcml9dF!`RtlBlThD-ZqE-qcnNym4O{@^s%BP6tZ$fX~nt=eCEy;yMf z;461^2}9Neb&Gmm&-L(rKWR+a+0W>P;UO))D*WmNy6>4yorfNM^>y2&2<-Ry&fU1oT{qf6PA8g9X?09Mu?Vul*5&FJXaZAtn-T7f+_x+yod3SVOD=+MP zuAo)i#9CwSk_%?n1+j%kt+xv*4d2v0h-~&n>$omm_T_N9?tFQ<_xl~>^KNlpX3W{K zKH|Mg>6snh|B`-qC1YCV_Gp(c*PmT5b=$&|bo5mq9N6@#U+=FnYcAYLJ^-XE89ycc z^T3gKzn6hl-2Kv?JQ{W}^Mv>D(GNQene}tY3FY@Tx~sa!2ff<<{llTro85NOu-}G| zmb|K!#j2fNG>N1(J4{2A;i}O?Hh;URwX$b!&GoonbY-$r&t%;gx#+Q_u%x8W^z2s; zTE1nC8~FT5GUpAC`!@<^F7w#y4}?!ygc}cpjR(TU1L5x+G8+$s|I?#mt=K?Z6qIV#5s6mI6WJI_wc0hrO-pGwA*r#Hz_SDwXr=MQEsv(AL4UHHAcv7- z2$EI_q*&4f1i*b%q^%HWaYUL*&&CoLq$LWpcp?Q^aRM#DPD4e;#0$h_!UwcaAbg+- zz==Wxi6RXoA}!V~5&2fYBMhLyEn*M|Gb3#kkuw^K;{{SOaYGVlClXRY@mT5xAK@@! z4V=Z&K>$xAX=&?-iv~t+AQFWJk)xsdP)YQp4WTZRD2-khD#^^`LuHASW@fGlb<3o5 zZD{CTKrb^j)CF45=~h7Z;#IPpAcv8Op-5UKkYYodfPhJ%jJ86c#f3`uQbVlJNEANe zL!}@qPG}_9X{bnIywF+~Tcqju(g@&R)apqN_az;aOydXT8 znVAXNiGYS8QN0eZ!$dKYMw@OrmL^j-<{C)){f7GXYJnM}aUi(pKr^jE3TPft0LwLlS5w5>oY~1kk&u*BfBO8ojK<2m*M$ zq@-k>-sLik+@P1_T}I?)vnNzCIc@yYh(IF(|7#I|C$zAVOMiIE4MPo_+coTl;rBD= z768)+;tj(M0U^>rMRw~=$J_MjcrJDP_E(n0xNN!j^X2j1A4!Uly%R9{Ui5+(^1(kd z($Agg_Gx)5{}HQ_WZrVw0rx|T0}cg$wDpE6W13CF6RgjWh(}#7rK62Z|8n5gvt^c+^>NPk1 z_KBR$QOyqTd}YHoRJO4sHzO=^<)>%OZ_PiyIj?6#@vlWwn~=4JoV@CSzR>T>U2UwDB|9XnY!*mNWElOA3Z zNZ`%9)5dqV)pRSRtB;wh+q6?;ZK+YwBFaBpUus$99+$I$^gJml*;D3 z0AIPblGz$E?&h(_x@_}OMb@V^{?zQ+N;ao+%zFA^AoNVPM7T)zD|X%FK9CmD3>*uAncxv!{CY826zq>6?iuhdZP- z4ZK9h-dw#yzFl*+T)ib~>a4guim}#^9tHQlFY}l0&)h;}``{7x+Ksn7M^68>=)=4(*VC1M>#tPwZFTY^UNd_mP4Xs3@5x2?)Xdp({B*T2;78cB7`plHCWQ0WUrf~@!g=t zcY_+=4QhNhsPWyP#&?5+@8CDS8`Su2P~*Enjqe5#FZOyDzPJMKM%iD~X?!;b--T&> zH>mO5AYJ3TL5=SQ;c362XYar6-Jq$x{0Zri`>%Hi;eG;sw-0}oh4bzXU%czFO1Mi{ za!vh<_b1o0%Oi(wPkg53@wiPlH#Una+)&f3Q``>+&Fz;oZT|3qi{e4y>^&FdZ8r?~ z=%y;@*EPdNe)*f*O~b_80XYK<#$wlb#kskICsxfkW7<@Cpy0v9>s?e z>3W_t%bZ6mOH(?0Y_{E{2j|nN@RBRUBH!er$%iU#a+xu=^K~)_>A&jr>j%xpQzRtn z{ASCYfYy(7t>GPA157?}ugD0VFO@3e*rJftwv>9dY6T)r8Ik#_ zm3EX%$*p$K*i>b+k9zJQb(OG*mzm+`jYvfR=y1_UD+6TY6ntHrY{;ahXCzn>d4U5g z4!>awgD-6BT;Tf*kIdB4TtX_I0fwzc8vQCOy}&`&2Nq^KVXcY5@FCCTB-x#AYdAepq_YFI@PMP zs&VP#tz0$53mBA7T~u}^9xenX;0~uD0+YavO!@RGbwMVF$Z)C!k-%Lik})ASV?yrB zbgjT7aO2urO+hv?*+r%or07hKX3)xUI{Jgst(ulzp^GU>UYSh8Y*h~`X^A|`rhCQA z8I>B)?tVEvp^KCO1T|Kj3BOUBURA@|8|-ZhwBda;dw-$OED^i038R0f31gjmpJ+CY z{T*-`gq6|i0O6U!PvcFVjAyQF8ub)a%=QX@>NSScznP3p4Dl@33re6u%3x5aP{iSs zO&NrBAges@2Xl^}?N&VGs}B#qfX39Km*MAf?AOz1ii4ia?SUWt!D7*#_P3SNz^7jC zrS4ut1MinxifZz3CjQlfCBX={Wh&*`aOPXe7}FiDOP&aVu|DyR_FEf zA(}3ze0!)EDIc4^3BI05%RR!>+3o9ua^p$OUC2xIF4Z_WySY5aaQ?>f3>yceW#| zJf*0f9mwE}1Txe^;(v{VWKIoV{QBGx0_|xsS3<3dE4$%sWR`$)ECB%KHg7ZMrD0*5 ztwS*L1qPNw7*<-5K}HBLDBbm#P3_#1#%|_e10Sg@M=nk-TbRNyHbRZ>!9TX&lTCkHD;w8Xfx6`RxD;Of$ znarg-qhzdcuLnaicgp5K2}>UsBB1nyok726xE&X;3=MbS5@0)uuz6)^r~6=KPUf=J zm+1^BP%0@@u>Q8WmRE7B;{!EL#R{4_pJM$|NulIn_f*_KVYgi9#toUTz1y1(k^ciX zZCC)2!BFmr8zx_^sB2?#i1CHyE5jKJ~@D~DAl2Z2&n zb5n3Hm$W) zCyZxXQ9Cvc(;0MVNIlaX0e*E1W*VejkN;v!yO|YvrCMkAV^&WOLQ^FST`mB8|~CB z~frf%V^u$XIH#gu!l&pgJau(kp8QY!?IuRyn*N z*+m969+Mrr18mmVXPg{|DS#m+1~mo=O92%NY6xMazF`{FDJafgM_?M;6#_J6Bn(Ze zQwabofgeN^G!~N<%t4J2PX?F*x<$C1yy1Wwo#o^W(OC#z7N)$t8|w|*ereE`*v;w< z{9YU*U?pXE3hu!IB*G000^FiEf*6`bLYcOE15_7B+rgG#b#v-2p*eP6I;oC^vjrZj z)gKxTxCpANuuR)=0n3EZ8d+J}rUHYL>4GU>x`>R!QUMw4W_}B-1EQX#0XEST0f;(F z1t0=!u~_DDv}lSwY8`~9nXX}BTxYrvGw2pBogGoJ0{F1f>`g@iM5a($*?|@7jT8zc z$M^qtDxii!7-`WL4uu$8_8G;jwJQW@vM9rDCoHiWqs##|>qab{eBlJOqdOhz3kH>- z3ab#H2m{M5%&v+UvQBe3sp6PR=w@>{qDI2D0_hYMO(84ryCaiaF|n2}sa&_DvZVxm8xNpjLz#COfiOkU6|ilA z85m|x-ONmMz6^k(%X&!Rpb7UoutZ70%|61-4lGf=@Wd2WEnF(&vAzr4?8XJwccGib z3oHLZH;Wg0ccO4pgRRlf4J!w(g`RMnq;OEf1sj)2>`s9n4L|~%{NU^dOA`_p|9>#K zH&rp!nXo^YT$qdL##F~uQqTj+M^ zGiV$kB0$4bXImZcQlV#GFMH4>bMRw-u!gXpV|f8F34dA8VOGhATIz%b1}SDJ1M4WB zz9bARwo^c`;Fs0wLL71!yG3zz-SuWe{(1yEKTa=b$1(=BgPW+R9m^QOQ>Z-Laxim( zBUv}DE}|nbp+raa2R~y1)~POI`Npyp(@*GzTa_i0S@uL2sO89y=?gmxyDb_SsE+)i zsX-!gy1e!R78;pDc@Yaid1l*_raPb#Om`qzXB#NLo4WtL>7w$eYdzE9r+P8O9o<2M z%UX*Ya}3&e;)t7g+@U+gjS!XC4f%;F1>KktiFHA!V#)6aTCv*^v_iL|RMa1x4R^nV zS|ak>38FaIpA(jUxbKF5%Z8uUM1HohcBt;z!iQkcvxGU3qHXY3sEeX)j`6U9 z3f;nJM>iXd38rGmFw={m&Vlk+_Z;~rbk{c>pmHb=zoO{`m0%k~5^GhYMeIh?nQcx; zinihC-!UCak6m?R-Y7T_LX3+s2ip_4!4E0mNEl2{7PDrs%r<+$znNb}Y92$O|92Yj za%b?JHGsH5--ysNY_Z!3dWY)}TPNrlwu0l>ie-nM_^`Oa-HhN1ksyMHj&_P0jM74v zPjQuUFbK{P(G|m3unoXRU)BS^aCt`%2xq>3KcDD(^d$zCv#aguIL)S#BEJ)X*@Mvj z>-4A{dt`w*2jBp94y;821_Lg!EKqUDbp$HoPh`O$f{ESCQzF(vC|#wFZqZY?#)-38 z)o?#qh)eX8v#Fp=eboiz9hQkmI4pa@aK~u4y$L@>%a#qSwR2$wmdMz$!8XHK+#rgf z7P3_V!ZhF-)*}ZLqA}<>419+%{|W^n<{YMFWD`yOZ=-;FoMn^%HdJ6bU068 zTeK@(@T0ND6hEgP6yS*ZP$2ToPy}EQGfM?1o*0l%>lo0o2x`Ov>I6mIfS5KOLLxcl zsL2|nW~uEM#LNAWa@8&temAI#juEid zv&>?t1lu#*p$KV3u=Xe8Vu z=TJ|(#)o0zWV|n!a72h$-WSw90>+W-`S~fN@R7G9M18N~>o9X61SO8Z2i}&^g*BdZ zoMG`*Pak+f3t6kll#s#*=?tz$x3F{t*KH3qVb)yru+DXMN)S8prhDzCtS zB3GzK5ur@5Vb6oZ+GYo+z zMOW!wlkj{`n&2&=#lQyPxFec9|7$i-YBLC;-ZtCtp?RBmvcr0yz~AajjzH|VwU_u+ z-Y~$fRSQ2A3zPAZ;H+O_YBJz%lUA6X@2!^htNAI#VOxf3KdLGgVKa)V!>O zCt!Q&j8ZekL1D8L-p^cLP>DS1Q%|U))wD!VdgD2-vKMh!Da{v@?*EN%;osF@>GSE{YKG&SuYnk+xDU`#>^{1BxXX$L_qG%TloItf(1eZKJ zP@0_UEqp$(q^ux&q%fP^-4_%s$q z!QmDzL?+-Cn1GudpG0m!I6DKer<@Lv!aWU#NVpK0oJF!zAS&SCW*bqaz_V3HmwRey z2l6K9!YugW;ovAsN!X*ich~aU3{4d>p!4VFY@ZORt?5P1&Z_a2#AVIk%4~%xwkh!S z6!@CS2Fpt05PCauv92RLx+aica;ZlgJQ^n|FjoGOTngXF(8RbC(#IAlzf4_7ts7iF zZijRpbapUj#i>xjtw=&NMci@}fzRSK-#8?vtC#>OAq^Fq+63l`dASS>J!C4}t%Thy zObItgBcbNN9{?R@!2kaE5@7v*{T)W|V}sRYrm+DSV-3*!&tUPF3U8RYPNw}C&HsdO z4Z`-vap5%kUnJbg)P`OD8O<9eQHUWYQyX^qKNQY~T;1)`0=*DO!!mr&wuMbUwghdU z)`;1_#9>l#oA2O UL9&+5`>*9^E&l{*_V?)j4|N#=KL7v# literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/icons/icon.ico b/packages/mobile-app/src-tauri/icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..95c90c96c57d0e3eae2c3cbe256ae22094988f5a GIT binary patch literal 8124 zcmcgx3sh5Ay54zUqSPkZ+NdZ*&|xao_$pKd5-s>bg|S?%10`r@bUHqmij{XxfLc&$ z(Y9#ogD~w_ota{6T@{hXfhxX6Ok3|55lSMjhzJP@kc1@X+`V@oK^t#ptaZ#;aZmrm(9aP8))T~TkI+0l4@=>M(vix00-7SEgS-(%fV_UH63 z@)mFVx6*i?*hzJBsVw+^+V0&vIH8bbxJ_LAmuZP1GgF->&R5D5$3UuHy|6AN%aUt3 z!z1`APHnOk{YWzH547}X!g)2@s_wqvXL=*W5Ufni(GMCpz42UMZAG|!No9RdvGyBr z*FJmy?IV?aufD+T{gHg9SWSHyXmnKuY*pVIew47eV?wmyo}xd38tjY$3AVcP)^6f1 zN2^p+x1m08XQF5xm1L2^dOON~M;H2`S$5(zZm&98 zYW#t>(GaIFt&nv@i+Y=-L=aD6h;O!aCADv*`o>Eg;UT6*`Yf*|gB*C#RY&F33pcd5 zuuN9h3haG~9^+DB^MIkP!wpzf?D#2ZRFh-qUkRLx+h%?5kNo zX7d|p2;2VCcDh_`xu>zNI3Q}u7?y5Q!$~xs?Y*b4PNzEV1T@>;S!+TNEB3Bsd2Gn4 zLd!8U(toLO?BV$+9iK+GB<&1qf%~&S(|@1W!Z;l&=YwFCdU!RV;@~5rdjXqgK={y@ z;&c#j+c9cBNP3~id{1=Vt0sxQ6wce7c@E!B7@X#lsY*Jbt0JlOzH)7OrCF`HVYc5% zLr*XQTaS6crT6ZDOV^vrHNExU4k3BFAq?gXhV5*&>S_kzD8*f!^!vINRascQF%@RR zC~X;iAIMve?Zr_4f!CnTkadOX9sOm-%$PI=g0PpiDna)=*}t%`4Yk(WFNK4vA>OVV zXhIn3UM0q*Xo_QMqQrk-;I)8?yaQzUJJ&T$7u(P(61bNesdB5J(%f~t<(#H79X8zA z4(A{zyzmp%t3#M0X6MlH_J9Mv<(2d1UU}(fGC&BQ3vmV{r>FM;!213dW`K=vH~U-k zkp@kHmK4+yq8u71`!~lyB-?%X_Qg}-bwC%szd&RA0U^6r8DUTw5M(g8F+7qCC(AX~JXW=>>=w%aqh9AgaD$btgIf*$!qjmtzyTbx3Zg62oB?@h zly;kpN~qSMcCeeHxIh0AqsWyK3`?7%43(-I>mbaJQSMY^#-vSiry?RhMyf(djB+oEGl(P> zKXGB8k{?TXJrRibMcV-)+ef~^J?5Y8sZ|h@Htz9_OeQsyEvv^2*XfLk0H%e=cT*e1A@mqH**HjA;bY{_>8)xw0ct8g$)@X& zcH;F{A}W#-ftUvLuZ~E(1U6k|Sst*u?eho6-#Py?On@BI9Lxk8nASW1Mg{&tCV(`c zIiMv%a_AqqR7${43M3TuTQ3$S9_FOJOzP z$}wK-m3Inuyp6V(g-C;q^UIR*nNjbwH*Qvt@T$MiL5aPfiK}Xv`$5wB9y3Y|YATnS z>Jr)>Gjd2A>1~q)4}Fkt&VXrKkCi$_XbEvaq+E0I(v@w*o#)E?tFxW1NTzzFq63*G z2#_YdOT{$#T8-0~K|prMjywyMZew&Ca#-b6uGu61m0&wpd<}Ms$qZ{JHI_>ixo4Ns ziS*TJ)`}Z*m7QsZm*@AGu`QB{v##U$?7n+J<8lb(_*(K?yr*axj@eU#@gS(O$pqP^ zo`qN{nIvU{A0mgitMlyJq{}N1UO9sAJ-7BYqSlV$xA)YSn#YEI=Xv;od~GCVbSFJW z>+!+6zOt~iikNn7%i?ek)bE_h z{L;{abLq)Q_Yieba$@Y8zAN&f3-iK8SZ^&gA)#q63)dY&ZrvW`o4H1ntNaw9Ixj2X zd6DCw7d8s8lFX$Qu!FsxJE)w4q5B>3LG*{i7JOIcxm_e03>zS597RuQJ;d^pwyK-b zH8tL*VOx$fX4nu}1^h}bW+r5H%!{ixT1!=F)6rB_Ds(h4+qy}yb}u>)SkRNJAwg!_ zu&`EgxlYnEqW)}CHD6*QhNj9%e>|>Go4jRR$-dWn>u)%aY@3jtiu`kky-0bis7nW& z8myVH|}zCGB~wi9|p8oxWif>9gWlbr0(>*^~8)EBG>ci z?xmMkp;)35PV2EETh_1zrN*AN`?v)f3{M>`4 zzMV68+^^~w!b&LI2(*)wH$GI+P(>=n+4ai|q)#oq^T|SQVeO9!`Km*bs-Mvp34AWW z`tpyr-fjkf`|K~&7uw%KAJEcJUo>2b2zYAYbEEk3R`Pa#eCVI-w^|2K)>=hN+ll3CxfO|~HxL7u~D z(&*{2)JHDY)1=R;rY1SvQ|Nb)-j`Q3yo}6yhYXuYF{5P%-DKz3hTTV6iJmG=cG^8% zi@ExFh)~hZ6#C9W?5>D8Idr1SJ*w8^>vYhhLIc#Ejo3KZEm*u|2EPZxR;Z)g1;Szl zJnHQzZj2+YzlJktWSfX~@iiA^CcASM^8%hfqI$On%XXVgiWOt!x#-^1%#+-+68u8POi;GDsr=MgQKZ9T24`$^{SI z9$n(L%p&GYk@KAsjNeh+fJ+t1@T;F}JZm)~p$T$yd<*@~eKsKFH%4f>d%vhzi0UEf z`944g45Gk&uwYeTCy%oT+3}^J;w$zz^zy*DNNd@=4(qrUsia*x;fl6WDH}&q;@V$$46{s7I`-%ymvo+v(O~Mv>rqk0ExdH_Trr_Q^Y* zGAkd#z;l?S*kn2BMcV3V))Qb`x15ZG$KEt}22hRCO(Oi#69I3;ZWElD^jeRR zx}ZU>jJF%QWb80q6g(oU$>)k_;IXyyy(I>3+9sFhE^c(an-PN|TKJQuYWufY?!|hd zPCMtXM!yG7Y%`9IbCvFc?udC3!kKjKNXpS}+u~whQ^4gB`OXQjMB}|gfGpRb`R?Hb zI`hb>>WGMw-R_TI&ZJkOzT0I|hqu|70FR|ZPB;zu0~I;52O7fBH-lzjk3bwke2wxAzF7#jE$`_d06vb=1^3sgs>p^}hI-Yoq&jxQXURlGhZeIM-;xhf> zzcS^AQ|Wtk&u+Q=`-a!0$@9EGX*c~*G<71+I$aJD+epe=4tIdMq~a;k{bRtv3$%c+ zsS?J@fp$`J#o)shg{IjSwu@G0+L`j^c8$Y+qRy;NqGC_5v-I@6LeesaKX{n#SAS6; zHt7rL;MI5OSyM<*l433V`|on92Z*4nBJlb?`^&4oQ&UOp7B3J~L{!AhF%3 zp1wqFBfn`CfM@)PS>tz?({Z6PMoprTH*3ie8?6o`f>Yqwwe5cEa%P+9t$vmNZtC?0 z|2RnecJ#$(B!YZ7xO_wWF8hscVjLeBLw*BWN!e_<{bX0rg|(l_90CSINQ*<$jT;&+ zTHjRq9sPS4@P{9vk`ignr&YEjYJPzn93;nCE1KxCJv@Q<9G45MJ_pG{Gp!=F;7)~v zR6(PT2$s=owc?RB%qBEy`Z;v-q*3{BkVc& z64z|>J~$weXw9p3-i#V>&=C@FO}gqPg$J2ouJ!pdZc4P7okP?a)o1BQSjn&YSG)*A zL0cIeDO~XDUJ~zeSLcN{eAKW(F{QqG{8w}r@u<&nS4o8c<)wRcoh{~T1&hzHDeU)^ z%zU{cnlhLf&T9&DKQ60=?nkqn<*uRULCk&l`L>~Y|6~^6vTdmTVwUrH#!$No7JjY; z&h9;5{FXl!{kRp)bell_fID4J7`-K#>~&_b)l0%&`fFVdzhCqE z03PxdKYQ*YI0-rT_*`Zwn$Bat0+_Rg?#*YG_TZVA>;xpMW6Wq-I4nO z)YD}*VQHCPwjj)MHJv7J`-<0J`rj8PuFq-JN7Xp!!wY3xNd$Y-qbyIoefOv4ry7T73Bu4MRu3neYfY{_jcFLWD@N^ zaQD7@&i%gceCNJ*OY;_IKM|7{gAjURQBKB-2r)2Z5E}*mMmJm>M@Tz=QAYZ2Hgpbm z&#HKN>*p;uiq1BxOXg15S~}tV<}=R-?R)-p{D&{Svdj~?X8Suk{#F`kE%|C&OV1}W z3xD4{K#dt*&Z+;7zmVBR?wymc2T!D%N4NNq;_phbj)@(Ud!>Pu}c2 zAAPMpaCOoIHA-ngSAXvv+M^dr{<3O#yhaysSm*xiz{lm%YZDj^;-+E=pG|cP-@)7T z!dw+vI%LDfGi^}Kyacob^{LUSi*CFxT_YfLQn5uZaxtA9DjfDbDKwx zM@Kl_2t}$6Ul{!f5EYgT`P*lyzyLYd0=OFA61)B5L!~|wi}YzYf&p7VwmxYRgY+X9 zbn0`#BXACbw);df*tyy}bi@B(@SMqcHpd>YyRs2?+r#5ajYs*w>3wx&+=(>2QR>x2 zWjH<7g%57sJ#<^_u}oj(SM_?|L)Dx7uF*do*XFcAXtYXj+DwklEWJU57(GgnY|~N= zyFau@Zhty5`-_Ut{)|}}i)=bIYB6o}{pXMB*?WBAAi(Rlde?SmnZI=4DnM9Qf_vzaRHZ{7w@pY z|3bF;9EIED88oMT;4W{Hohxx@RY;W*2QXo3R?kXFeUSx4m%q!LHOt1g4 zIFlx~vwOSooWD;lWjVxkdnz`ow{5`jDnd0&zuAfx5`erxFC*m*8U>V{tydJug2Kq! zM4M-q`b1WvV$tssv93<~_lZl!wFA;+-SX*fUaJSwnEl;26WSp~a(eUC0&4P1=T*B` z;Q&Uth%}UCi|HxA2L9GV<)!tCfGy&vr0mAOYYK)KgWz)80*VCqE09eA z7*GH%d_^S-)-UBJ0yz~JYy^K=FQ>sCzdbN!q>4sz#b~=MR%?KAr)2si>kddu$?}^d zhs75Ug#q;OXn>{$*>rB)0Xop>3LpRD5g; zgWS{(IxB9pOPO+2|3@EFk_?~dNU=}0;!MI0fLB0T;1A|76+#J>A!tHEK)9J{auC&o zMbyyXP(TS7w0h)cVG+m(J`va;j#B^|o`qwmt3xp`4?@yCpb93Z*M6OJ5L6J*AlR8D z#)JbfI4s23>g_T}r&k9QAcIb&Fv#t~TBs&OkYyBfg$?&eL54=824wOI5DR4}ao__g zNKovJ;FToe9I!M;wiBE)M{sc18Ujq9Dfu;|Sq(%%swG7t4=cjpVKG$-bjn70CWE54 z)?S?ElWk35cDSH3L+*29l^Au zGPX^U8+s?B>90%wAn~`_R{a3adp1Q#^X=PR$fIui`e%D>v2xZw(P> zrF?2M6J*rq8HgbwMG{S$!3D~&9=8VU;XVHLb?Eypg>eq zAtIy}PIO{<5s^X@n$t-O!?xsH_oWhySjU$=3?lm>5c0eNg0^E21mz$g#bsa;^17!@1$zmk=gNsLgpY!T%*$v< zc@#8cI3i0dGh7K4hah5nmb3$;g<`=FK=(^jFPidH0A#5Ejo_Rnej+MDU1CYEX-PSP zMOTw8f<>p4)wJl803?kh9;R+qHlV@_9)Cq}9u5f7&@yJ)QBBlNYNhIQM~XSrMj_Bk z0fz$g`&ohwD?PbY?g-LzF@{jK>Yp-HHf4H{mop1zo6FGpikZj1_@2y^OAl50@W#U6BB_~gt6|;6hahMs6Bo~_q+rZL%tB?z!28< zq%egPW|I`$vP>xr(n4Se>l+VINKmCcBv0_@OfCy!J;H`5lp!l|l`N0)h5&Jt1t^>t zG*!e4BL?{uO36EgOaF8h$;v>~x(BWcJrYgKEmy}45A$(gsdT7KGS~?|9ktvEKo6gk zNy3pjUY@xJ+@O@KpU-{|4WYoF= z>%OL&Bqx?sivA@_c5U<-Es`EpuTo2aFY?jbCiq;;OJQu|1H5HmKT0MK%{LAA4(3a% zb7vG{x|&R!%$MMuPL`+iX+2`T#8ms>^IQ0ESJPS%J`x+P@YS4fgS|WUg*_nBN7`LY zuUWi4`0xR-n$BgJ4~hOr&JADb$dFi4Z1FZK3HNCC5AG9vy!>cdNiiRaVzYWPE#6pR zWN^~TS#6yUer<|Md{(#6|F-5>)swf?>o}XP<@L4-;a2MD;s!ZwUi%== z!R>?GK!hmChvDDs5%SmLcMR(^NRP->4X!2RmO@_`a9^S49TyDyq+xW}c=UATwx-cT zb&~Yf%}AudDf*mC_Q{85L>w}(HEujmk-C6|SJ2#?3!-=J_>}?2$mbvF?~P4jQF0uk zLd_G j5+w}r0mjktkYWG;5HF+Oy9~6m00000NkvXXu0mjfR5FAl literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/packages/mobile-app/src-tauri/icons/ios/AppIcon-20x20@2x-1.png new file mode 100644 index 0000000000000000000000000000000000000000..e1df2a2bf2d2fb18773f354238e9c91034659508 GIT binary patch literal 501 zcmV5qK#>c~ z7L2>V@ZsxU`a5ubjy#x7obnJwT!5Py?zoE&zA^MC%3xJTQwM?^m*6M@=ObJ0pv?nm`9F*bEH%OvQIJ9zaAioAn151hSl|68PVDFP35nmG_N{Ck8A9`Z~W+tpaPe3X!iAsZ{IFi`(N2=op=3-(UNo@Kf)p10XgTy$JnDj@E zZ9J%4t2fXQz633C;9>nHI`~7clrawv@M^=Ls-I2%z`T>l# zAumOaBfCY7Y>~bKCp?PL(;u=ravTRKz$qBr0+oO77a73QpF#2U!PV^qWeCHPYv8D) rq~}Jn5F8y@7*jmkf|vGW5z5b00000NkvXXu0mjfxZ%-0 literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/icons/ios/AppIcon-20x20@2x.png b/packages/mobile-app/src-tauri/icons/ios/AppIcon-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..e1df2a2bf2d2fb18773f354238e9c91034659508 GIT binary patch literal 501 zcmV5qK#>c~ z7L2>V@ZsxU`a5ubjy#x7obnJwT!5Py?zoE&zA^MC%3xJTQwM?^m*6M@=ObJ0pv?nm`9F*bEH%OvQIJ9zaAioAn151hSl|68PVDFP35nmG_N{Ck8A9`Z~W+tpaPe3X!iAsZ{IFi`(N2=op=3-(UNo@Kf)p10XgTy$JnDj@E zZ9J%4t2fXQz633C;9>nHI`~7clrawv@M^=Ls-I2%z`T>l# zAumOaBfCY7Y>~bKCp?PL(;u=ravTRKz$qBr0+oO77a73QpF#2U!PV^qWeCHPYv8D) rq~}Jn5F8y@7*jmkf|vGW5z5b00000NkvXXu0mjfxZ%-0 literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/icons/ios/AppIcon-20x20@3x.png b/packages/mobile-app/src-tauri/icons/ios/AppIcon-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..d0e2ef09659c0a3b30eea1c99791e1fa3f248090 GIT binary patch literal 665 zcmV;K0%rY*P)O%+i&Kl5o>CPv(QposU7c z`M&W9o4IAJx>EzjhF%?S^&7r2xP~>nEcGztPeLX@04&v3ERl(I)w G=AkLptjE7iQr=91qcOcfz|D z;{-;1;62eH4g63GgBi?V1~Zt!3}!Hc=ePU<6SHZu!SJt0Q4mY zTzv42A#utBFdL+f7$36`Yaz&BWM9DI@xYbOaB+}Yn0kCZW+BHySbTylz4`^k5@IZ7 zA;&`G_&jj+Ge&$8vY3Tb3(?~f7RRJmjFONP#Ms~p7XlUH3LfP6#0Eh679v>=G8d<% z4_^I(r^I-mk8!0$^!UUUTJcc5AoD=x;tgIx!GkM4Nr@g7T)~6OVtnyQjD>i!3@(dt z#V4^A5-c3BS&S_{NwE+F7>Q{kAsY$t3Nj#@foEd>Be32WK^-zRK;>jmEM}F{{GGOu7zoB>a*#DvaYtA&Ky3?*3{kKO{(O|(qz!gs-s=% zMHa{4*qH!5%B`vUvkhfkZcc>Frk$YAwN8Hwj>Tuhdg`Ksx2Nk4=W`cT)oA~Y^zGj5 zINFW&Pw$8MPZ`J8b(Ob1;}=mkB-(arc_ebSKHWA9iH6UjjO-#8q#c#QLnDwrv9H@G zVSy8J+|+YN8J8HlulrecJL)tB(~xL;ZqRbm0mA_crXjubS*)}M-??DHB;+`Kx{U)C zOhS&+rgtmkGSWI46joQC9FUPW3_)OZ^vMNDvNQ674XRI0NU|-UAFQ7~u{zHQsVM8Z z=#aZ)`Q=f#_R=Rt`K8`rgCxI_7=}g1^VUZoePSg)dtM#DQbV$wUeG~w9HUg|-J=9u zKW$jlv{%tPmiME!qq^%S!g4^epGQ+Cul)%1KbeHl^QVf-MX-|pFbJrC3aEezsDKKn dfC?zD{Q32WK^-zRK;>jmEM}F{{GGOu7zoB>a*#DvaYtA&Ky3?*3{kKO{(O|(qz!gs-s=% zMHa{4*qH!5%B`vUvkhfkZcc>Frk$YAwN8Hwj>Tuhdg`Ksx2Nk4=W`cT)oA~Y^zGj5 zINFW&Pw$8MPZ`J8b(Ob1;}=mkB-(arc_ebSKHWA9iH6UjjO-#8q#c#QLnDwrv9H@G zVSy8J+|+YN8J8HlulrecJL)tB(~xL;ZqRbm0mA_crXjubS*)}M-??DHB;+`Kx{U)C zOhS&+rgtmkGSWI46joQC9FUPW3_)OZ^vMNDvNQ674XRI0NU|-UAFQ7~u{zHQsVM8Z z=#aZ)`Q=f#_R=Rt`K8`rgCxI_7=}g1^VUZoePSg)dtM#DQbV$wUeG~w9HUg|-J=9u zKW$jlv{%tPmiME!qq^%S!g4^epGQ+Cul)%1KbeHl^QVf-MX-|pFbJrC3aEezsDKKn dfC?zD{QE?^|2@9WQ{5EtB51WXclkl%7moiKjlQR` z=$XVldwR)v`;Qeh%O7`S?7sJ(tNh2amDV>R%c`T#OS?No&aK&9^7!)Vn%X^aasOk| z^zwh)yY}Gz|MQ>r&)%&*x4_+fvdp%K_>KSg+3laPmrH$nw)fY8)$?NO{(hZoUAU0b zM=W7B!}(Lu_kTwnu+`q+8v1qhK93)jAFH1g{n@$Ect<*CS#f!c<*u3?^X=w8Jsr)o zY)V420!!`L z%I2ea!0aSQB~#AjJzJT8+-)bLxk239d#o3wdAFc`zfhU|?}V3&=Ll53X1=%n$Gbg$-zfB*UcBU0$`?1Y zO-udV+E3lyw6|#Squrmsncs{)k<~AuobP;GHu9}t^3&WC_XRiIQa*O#_67S3DLXm& zXKpvNk_U>kow*&Ms|r-({51DW+}t&04;4R|eRCA>G6%c^mD}H5GtH0vlO@V|En)Hb&K1me%<^lM3FYvk)a4^%*e&?^7D2vIqrmK6B&3aB8;4tzW1MTx;^rPifx_f<)5F~LsJ$< v{`%K4qm-Y&<@jW$hHfUkf<~+aSJ?HUOP}ITxt;$8%ux)Uu6{1-oD!M5qK#>c~ z7L2>V@ZsxU`a5ubjy#x7obnJwT!5Py?zoE&zA^MC%3xJTQwM?^m*6M@=ObJ0pv?nm`9F*bEH%OvQIJ9zaAioAn151hSl|68PVDFP35nmG_N{Ck8A9`Z~W+tpaPe3X!iAsZ{IFi`(N2=op=3-(UNo@Kf)p10XgTy$JnDj@E zZ9J%4t2fXQz633C;9>nHI`~7clrawv@M^=Ls-I2%z`T>l# zAumOaBfCY7Y>~bKCp?PL(;u=ravTRKz$qBr0+oO77a73QpF#2U!PV^qWeCHPYv8D) rq~}Jn5F8y@7*jmkf|vGW5z5b00000NkvXXu0mjfxZ%-0 literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/packages/mobile-app/src-tauri/icons/ios/AppIcon-40x40@2x-1.png new file mode 100644 index 0000000000000000000000000000000000000000..f6585c0fe25787b704e58c70d36cfa4280f7ab91 GIT binary patch literal 857 zcmV-f1E&0mP)dCzu!R=G0EqGCaH{80LZpWX0s(A1GvAcHyz)FDk7bmu} z=e>>n1o8xc(YNndL#pNLu-FM74FK7RdOJO2j5qx7}Xh&p7vS@~xM)8@6(h&p7v zD&s$CL>)3-+4wIjNkkno-mLLov7``nsCZ@hXDbOr9V(vd=U?B5I#j%>`6rF2L&d9; zf0BqgL_C-LlS0%X;!)23aU<#w@tWkHZ$up;Ueo;ZiKs)vqsl*Dh&nVp?)499#L6?$ zbIH%N8mlq!`Y&Dnx$N_-zB(U){q2!@Gqg{9E?w`a;pS>GLp)Ei{!5*IYGM|9KHXQd z&$cpsG<{{sKp%))`Nx6h^e)Ve$d!K_c+TTMPDGCUpHDpUHIXKW6#3^Hk7ZTP6|rglN#L=q z3%ejT$v-JP)+-b0AU4TANj&yzW2z#$=ASg)`OJ-nuRR&8ch!sDvh{yIPX8v4Gu6Fy z?c&Zo{}42u@^yu3))5<`0k9#ID$Q z>Fg-E()2-h!D;+wYtJX1$W{9J4w~(FYqin%uh^dNnv?$Stns@?*8Gb$zNj`-NBaIZ z&h?K=f48PNP0hV-n{N*%-gPO~Kd#IvY&$BY?|*Cc&4z8l!yW4Y1Rwwbhyox00SG`8 j009U<0HOd0z?RiNYXZTo=bp1w00000NkvXXu0mjfly9fP literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/icons/ios/AppIcon-40x40@2x.png b/packages/mobile-app/src-tauri/icons/ios/AppIcon-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f6585c0fe25787b704e58c70d36cfa4280f7ab91 GIT binary patch literal 857 zcmV-f1E&0mP)dCzu!R=G0EqGCaH{80LZpWX0s(A1GvAcHyz)FDk7bmu} z=e>>n1o8xc(YNndL#pNLu-FM74FK7RdOJO2j5qx7}Xh&p7vS@~xM)8@6(h&p7v zD&s$CL>)3-+4wIjNkkno-mLLov7``nsCZ@hXDbOr9V(vd=U?B5I#j%>`6rF2L&d9; zf0BqgL_C-LlS0%X;!)23aU<#w@tWkHZ$up;Ueo;ZiKs)vqsl*Dh&nVp?)499#L6?$ zbIH%N8mlq!`Y&Dnx$N_-zB(U){q2!@Gqg{9E?w`a;pS>GLp)Ei{!5*IYGM|9KHXQd z&$cpsG<{{sKp%))`Nx6h^e)Ve$d!K_c+TTMPDGCUpHDpUHIXKW6#3^Hk7ZTP6|rglN#L=q z3%ejT$v-JP)+-b0AU4TANj&yzW2z#$=ASg)`OJ-nuRR&8ch!sDvh{yIPX8v4Gu6Fy z?c&Zo{}42u@^yu3))5<`0k9#ID$Q z>Fg-E()2-h!D;+wYtJX1$W{9J4w~(FYqin%uh^dNnv?$Stns@?*8Gb$zNj`-NBaIZ z&h?K=f48PNP0hV-n{N*%-gPO~Kd#IvY&$BY?|*Cc&4z8l!yW4Y1Rwwbhyox00SG`8 j009U<0HOd0z?RiNYXZTo=bp1w00000NkvXXu0mjfly9fP literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/icons/ios/AppIcon-40x40@3x.png b/packages/mobile-app/src-tauri/icons/ios/AppIcon-40x40@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..3bc0bb0bbd89cd7a419d550f59019602acc0e342 GIT binary patch literal 1160 zcmYk6do6+15Ya=ly=3^PK08_ngmLehPoW#PF~o z3zOC<{rI{b^oLFA;V8{!A3)@YmJ^odN5$Xg$^JHlfQ>IY)ZTe>a}qR-SlAARiS znK3pBl|<4qR|{~3ij&Lu>G^^u$gp-NoKBHB%mo$-tAN?+e4-oocGm0rzpvMKP&;_Q z_Vmr8{b_^aNWt{pdZUh6$?b+&hOvqC1-%`uicCb&Io zq_)*2$tgOd%OgmERD}wc&g3e!SRS3liEuB9tuPYn@&>9wRsWG0O-DJLI9ch-V|MN= zxTHXX#W>+O0{^}+%eiA@Bo;}8^R<>jjvHDyblpNZ4 zjy$4l`I3m$sbx_=bv|pzV}dM`>u36wA>lBYU&WZlC>~J?-?Gu zzq7jJ<~9pR2d9fz%ky_mSNr`;tV20{&Du-uSm?(6YvaDL|6~^!{H^>(=QqY`)wW+Ud&fhk!z>}KSNp@j MFd=v}J1DOBZ-ur|wg3PC literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/icons/ios/AppIcon-512@2x.png b/packages/mobile-app/src-tauri/icons/ios/AppIcon-512@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..27a4c36541d643d66abd18e1b2ce9754e1ab0b51 GIT binary patch literal 11717 zcmeHN3v67)6`fr#$r2K5^C>^O-q_$*65eeo~MR82B3+5ZvtQ`mMmi4~rJ$L5KzBNv; zRi&*|0EO9i@7%fP+|SH=lY72*S;L%J$IlW%%vtcA`YVJ8&@do^Gw6R-|Ni0yfBMIaw-XDuowMbfMEavcZu)53w(}S6YkDQ~@aS{9_iW!a z&qph*`pH-g{ZC*l%J9eM4kN4uZ^h>hr)E(tIW>!(B@dSIX>c)p&!=JB19ghSgJgU< zM6>P`-Q|c+$M_d1BJfWk%+n!S=@#aC%uGI(kk5 zM67(nyA47-)Sk97yZZdGT%Q~r>i+VrIp=-3{$_b?ID~d(A9~}}&+mQP+O+iD{%F7_ z=HGkCuJySWyM3bVjYr1ct3Bq^=rI8iN)T!1u8roS*^d)v2e@s$a*9|Oy=jH4Y8N6X zMC`aBS$g!*Wa6bXln3&1ZqP5@Ie~sWyw7oGed%L)h>sV(J7?7ivA33d*q)Z7wE^+c z*`&L2lg!NVp_kiAgZOTI%UM(=LeUZ-9{nUI#}D_3y>m$6iL|`)B910$$UoL4*I8{l zHqPwM+uVtBa!XzE^~cE+{g=FBSDE-_f8uB%{6ehA%VCks_(X`79FuEzJ(q5lKJc28 z@5!Mk8K?1evRRfY^=Li}ly=@4lNivXepwQ7m24WU$U}*YH_FSElG27!zZ{SyP%m}Dhrvl6s-$opKA(57`GW0Gt`$u-%#yE~fsq)Com8I@00%@~tIJLM7yKbCjN zMb5rGqcRnYIni(CtaMd`y!&uO{yo|nAPTbiXr|N_qIX}Gf3I|ZkJIl)nQ-Mv^)sc# z_(`)5@RR0xr4xRBG4}xEOo^*>zJzk2%W9xB+_sy-y*o@{^n{5NHY|7`9~2d`d2g`o z)3fI0tvctnjTSYq*ho6q64)UJ^PE2RV4~d4 zhj$~TF{aFG3FT2SuP?%w2TjC`>>LI(fU*a=pMgTTE|+scdC{K=N;>N#A!WGku}nt zmz&4OTdmv2`cADDkrUy31$B(>TXU-#c5RX)hb;=nD+)%fABpVyRF~lwoZ4@DoalV+ zX8GD-@~S`?x5Rk3%gV_U$YNe3ae#vkwVY^x&_Md zjz1UUK;*qRVk6yyML3|~WdaICO#I(i=(57WlzhOXK!xnGQZiq*bG1Ci#|79jUQ1*? zS(&rSBb>(ZD9G$r3S2`-s~)Wo5P}q{a+bEB6$%!V!W1mXl*}AKJ{Svql(^(VfO-PQ zbBGd~#VVW$ub83uS|F*HCZ zBt}YcV5*`1$MY*CAHbA2na$i4A1Q@}<-&bL4=`i7;2CJ&v~%R6K{Dv)p4*MH;4%`k z`xUgwG||?g#jVPHBaVl0sOt_gI_w-BYPLbAW*hKy?t^h7J`i}qHUT^zMO|(TbfykK zilA%Yni&_TCJuWG9cIU5h=}uq$%9h^F=B<2X)sY#G+yENG0H|kXRpt^L< zqML95l&O?PuRwjwM8>1KbQaZ9>6US!>m*|_3V>&O#Hu!pm~+q-bD^*16jkPQ<=3fN zM&+YmX#X!40WNJW7>Favbreihd8~!R{R^a(4c8F`8{9SWAfkO!(#dK05 zNw@?;oYa)URa>X#LBT=2e&GRo>^ekj2E-yjD!4?1rKCpzMN4qtG5BP{$_S9A+Y=2# zH-hDfh7K0!i=pIQaXLFQ$H%V-VhJ(#8xcX<_Hid=K2>i`Ka#z$<>pDbia+X2t;SyqGnIW6S z;L;Nd57kE8i~fS7Qm$}^AV-)i*O=>yPFc@d9#_m-O3Z7StWgQvD{0eIX`x)96!f8* zNbJVV73;Wp#2hyyMR>L{$4NACHN}hI3?fWd{B*%RhoG=J?->2V9ivD21U1VtA5Z1z z@&mz+A{2CNp%s`jkC3JbD_*8PYVaOSV%j;d7nb6^jb7BSVPmU=xLeq?fogqhO?bfi19IAn zSIQ8(POp*5dg=ZbS8K6v>^gQyfy%S#`xbasVxH|nB{9z~K*egLUwGib z&eWI`QPi;OeAJ;RB}>0m?j~KARYply=HsN}x}cn-e>v$;RRIz9d)PpdqC%Oh&3OCr zF5Ercm{h|l1K9*PBamBzp`iKhexMEbK;5>bD$XP0oEXMAl z1Brstft*iqMgbOF4{*J4wW9o#n7%ruBqN)U2H_y|fsIZUE|y4uT!+4fTCbUl_Ejh@ zA_3#P=0Z8o*d{_ZR4M3&$pSWVezlx=VCdR;_*w*Aluv|i#qPtw97)?DOx|?hgzm{3 zm6Z@veWIjDjEY!RNWu^oze!pxHc6|*T;0g&;@NTf`mK72)^{Ihb96ozTz@!sQ^Li= zLDuJkwQ;@lh-&VOT<~x{)FOloW(`T)?;dR*7jecz=QLdtQSb96kR5ByXD zreIK>oU=JX=G<`0e%23(?5EV|_ve8f^pfxOfbs^h)YLP#5PPVHm}i4q5B1Eg3LNiP zF7=|C^9H9G6$|}9lZQZi@&;L&EV&=Mlu=N@((LLqR@jJq#IlI|WanHmNbh|6Ur)y# zu|#5dL+!fbSuNy;zUKks0kxl99^Ubl1!|7SA?^&<`T>%`9L2Y zLNaS)W9Nmrw9Otu;hpJUgzrtsO3FT}4N*=}Ys+p)N=I zdaf)kB|4trgrdqyp!av-nkf>XaXeb}t3ljGmf;wNlLt#^zOm)COXJNPL$uZI3|XbA zEqr4{tkO8fS5<{^G?UJA?uiDPk>L@wJ=A1l zT!!hz>>7fwQDv-MLvC**a3X$lf44|}Qt7)q{K4MG7UhzZl$e1F@9|5M*;09BV#z>h zHQmrs)kZe?M6%i6WM!kb5B#n<7ondQqC%sVgDm`2b z+8UFxs&hzW}oYK63Wz7IR}^K$KDj!u$7+Bzhj#$LAf)d!gt!}TM0U~ z_H5+EY+pm{<)ztm^tGu+EpML{_Mf_!biz4nyPEY%Hu$#>itN0yZ;~h3GppzZY^`h> z${`Puv%Brna>a&H!M6KLsgI7ylydr^*UE>#j%h=(UpYO0RZa4TVFKGE`*!j*c86TFfXG%m10p#7BTeDaCyrISiWDb9b+C_9&3o5QN(OZ`(#g6 zL#N=-J(a%rc#%W%ZV6Q6iXFOXk|!IwikJ5{#G+1a@XnG5y@B>Tu0nF;x&E$go)CSN zUq&f2>^bUt0d7+oH=ix=`hx2W`$MC_uLOuRzqH{7fRFu{K?R+&a2rdr=;f9MT8Xrk z5^3}6lh#(o`5lOFIgLs6O~aU^fmZUGc_1;*)zne*Jmqz?Xf0n*hzLf*U(>2p}%Iba(sFE z0{L$1lIUEzx)xMlLH}XxbTuwQ1U7hb&`*COBeHa+5Z`cGgF9r2_~#57e|iF*+vKxR zj6ya>^!ZG3brqdvX-WD(+Dbi# z{vpz71^n^(L!$M+{dX8Cj#GA*q45Od*c3dU4i^X2_|)9>K%0)|(=Z+ZZJ(bPp4AVM zaSzm~4X5Mz)GVqT@<5&1@ZU6UUp}z9?P(9CiHZ_u1EX-iBuj NF21b(*$W%n{{^?e3DW=o literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/icons/ios/AppIcon-60x60@2x.png b/packages/mobile-app/src-tauri/icons/ios/AppIcon-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..3bc0bb0bbd89cd7a419d550f59019602acc0e342 GIT binary patch literal 1160 zcmYk6do6+15Ya=ly=3^PK08_ngmLehPoW#PF~o z3zOC<{rI{b^oLFA;V8{!A3)@YmJ^odN5$Xg$^JHlfQ>IY)ZTe>a}qR-SlAARiS znK3pBl|<4qR|{~3ij&Lu>G^^u$gp-NoKBHB%mo$-tAN?+e4-oocGm0rzpvMKP&;_Q z_Vmr8{b_^aNWt{pdZUh6$?b+&hOvqC1-%`uicCb&Io zq_)*2$tgOd%OgmERD}wc&g3e!SRS3liEuB9tuPYn@&>9wRsWG0O-DJLI9ch-V|MN= zxTHXX#W>+O0{^}+%eiA@Bo;}8^R<>jjvHDyblpNZ4 zjy$4l`I3m$sbx_=bv|pzV}dM`>u36wA>lBYU&WZlC>~J?-?Gu zzq7jJ<~9pR2d9fz%ky_mSNr`;tV20{&Du-uSm?(6YvaDL|6~^!{H^>(=QqY`)wW+Ud&fhk!z>}KSNp@j MFd=v}J1DOBZ-ur|wg3PC literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/icons/ios/AppIcon-60x60@3x.png b/packages/mobile-app/src-tauri/icons/ios/AppIcon-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..5962c9729e1dc4d9865111e460b90db997463995 GIT binary patch literal 1558 zcma)+X;hMF7=}^Y!!fhWB~NPVBWhyS?I;C0Wek{!b!UboP+>($=v(juT{H9c} zm8P^3QPDEjtPTZbg9C=PD4J~Ag32c{pPf1Vo%!*;*L|Mny`OWQAMafe9TjG1U||4* z!3-n9gLgx0Sw4C?(CznJXA2CbEsO{b+*2Ts3jKDj&RADAt9#_iE_Z;o+Vc+6qA~Nt z&eWiE{XA1(-gw3`p*lhHdYq|Zf5yg*-KODZ9L!7*)-O|Y(0vwmn`M#*zUmtBHk(Pi z!TrsPTUf%nx{9>lCN{1#KU7^qf<>wW3?i@-fLQC5(Ny_ddGS*8fZMd5NL2cTK|M_jnp#89 z%4Gy7`J9ISi$_MIjmRvs>RHr_rh}8A{B*?^6x5bKXXDZMI_WJbs0W39iCh` z-+#2Zg(|)iWyvh9M-h+rzU~iy>Q*AY>!t;IM-K7j_a%I9yd&rr6+cr@X=f=g!QBQE zO+=xi6EkGNG!feQ{#=6}t+k!XOw0aqK1fV&lwv9Y%SV*HpKuvqqLaujek3tC>yUrO z<621(HiO7zi2b*$2l(f_yl7 zmXmPqOwtfhK~5Zqxp(afubJwX7WcU}sDkbx!Qg>dzlb;s+((eV0Zrg2DdZ$2QE@8K z3hPL>mt#0UESW+|nkFi$5|LA{>Gm>=i<54h0kdC)KyRSM&?v3co=imE5Zzviak11E z+Tk2QbN1svuhOJvL>-0+mvEJGkJ=+a6e3n&UfV(QRtn3u(lzl_LkEK$G(bh ziH^riZNW_A!AHM$so!v1i~A+3@^)D-P>WT>9~TCNA!nhb+GxevuS#u1%1X^a3Day{ zJ`W}Bu0E{Fbsm&>57Rm|y!C&6Kc}nCXOnckuD8CKc2c$U)*SkaxMNRj>bnl?`K0pd zuyUh0hIcKop18s-hyDyfE{r_ObFixlrE2B0{2qn}Slw{{RU_04e-6bJBevM4474g^yLFO5wCCXJS!y+!Z~chasZ3gulI1ONjVzyJm? z6uTiU;c~ z&f0c)VcNbn9?hu_pMRc3jDtxvUYUCm$DE`sHk zWoTa$2WAGg`{IrTFs;u(i_5WGCci@ankcXWt2L7iKlw7Rfx5tY>$ijZni#M$*1zdO zHhh07D8GthtNP+V(`giO7MbQ(>)bAFG|+FTt1NM(?9NHWiwd zh~)w^^RMHR-}cy4Xj&qc&-^PDnwE$)YW{V;QlM#RSflc*43+{-OT!wMUsc74rln!k z{r<>>rln!|=2t2-EeR_$zfz!SNmzCAiyKW#!lGRNiiM^nVKvDwE;KC#iz2@`(6kgR zs{D$L=DntWCI3^Gbp1YLY+nCi2mPO_^ZdQ~>R7unx52s(Z#Ft<>ijxi=G@FVR+Sl( z8Jig+X@imj$ZnXOSi{(B{TpT4yH_u|1;p1rfP{7w$nS(;UM+tVcL_m}pPl>QKw_(x}? yL*aQzfB_6(00S5bU;qOczyO8<7{CA~v(5mBO44=+ZkK}q0000rd7I~;pp<)3=L2x`e zxK@TNYB7vL<)PH^Xp0*U+t2`#@*Izf9a2Cl&tiLbyO?E(`Et(hnf-ZQ8DxhXz(rmMBUT8kPNP59L{-e_y(HJVPOIj%ju4^G4ss zDOfMv;^xbS4QGZ;G73C$nho?R`jlHho=)4-cj&a?uukrP7vQy7_^Y)Tl~h@{(;FFIpT7W$cOIx|&@dolV<1Ix3cRj0UEQ&a3lkBFd(+M6~<8BE8tx z4do;A0*jFwt9$h5*A|a79Af~OB(crL<# z{H+vs{)y%km(|ouG~Pm2a~1Pm#e4EsWM)p@!2}CmgzR;%ay&TAmE?GN*)Ath&v(1* z@Byp(IH<_XM0hKa^FZRFp()0j2KYdU9-)s#e9AO0FT1nC8$!!BgnE(u@X=+j?*>?%Z3qr;SC$keVUwMDyRS6L2t8i@X5XJN-M19y3!sVKAUA zGq87Y-Ys%BQ6Fs8PHlnFt<^OKR%--Y%iQt0MhL{hO{mwwC>Tf8U@(jqE9Cy4Y%;}K zZPkN9&xFAlatVCeppqN{V{HhEK-GEC*$>%lB^160=`Rc79aybTS>2H;BzNd;)5lc1 z>+k11!n$rch9dz!}QV9k>l)ges%8-auTjF$MslUkw%lSyboVCbo64wERvCcECpQjOIh zlJtns5O%Vng-L9~-tcQprVg!xjwIrob#I>U6{s%z;BZ=DSmA|Pa!6uSOZ{1l{XLD? zzDCIi{L#IlmPzd3oTHl`>TFoDGvdEd8dd1AE8rUfe=|p1!SwYCj!y(Mo~GGS!_?q0 zk~N*(o#A^5Qf2$ho>=Scyu4z^;|IA-pV~=;uXQ)5jzQw~i3bJEa{Gmb=WjYMFY$IJ zuu~$aXQ<9%vi_{j@fCr|lJ~GR};B6|&w2I#c{GEF~E`X}^VwqXuZIf=JOB zj#<8sn8kTwhi6s=oH;T_Zf_JDMulHnmg>XbgjzlcV10jddLpqXF{N#4Q|M?Hw^gyK zBt03InS=XDHR<=hWA_U#HM^QNnK&kDX(Nd#uci5Q2W%kM?( z)#Jxy`~Ah5{J$cR8*QA1Aq&du^GOgReRo%{ID&~RHPU_Vi!V~ecPlRFwteU;S3ej| zi2EZyS~>Yi_f9r?(_JrV?L68HOQ>0xjIXQz3z9E$=X3IWkvpHG>T`ig`blrCg$3B; Snr6a@3E({O9yRWi%zpq%2AYKc literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/packages/mobile-app/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..211dab6995710a21086bfcee037cd9855585d080 GIT binary patch literal 1659 zcmZvdYfzF|7{>_^mCUrYEYr5!ve7N0Kp@SPT(w9=jI_|av}+=Ec`d~xQ^2;1yIq$p zP*B;L&@{6mGfl*6*B3A`r!2{`Ak?G;Q&VbRw`uoFAI>??ng8>9X3m%2$){4*%|N@L zVKCSX^7^2S&{;aQY*5giF(+&g215msgVt`!>VDf3p0YJ+F876|uYF_1+(;UF0}Z+M zS<;12O?6zW_aH@-({yPoo<(;YJU4NBsLm^I;&!Feu~X3h~Lb84Yk-&alCJiDbju87M+6wF~5=AyBNNd$F~s&^)61E#p5@U zkrel|Szi-JqV3njM*Lrbq`;T{pJ0<{d~1z4zs2?F?;}K{lJL1H>7%*xVTMu7>FBs` zxt_5Dn=`6Oj`{Q&uvcMy8w=klM_UBIs9LNxj-_A8lW9CotPu>w7cDRt?XTvJwRu^e z5RB+VY<(O0RJNh@;Xzki+Koe|LmN08=63!;lqz$-qPZZQYwF6ek3r{pGGCnk^&tC- zft_AM2vpy$9anHxoaAE}lFMShVpZ`0A7z8zX^Qu=A%)|3jfqYBxaVzE>B-$QWYZ{d^f6cB1!T<18ZoUoITC4Hdi zfkiOVlNe%Ec)n#PI^_{N-J&{}VUZC=t=w!=P{~+Qg-UcM!4EfFfJe96fyvzHB0|NS zX(=e(5Xo5`UhNCOacwtMbU4<(R-5%seMGB=4F;o9wfKkP8(T<7FmW6$85v16x1Zi zJR#X)35?_=C-#d7(^73pGlk)x2&}c1&NG{Y+LO2gsHeO!|gdhuf z%xwR~Y7!Rnw(7{M*(oTgoQ;JZ9wss+BcY)UUC+%^=<|=FcnIw-A;=-ryb{?85PEDS5Q8inNC3@e+=U&J zXA+_S;~+C2bhT=4eW>%5Gx|7x3Vp}$z^4duf>N&KJXs@ttiCq>iumBYH@Vo@)~v6u z7O?2o>X2nS-%e%)EXX1GgQD1NKWW8(Z$`?J@`ou}2ewuJc5SDZk7r7vb>R`1 zsO7IN%qu&@*31uMg)U6BD$miHZD_AwUUlK678B&nn{Ex>@Q@Vh!_}B5V2i}6tc$Jj z3mb`*r@+ehXpg}(tse9^#3MWq!eb_9H(lnYiU>b6;i^~ea3=XEMGq4TpLW#klEmng zM={D5JLi`jW@Ej=_{691@@?8JU7!H2sy%ZVQ5IV!~~uJ#^94~6+(p#I8gBao{!fPS&ws}+abR&yX-0*$-dd$x+o=tz_po1OGGXg_a zI2SODScB+6V?FBo8`G_q`4Q5NGe; zIH{}RZhnYuExoG`9y^`3JGxfE z#jD*soO?BV!1mtc>Phy+&(hZWxo3MbiU$L{1BNNND=vYe{7^F4fN$U1we@M%-KJjf zq7mFszX&q!l!Lz|*e`beH~C`cDfNEa+vukp-}ca%K0VGO(5C_;2UCJB2PWqI1G6wC AQ2+n{ literal 0 HcmV?d00001 diff --git a/packages/mobile-app/src-tauri/permissions/autogenerated/connect_server.toml b/packages/mobile-app/src-tauri/permissions/autogenerated/connect_server.toml new file mode 100644 index 000000000..f545cb0bf --- /dev/null +++ b/packages/mobile-app/src-tauri/permissions/autogenerated/connect_server.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-connect-server" +description = "Enables the connect_server command without any pre-configured scope." +commands.allow = ["connect_server"] + +[[permission]] +identifier = "deny-connect-server" +description = "Denies the connect_server command without any pre-configured scope." +commands.deny = ["connect_server"] diff --git a/packages/mobile-app/src-tauri/policy/Cargo.toml b/packages/mobile-app/src-tauri/policy/Cargo.toml new file mode 100644 index 000000000..5984ae885 --- /dev/null +++ b/packages/mobile-app/src-tauri/policy/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "codenomad-mobile-policy" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[dependencies] +url = "2" + +[dev-dependencies] +serde_json = "1" diff --git a/packages/mobile-app/src-tauri/policy/src/lib.rs b/packages/mobile-app/src-tauri/policy/src/lib.rs new file mode 100644 index 000000000..2946d8e6c --- /dev/null +++ b/packages/mobile-app/src-tauri/policy/src/lib.rs @@ -0,0 +1,262 @@ +use url::Url; + +pub fn canonical_endpoint(input: &str) -> Result { + if input.len() < 9 + || !input + .get(..8) + .is_some_and(|prefix| prefix.eq_ignore_ascii_case("https://")) + || !input.bytes().all(|c| (0x21..=0x7e).contains(&c)) + || input.contains(['\\', '?', '#', '@', '%']) + { + return Err("invalid"); + } + let remainder = &input[8..]; + if remainder + .find('/') + .is_some_and(|slash| &remainder[slash..] != "/") + { + return Err("invalid"); + } + let url = Url::parse(input).map_err(|_| "invalid")?; + let host = url.host_str().ok_or("invalid")?; + if url.scheme() != "https" + || host.ends_with('.') + || host == "localhost" + || host.ends_with(".localhost") + || host == "tauri" + || host == "[::1]" + || host == "0.0.0.0" + || host.starts_with("127.") + || !url.username().is_empty() + || url.password().is_some() + || url.path() != "/" + || url.query().is_some() + || url.fragment().is_some() + { + return Err("invalid"); + } + Ok(url) +} + +// Native selection is the only way to change remote authority. A redirect can +// never enlarge it, and a recovery navigation clears it before loading local UI. +pub struct NavigationPolicy { + launcher: Url, + endpoint: Option, + revision: u64, +} + +pub struct PendingConnection { + endpoint: Url, + revision: u64, +} + +pub struct CommittedConnection { + endpoint: Url, + revision: u64, +} + +impl CommittedConnection { + pub fn endpoint(&self) -> &Url { + &self.endpoint + } +} + +impl NavigationPolicy { + pub fn new(launcher: Url) -> Self { + Self { + launcher, + endpoint: None, + revision: 0, + } + } + + pub fn is_launcher(&self, url: &Url) -> bool { + url == &self.launcher + } + + pub fn prepare_connection( + &self, + label: &str, + current: &Url, + input: &str, + ) -> Result { + if label != "main" || !self.is_launcher(current) || self.endpoint.is_some() { + return Err("unauthorized"); + } + Ok(PendingConnection { + endpoint: canonical_endpoint(input)?, + revision: self.revision, + }) + } + + pub fn commit_connection( + &mut self, + label: &str, + current: &Url, + pending: PendingConnection, + ) -> Result { + if label != "main" || !self.is_launcher(current) || pending.revision != self.revision { + return Err("unauthorized"); + } + self.revision = self.revision.wrapping_add(1); + self.endpoint = Some(pending.endpoint.clone()); + Ok(CommittedConnection { + endpoint: pending.endpoint, + revision: self.revision, + }) + } + + // A late native failure must not revoke a newer connection or recovery. + pub fn abort_connection(&mut self, committed: &CommittedConnection) { + if self.revision == committed.revision { + self.disconnect(); + } + } + + pub fn disconnect(&mut self) { + self.revision = self.revision.wrapping_add(1); + self.endpoint = None; + } + + pub fn admit(&mut self, url: &Url) -> bool { + if self.is_launcher(url) { + self.disconnect(); + return true; + } + self.endpoint.as_ref().is_some_and(|endpoint| { + url.scheme() == "https" + && url.username().is_empty() + && url.password().is_none() + && url.origin() == endpoint.origin() + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn shared_url_fixtures() { + let fixtures: serde_json::Value = + serde_json::from_str(include_str!("../../../tests/urls.json")).unwrap(); + for pair in fixtures["valid"].as_array().unwrap() { + let input = pair[0].as_str().unwrap(); + assert_eq!( + canonical_endpoint(input).unwrap().as_str(), + pair[1].as_str().unwrap(), + "{input}" + ); + } + for value in fixtures["invalid"].as_array().unwrap() { + let input = value.as_str().unwrap(); + assert!(canonical_endpoint(input).is_err(), "{input}"); + } + } + + #[test] + fn redirect_fencing_and_native_recovery() { + for launcher in [ + "https://tauri.localhost/index.html", + "tauri://localhost/index.html", + ] { + let mut policy = NavigationPolicy::new(launcher.parse().unwrap()); + let endpoint = canonical_endpoint("https://example.com").unwrap(); + assert!(!policy.admit(&endpoint)); + let current = launcher.parse().unwrap(); + let pending = policy + .prepare_connection("main", ¤t, endpoint.as_str()) + .unwrap(); + policy.commit_connection("main", ¤t, pending).unwrap(); + assert!(policy.admit(&"https://example.com/login?return=%2F#form".parse().unwrap())); + for input in [ + "http://example.com/", + "https://evil.com/", + "https://example.com.evil/", + "https://example.com:8443/", + "https://user@example.com/", + "file:///index.html", + "about:blank", + "javascript:alert(1)", + "https://tauri.localhost/evil.html", + ] { + assert!(!policy.admit(&input.parse().unwrap()), "{input}"); + assert!(!policy.is_launcher(&input.parse().unwrap())); + } + assert!(policy.admit(&launcher.parse().unwrap())); + assert!(!policy.admit(&endpoint)); + assert!(!policy.is_launcher(&endpoint)); + assert!(!policy.is_launcher(&format!("{launcher}?spoof=1").parse().unwrap())); + } + } + + #[test] + fn command_authority_and_late_connection_fences() { + let launcher: Url = "https://tauri.localhost/index.html".parse().unwrap(); + let mut policy = NavigationPolicy::new(launcher.clone()); + for (label, current) in [ + ("other", launcher.as_str()), + ("main", "https://example.com/"), + ("main", "https://tauri.localhost/index.html?spoof=1"), + ("main", "https://tauri.localhost.evil/index.html"), + ] { + assert!(policy + .prepare_connection(label, ¤t.parse().unwrap(), "https://example.com") + .is_err()); + } + let pending = policy + .prepare_connection("main", &launcher, "https://example.com") + .unwrap(); + assert!(policy.admit(&launcher)); // Native return while readiness is pending. + assert!(policy + .commit_connection("main", &launcher, pending) + .is_err()); + let first = policy + .prepare_connection("main", &launcher, "https://example.com") + .unwrap(); + let duplicate = policy + .prepare_connection("main", &launcher, "https://evil.com") + .unwrap(); + let committed = policy.commit_connection("main", &launcher, first).unwrap(); + assert!(policy + .prepare_connection("main", &launcher, "https://evil.com") + .is_err()); + assert!(policy + .commit_connection("main", &launcher, duplicate) + .is_err()); + assert!(policy.admit(committed.endpoint())); + assert!(!policy.admit(&"https://evil.com".parse().unwrap())); + } + + #[test] + fn native_failure_rolls_back_only_its_own_committed_generation() { + let launcher: Url = "https://tauri.localhost/index.html".parse().unwrap(); + let mut policy = NavigationPolicy::new(launcher.clone()); + let pending = policy + .prepare_connection("main", &launcher, "https://one.example") + .unwrap(); + let first = policy + .commit_connection("main", &launcher, pending) + .unwrap(); + policy.abort_connection(&first); + assert!(!policy.admit(first.endpoint())); + let pending = policy + .prepare_connection("main", &launcher, "https://two.example") + .unwrap(); + let second = policy + .commit_connection("main", &launcher, pending) + .unwrap(); + policy.abort_connection(&first); // Late failure of an already revoked transaction. + assert!(policy.admit(second.endpoint())); + assert!(policy.admit(&launcher)); + policy.abort_connection(&second); + let pending = policy + .prepare_connection("main", &launcher, "https://three.example") + .unwrap(); + let third = policy + .commit_connection("main", &launcher, pending) + .unwrap(); + assert!(policy.admit(third.endpoint())); + } +} diff --git a/packages/mobile-app/src-tauri/recovery/Cargo.toml b/packages/mobile-app/src-tauri/recovery/Cargo.toml new file mode 100644 index 000000000..25b95548e --- /dev/null +++ b/packages/mobile-app/src-tauri/recovery/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "tauri-plugin-mobile-recovery" +version = "0.1.0" +edition = "2021" +license = "MIT" +links = "tauri-plugin-mobile-recovery" + +[dependencies] +tauri = { version = "=2.12.1", default-features = false } +serde_json = "1" + +[build-dependencies] +tauri-plugin = { version = "=2.7.1", features = ["build"] } diff --git a/packages/mobile-app/src-tauri/recovery/android/build.gradle.kts b/packages/mobile-app/src-tauri/recovery/android/build.gradle.kts new file mode 100644 index 000000000..6a6ad1968 --- /dev/null +++ b/packages/mobile-app/src-tauri/recovery/android/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "ai.neuralnomads.codenomad.recovery" + compileSdk = 36 + defaultConfig { minSdk = 26 } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { jvmTarget = "1.8" } +} + +dependencies { + implementation(project(":tauri-android")) + implementation("androidx.webkit:webkit:1.12.1") +} diff --git a/packages/mobile-app/src-tauri/recovery/android/src/main/AndroidManifest.xml b/packages/mobile-app/src-tauri/recovery/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..4bfc548c6 --- /dev/null +++ b/packages/mobile-app/src-tauri/recovery/android/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/packages/mobile-app/src-tauri/recovery/android/src/main/java/ai/neuralnomads/codenomad/recovery/ConnectionAuthority.kt b/packages/mobile-app/src-tauri/recovery/android/src/main/java/ai/neuralnomads/codenomad/recovery/ConnectionAuthority.kt new file mode 100644 index 000000000..2b97272df --- /dev/null +++ b/packages/mobile-app/src-tauri/recovery/android/src/main/java/ai/neuralnomads/codenomad/recovery/ConnectionAuthority.kt @@ -0,0 +1,64 @@ +package ai.neuralnomads.codenomad.recovery + +import java.net.URI +import java.util.UUID + +// Pure JVM authority used by the actual WebView adapter and its unit tests. +// Readiness is a read, never selection. Each WebView has a unique identity. +class ConnectionAuthority { + companion object { const val LAUNCHER = "https://tauri.localhost/index.html" } + private val identity = UUID.randomUUID().toString() + private var revision = 0L + private var endpoint: URI? = null + private var pendingDocument: String? = null + private fun token() = "$identity:$revision" + + @Synchronized fun snapshot(): String = token() + @Synchronized fun canConnect(): Boolean = endpoint == null && pendingDocument == null + + @Synchronized fun commit(generation: String, value: String): Boolean { + if (generation != token() || !canConnect()) return false + val uri = try { URI(value) } catch (_: Exception) { return false } + if (uri.scheme != "https" || uri.host == null || uri.rawUserInfo != null || + uri.rawPath != "/" || uri.rawQuery != null || uri.rawFragment != null || + uri.host == "localhost" || uri.host.endsWith(".localhost")) return false + endpoint = uri + revision++ + return true + } + + @Synchronized fun disconnect() { endpoint = null; revision++ } + + @Synchronized fun documentStarted(url: String) { + revision++ + pendingDocument = url + if (url == LAUNCHER) endpoint = null + } + + @Synchronized fun documentFinished(url: String) { + if (pendingDocument == url) pendingDocument = null + } + + @Synchronized fun admits(url: String): Boolean { + if (url == LAUNCHER) return true + val selected = endpoint ?: return false + val uri = try { URI(url) } catch (_: Exception) { return false } + fun port(value: URI) = if (value.port == -1) 443 else value.port + return uri.scheme == "https" && uri.rawUserInfo == null && + uri.host == selected.host && port(uri) == port(selected) + } + + // Deny the INITIAL main-frame POST/PUT/etc before networking. Android does + // not re-run shouldInterceptRequest for redirects; 307/308 preserve POST. + // fetch/XHR API POSTs are subresources, not top-level navigation. + @Synchronized fun allowsRequest(url: String, method: String, mainFrame: Boolean): Boolean = + !mainFrame || (method == "GET" && admits(url)) + + // Admission plus invalidation is atomic against a queued connection, even + // on the background request thread before onPageStarted reaches the UI. + @Synchronized fun interceptRequest(url: String, method: String, mainFrame: Boolean): Boolean { + if (!allowsRequest(url, method, mainFrame)) return false + if (mainFrame) documentStarted(url) + return true + } +} diff --git a/packages/mobile-app/src-tauri/recovery/android/src/main/java/ai/neuralnomads/codenomad/recovery/NavigationFence.kt b/packages/mobile-app/src-tauri/recovery/android/src/main/java/ai/neuralnomads/codenomad/recovery/NavigationFence.kt new file mode 100644 index 000000000..9d323f51d --- /dev/null +++ b/packages/mobile-app/src-tauri/recovery/android/src/main/java/ai/neuralnomads/codenomad/recovery/NavigationFence.kt @@ -0,0 +1,78 @@ +package ai.neuralnomads.codenomad.recovery + +import android.graphics.Bitmap +import android.net.http.SslError +import android.webkit.* +import java.io.ByteArrayInputStream + +// shouldOverrideUrlLoading alone does NOT cover POST navigations on Android. +// Wrap Wry's client to also reject cross-origin main-frame requests and recover +// from an unexpected page start. Do not disable TLS or replace protocol handling. +class NavigationFence( + private val original: WebViewClient, + private val authority: ConnectionAuthority, + private val rendererGone: (WebView) -> Unit +) : WebViewClient() { + companion object { const val LAUNCHER = ConnectionAuthority.LAUNCHER } + var loading = false + private set + + override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { + if (!authority.allowsRequest(request.url.toString(), request.method, request.isForMainFrame) || + !authority.admits(request.url.toString())) return true + if (request.isForMainFrame && request.url.toString() == LAUNCHER) authority.disconnect() + return original.shouldOverrideUrlLoading(view, request) + } + + override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? { + if (!authority.interceptRequest(request.url.toString(), request.method, request.isForMainFrame)) { + return WebResourceResponse("text/plain", "utf-8", 403, "Forbidden", emptyMap(), + ByteArrayInputStream(ByteArray(0))) + } + if (request.isForMainFrame && request.url.toString() == LAUNCHER) authority.disconnect() + return original.shouldInterceptRequest(view, request) + } + + override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { + loading = true + if (!authority.admits(url)) { + authority.disconnect() + view.stopLoading() + view.loadUrl(LAUNCHER) + return + } + authority.documentStarted(url) + original.onPageStarted(view, url, favicon) + } + + override fun onPageFinished(view: WebView, url: String) { + if (authority.admits(url)) { + authority.documentFinished(url) + loading = false + } + original.onPageFinished(view, url) + } + override fun onPageCommitVisible(view: WebView, url: String) = original.onPageCommitVisible(view, url) + override fun doUpdateVisitedHistory(view: WebView, url: String, reload: Boolean) = + original.doUpdateVisitedHistory(view, url, reload) + override fun onReceivedError(view: WebView, request: WebResourceRequest, error: WebResourceError) { + if (!request.isForMainFrame || authority.admits(request.url.toString())) original.onReceivedError(view, request, error) + } + override fun onReceivedHttpError(view: WebView, request: WebResourceRequest, response: WebResourceResponse) = + original.onReceivedHttpError(view, request, response) + override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) { + handler.cancel() + } + override fun onReceivedClientCertRequest(view: WebView, request: ClientCertRequest) { request.cancel() } + override fun onReceivedHttpAuthRequest(view: WebView, handler: HttpAuthHandler, host: String, realm: String) { + handler.cancel() + } + + override fun onRenderProcessGone(view: WebView, detail: RenderProcessGoneDetail): Boolean { + authority.disconnect() + // Do not delegate to Wry's default unhandled callback or reuse a dead + // WebView. The host removes/destroys it and closes the Activity; reopen. + rendererGone(view) + return true + } +} diff --git a/packages/mobile-app/src-tauri/recovery/android/src/main/java/ai/neuralnomads/codenomad/recovery/RecoveryPlugin.kt b/packages/mobile-app/src-tauri/recovery/android/src/main/java/ai/neuralnomads/codenomad/recovery/RecoveryPlugin.kt new file mode 100644 index 000000000..cab911605 --- /dev/null +++ b/packages/mobile-app/src-tauri/recovery/android/src/main/java/ai/neuralnomads/codenomad/recovery/RecoveryPlugin.kt @@ -0,0 +1,142 @@ +package ai.neuralnomads.codenomad.recovery + +import android.app.Activity +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.view.Gravity +import android.view.ViewGroup +import android.webkit.WebSettings +import android.webkit.WebView +import android.widget.Button +import android.widget.FrameLayout +import androidx.webkit.WebViewFeature +import app.tauri.annotation.Command +import app.tauri.annotation.InvokeArg +import app.tauri.annotation.TauriPlugin +import app.tauri.plugin.Invoke +import app.tauri.plugin.JSObject +import app.tauri.plugin.Plugin + +// Native chrome, not DOM and not a second WebView. No frontend permission grants. +// Live-WebView recovery covers TLS/HTTP/JS errors, not renderer death. +@TauriPlugin +class RecoveryPlugin(private val activity: Activity) : Plugin(activity) { + private var ready = false + private var button: Button? = null + private class Binding(val view: WebView, val authority: ConnectionAuthority, val fence: NavigationFence) + private var binding: Binding? = null + + private fun currentLauncher(): Binding? { + val current = binding ?: return null + if (!ready || activity.isFinishing || activity.isDestroyed || + button?.isAttachedToWindow != true || !current.view.isAttachedToWindow || + current.view.webViewClient !== current.fence || current.fence.loading || + !current.authority.canConnect() || + current.view.url != NavigationFence.LAUNCHER) return null + return current + } + + // Called only by Rust, before a connection. No frontend command permission. + @Command + fun readiness(invoke: Invoke) { + activity.runOnUiThread { + val current = currentLauncher() + // SIDE-EFFECT FREE: no client installation or origin selection here. + val result = JSObject().put("ready", current != null) + if (current != null) result.put("generation", current.authority.snapshot()) + invoke.resolve(result) + } + } + + // Rust calls this only after its own generation commit. Native authority + // commit + loadUrl are serialized together with return/replacement on UI. + @Command + fun connect(invoke: Invoke) { + val args = invoke.parseArgs(ConnectionArgs::class.java) + activity.runOnUiThread { + val current = currentLauncher() + if (current == null || !current.authority.commit(args.generation, args.endpoint)) { + invoke.resolve(JSObject().put("connected", false)) + return@runOnUiThread + } + try { + current.view.loadUrl(args.endpoint) + invoke.resolve(JSObject().put("connected", true)) + } catch (_: Exception) { + current.authority.disconnect() + invoke.resolve(JSObject().put("connected", false)) + } + } + } + + override fun load(webView: WebView) { + // Wry uses HTTPS-intercepted tauri.localhost / ipc.localhost, not an + // HTTP server. Set this synchronously before any hosted navigation. + webView.settings.mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW + // post (rather than inline runOnUiThread) waits for Wry's creation/client + // setup to finish. A later client replacement makes readiness fail closed. + webView.post { + binding?.authority?.disconnect() + val authority = ConnectionAuthority() + val fence = NavigationFence(webView.webViewClient, authority) { dead -> + if (binding?.view === dead) { + ready = false + binding = null + } + (dead.parent as? ViewGroup)?.removeView(dead) + dead.destroy() + // Never stop/loadUrl/reuse this WebView after renderer death. + activity.finish() + } + webView.webViewClient = fence + binding = Binding(webView, authority, fence) + webView.settings.javaScriptCanOpenWindowsAutomatically = false + webView.settings.setSupportMultipleWindows(false) + ready = WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT) + button?.bringToFront() + } + activity.runOnUiThread { + if (button != null) return@runOnUiThread + val root = activity.findViewById(android.R.id.content) + val density = activity.resources.displayMetrics.density + val button = Button(activity).apply { + text = "↩ CodeNomad" + contentDescription = "↩ CodeNomad" + setTextColor(Color.WHITE) + background = ColorDrawable(Color.rgb(0, 82, 204)) + minHeight = (48 * density).toInt() + setOnClickListener { + val current = binding ?: return@setOnClickListener + current.authority.disconnect() + current.view.stopLoading() + // Never use history or a value supplied by remote content. + current.view.loadUrl(NavigationFence.LAUNCHER) + } + } + val params = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, (48 * density).toInt(), + Gravity.BOTTOM or Gravity.END + ).apply { + rightMargin = (12 * density).toInt() + bottomMargin = (24 * density).toInt() + } + root.addView(button, params) + this.button = button + button.setOnApplyWindowInsetsListener { view, insets -> + val layout = view.layoutParams as FrameLayout.LayoutParams + @Suppress("DEPRECATION") + val bottom = insets.systemWindowInsetBottom + layout.bottomMargin = bottom + (12 * density).toInt() + view.layoutParams = layout + insets + } + button.requestApplyInsets() + } + } +} + +@InvokeArg +class ConnectionArgs { + lateinit var endpoint: String + lateinit var generation: String +} diff --git a/packages/mobile-app/src-tauri/recovery/android/src/main/res/xml/codenomad_mobile_network_security.xml b/packages/mobile-app/src-tauri/recovery/android/src/main/res/xml/codenomad_mobile_network_security.xml new file mode 100644 index 000000000..cf23a2063 --- /dev/null +++ b/packages/mobile-app/src-tauri/recovery/android/src/main/res/xml/codenomad_mobile_network_security.xml @@ -0,0 +1,17 @@ + + + + + + + + + + localhost + ip6-localhost + 127.0.0.1 + ::1 + + diff --git a/packages/mobile-app/src-tauri/recovery/build.rs b/packages/mobile-app/src-tauri/recovery/build.rs new file mode 100644 index 000000000..4a42ff596 --- /dev/null +++ b/packages/mobile-app/src-tauri/recovery/build.rs @@ -0,0 +1,7 @@ +fn main() { + // No frontend commands and no frontend permissions, local or remote. + tauri_plugin::Builder::new(&[]) + .android_path("android") + .ios_path("ios") + .build(); +} diff --git a/packages/mobile-app/src-tauri/recovery/ios/Package.swift b/packages/mobile-app/src-tauri/recovery/ios/Package.swift new file mode 100644 index 000000000..7dabfca24 --- /dev/null +++ b/packages/mobile-app/src-tauri/recovery/ios/Package.swift @@ -0,0 +1,12 @@ +// swift-tools-version:5.3 +import PackageDescription + +let package = Package( + name: "tauri-plugin-mobile-recovery", + platforms: [.iOS("15.0")], + products: [.library(name: "tauri-plugin-mobile-recovery", type: .static, + targets: ["tauri-plugin-mobile-recovery"])], + dependencies: [.package(name: "Tauri", path: "../.tauri/tauri-api")], + targets: [.target(name: "tauri-plugin-mobile-recovery", + dependencies: [.byName(name: "Tauri")], path: "Sources")] +) diff --git a/packages/mobile-app/src-tauri/recovery/ios/Sources/RecoveryPlugin.swift b/packages/mobile-app/src-tauri/recovery/ios/Sources/RecoveryPlugin.swift new file mode 100644 index 000000000..95b88a88b --- /dev/null +++ b/packages/mobile-app/src-tauri/recovery/ios/Sources/RecoveryPlugin.swift @@ -0,0 +1,94 @@ +import Tauri +import UIKit +import WebKit + +// Native UIKit control outside WKWebView; no frontend permission grants. +class RecoveryPlugin: Plugin { + private weak var webview: WKWebView? + private var button: UIButton? + private var generation = UUID().uuidString + private var urlObservation: NSKeyValueObservation? + private var loadingObservation: NSKeyValueObservation? + + private func launcherReady() -> Bool { + // Every caller runs on the main queue, including Tauri background IPC. + guard let button = button, let webview = webview else { return false } + return button.window != nil && webview.window === button.window && + !webview.isLoading && webview.url?.absoluteString == "tauri://localhost/index.html" + } + + // Rust-only readiness query. WKUserScript supports document-start injection. + @objc public func readiness(_ invoke: Invoke) { + DispatchQueue.main.async { [weak self] in + guard let self = self else { invoke.resolve(["ready": false]); return } + invoke.resolve(["ready": self.launcherReady(), "generation": self.generation]) + } + } + + @objc public func connect(_ invoke: Invoke) throws { + let args = try invoke.parseArgs(ConnectionArgs.self) + DispatchQueue.main.async { [weak self] in + guard let self = self, self.launcherReady(), args.generation == self.generation, + let url = URL(string: args.endpoint), url.scheme == "https", + url.user == nil, url.password == nil else { + invoke.resolve(["connected": false]); return + } + self.generation = UUID().uuidString + self.webview?.load(URLRequest(url: url)) + invoke.resolve(["connected": true]) + } + } + + override func load(webview: WKWebView) { + DispatchQueue.main.async { [weak self, weak webview] in + guard let self = self, let webview = webview, + let root = webview.superview else { return } + self.generation = UUID().uuidString + self.webview = webview + self.urlObservation = webview.observe(\.url, options: [.new]) { [weak self] _, _ in + self?.documentChanged() + } + self.loadingObservation = webview.observe(\.isLoading, options: [.new]) { [weak self] _, change in + if change.newValue == true { self?.documentChanged() } + } + self.button?.removeFromSuperview() + let control = UIButton(type: .system) + control.setTitle("↩ CodeNomad", for: .normal) + control.accessibilityLabel = "↩ CodeNomad" + control.setTitleColor(.white, for: .normal) + control.backgroundColor = UIColor(red: 0, green: 82 / 255, blue: 204 / 255, alpha: 1) + control.contentEdgeInsets = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12) + control.translatesAutoresizingMaskIntoConstraints = false + control.addTarget(self, action: #selector(self.recover), for: .touchUpInside) + root.addSubview(control) + NSLayoutConstraint.activate([ + control.trailingAnchor.constraint(equalTo: root.safeAreaLayoutGuide.trailingAnchor, constant: -12), + control.bottomAnchor.constraint(equalTo: root.safeAreaLayoutGuide.bottomAnchor, constant: -12), + control.heightAnchor.constraint(greaterThanOrEqualToConstant: 48) + ]) + self.button = control + } + } + + @objc private func recover() { + generation = UUID().uuidString + webview?.stopLoading() + // The navigation admission policy revokes remote authority on return. + webview?.load(URLRequest(url: URL(string: "tauri://localhost/index.html")!)) + } + + private func documentChanged() { + if Thread.isMainThread { generation = UUID().uuidString } + else { DispatchQueue.main.async { [weak self] in self?.generation = UUID().uuidString } } + } +} + +private struct ConnectionArgs: Decodable { + let endpoint: String + let generation: String +} + +@_cdecl("init_plugin_mobile_recovery") +func initPlugin() -> Plugin { + RecoveryPlugin() +} diff --git a/packages/mobile-app/src-tauri/recovery/src/lib.rs b/packages/mobile-app/src-tauri/recovery/src/lib.rs new file mode 100644 index 000000000..991a192fc --- /dev/null +++ b/packages/mobile-app/src-tauri/recovery/src/lib.rs @@ -0,0 +1,86 @@ +use tauri::{ + plugin::{Builder, TauriPlugin}, + Runtime, +}; + +#[cfg(mobile)] +struct Recovery(tauri::plugin::PluginHandle); + +// Side-effect-free snapshot. Tokens are opaque platform document generations. +pub fn readiness(app: &tauri::AppHandle) -> Result { + #[cfg(mobile)] + { + use tauri::Manager; + let result: serde_json::Value = app + .state::>() + .0 + .run_mobile_plugin("readiness", ()) + .map_err(|_| "unsupported")?; + if result.get("ready").and_then(|value| value.as_bool()) != Some(true) { + return Err("unsupported"); + } + result + .get("generation") + .and_then(|value| value.as_str()) + .map(str::to_owned) + .ok_or("unsupported") + } + #[cfg(desktop)] + { + // Desktop compilation is a validation fixture, not a supported client. + // Do not permit a hosted page without independent native recovery. + let _ = app; + Err("unsupported") + } +} + +// Only Rust invokes this; there are still no frontend plugin permissions. +pub fn connect( + app: &tauri::AppHandle, + generation: &str, + endpoint: &str, +) -> Result<(), &'static str> { + #[cfg(mobile)] + { + use tauri::Manager; + let result: serde_json::Value = app + .state::>() + .0 + .run_mobile_plugin( + "connect", + serde_json::json!({ "generation": generation, "endpoint": endpoint }), + ) + .map_err(|_| "unavailable")?; + if result.get("connected").and_then(|value| value.as_bool()) == Some(true) { + Ok(()) + } else { + Err("unavailable") + } + } + #[cfg(desktop)] + { + let _ = (app, generation, endpoint); + Err("unsupported") + } +} + +#[cfg(target_os = "ios")] +tauri::ios_plugin_binding!(init_plugin_mobile_recovery); + +pub fn init() -> TauriPlugin { + Builder::new("mobile-recovery") + .setup(|_app, _api| { + #[cfg(target_os = "android")] + let handle = _api + .register_android_plugin("ai.neuralnomads.codenomad.recovery", "RecoveryPlugin")?; + #[cfg(target_os = "ios")] + let handle = _api.register_ios_plugin(init_plugin_mobile_recovery)?; + #[cfg(mobile)] + { + use tauri::Manager; + _app.manage(Recovery(handle)); + } + Ok(()) + }) + .build() +} diff --git a/packages/mobile-app/src-tauri/src/lib.rs b/packages/mobile-app/src-tauri/src/lib.rs new file mode 100644 index 000000000..996f989a4 --- /dev/null +++ b/packages/mobile-app/src-tauri/src/lib.rs @@ -0,0 +1,85 @@ +use codenomad_mobile_policy::NavigationPolicy; +use std::sync::{Arc, Mutex}; +use tauri::{Manager, WebviewUrl, WebviewWindow, WebviewWindowBuilder}; + +// The hosted application reads these during module initialization. Do not rely +// on UA detection: Tauri remains the container but never the hosted UI's host. +const HOSTED_CONTEXT: &str = r#" +Object.defineProperty(window, '__CODENOMAD_RUNTIME_HOST__', { value: 'web', writable: false, configurable: false }); +Object.defineProperty(window, '__CODENOMAD_WINDOW_CONTEXT__', { value: 'remote', writable: false, configurable: false }); +"#; + +type Policy = Arc>; + +#[tauri::command] +async fn connect_server( + webview: WebviewWindow, + policy: tauri::State<'_, Policy>, + endpoint: String, +) -> Result<(), String> { + let current = webview.url().map_err(|_| "unavailable")?; + let pending = policy + .lock() + .map_err(|_| "unavailable")? + .prepare_connection(webview.label(), ¤t, &endpoint)?; + // Runs off the UI thread: mobile readiness calls dispatch to the main thread. + // Do not connect unless native recovery is attached and document-start + // injection is available (Android's onPageStarted fallback is too late). + let native_generation = + tauri_plugin_mobile_recovery::readiness(webview.app_handle()).map_err(|_| "unsupported")?; + let current = webview.url().map_err(|_| "unavailable")?; + let committed = { + let mut guard = policy.lock().map_err(|_| "unavailable")?; + guard.commit_connection(webview.label(), ¤t, pending)? + }; + // Top-level navigation preserves the hosted site's same-origin cookies, + // SDK, SSE, and authentication. There is deliberately no iframe or proxy. + // Selection and load are one native UI-thread transaction. Readiness never + // selects an endpoint. Native return/document replacement invalidates the + // snapshot even before Rust receives the corresponding navigation callback. + if tauri_plugin_mobile_recovery::connect( + webview.app_handle(), + &native_generation, + committed.endpoint().as_str(), + ) + .is_err() + { + if let Ok(mut guard) = policy.lock() { + guard.abort_connection(&committed); + } + return Err("unavailable".into()); + } + Ok(()) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + let launcher = if cfg!(any(target_os = "android", target_os = "windows")) { + "https://tauri.localhost/index.html" + } else { + "tauri://localhost/index.html" + }; + let policy = Arc::new(Mutex::new(NavigationPolicy::new(launcher.parse().unwrap()))); + tauri::Builder::default() + .manage(Arc::clone(&policy)) + .plugin(tauri_plugin_mobile_recovery::init()) + .invoke_handler(tauri::generate_handler![connect_server]) + .setup(move |app| { + let navigation = Arc::clone(&policy); + WebviewWindowBuilder::new(app, "main", WebviewUrl::App("index.html".into())) + .title("CodeNomad Mobile") + .use_https_scheme(true) + .initialization_script(HOSTED_CONTEXT) + .on_navigation(move |url| { + navigation + .lock() + .map(|mut guard| guard.admit(url)) + .unwrap_or(false) + }) + .on_new_window(|_, _| tauri::webview::NewWindowResponse::Deny) + .build()?; + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("mobile application startup"); +} diff --git a/packages/mobile-app/src-tauri/src/main.rs b/packages/mobile-app/src-tauri/src/main.rs new file mode 100644 index 000000000..37609d866 --- /dev/null +++ b/packages/mobile-app/src-tauri/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + codenomad_mobile_lib::run(); +} diff --git a/packages/mobile-app/src-tauri/tauri.conf.json b/packages/mobile-app/src-tauri/tauri.conf.json new file mode 100644 index 000000000..f2bd035f9 --- /dev/null +++ b/packages/mobile-app/src-tauri/tauri.conf.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "CodeNomad Mobile", + "version": "0.1.0", + "identifier": "ai.neuralnomads.codenomad.mobile", + "build": { + "beforeBuildCommand": "npm run build", + "frontendDist": "../dist" + }, + "app": { + "withGlobalTauri": false, + "windows": [], + "security": { + "capabilities": ["launcher"], + "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src ipc: http://ipc.localhost https://ipc.localhost; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'" + } + }, + "bundle": { + "active": true, + "targets": [], + "android": { "minSdkVersion": 26 }, + "iOS": { "minimumSystemVersion": "15.0" } + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1-codenomad.patch b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1-codenomad.patch new file mode 100644 index 000000000..16a15e08c --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1-codenomad.patch @@ -0,0 +1,112 @@ +--- a/build.rs ++++ b/build.rs +@@ -4,6 +4,8 @@ + + use heck::AsShoutySnakeCase; + use tauri_utils::write_if_changed; ++ ++mod codenomad_ios_abi_guard; + + use std::{ + collections::BTreeMap, +@@ -248,6 +250,9 @@ + } + + fn main() { ++ if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("ios") { ++ codenomad_ios_abi_guard::verify(); ++ } + let mut checked_features = Vec::new(); + let custom_protocol = has_feature(&mut checked_features, "custom-protocol"); + let dev = !custom_protocol; +--- a/Cargo.toml ++++ b/Cargo.toml +@@ -291,6 +291,9 @@ + [build-dependencies.heck] + version = "0.5" + ++[build-dependencies.sha2] ++version = "=0.10.9" ++ + [build-dependencies.tauri-build] + version = "2.7.1" + default-features = false +--- a/mobile/ios-api/Package.swift ++++ b/mobile/ios-api/Package.swift +@@ -20,7 +20,7 @@ + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. +- .package(name: "SwiftRs", url: "https://github.com/Brendonovich/swift-rs", from: "1.0.0") ++ .package(name: "SwiftRs", url: "https://github.com/Brendonovich/swift-rs", .revision("f64a4514de07f450ec5b6aa297624cd3479d9579")) + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. +--- a/mobile/ios-api/Sources/Tauri/Tauri.swift ++++ b/mobile/ios-api/Sources/Tauri/Tauri.swift +@@ -135,11 +135,11 @@ + + @_cdecl("run_plugin_command") + func runCommand( +- id: Int, ++ id: Int32, + name: SRString, + command: SRString, + data: SRString, +- callback: @escaping @convention(c) (Int, Bool, UnsafePointer) -> Void, ++ callback: @escaping @convention(c) (Int32, Int32, UnsafePointer) -> Void, + sendChannelData: @escaping @convention(c) (UInt64, UnsafePointer) -> Void + ) { + let callbackId: UInt64 = 0 +@@ -148,7 +148,7 @@ + command: command.toString(), callback: callbackId, error: errorId, + sendResponse: { (fn: UInt64, payload: String?) -> Void in + let success = fn == callbackId +- callback(id, success, payload ?? "null") ++ callback(id, success ? 1 : 0, payload ?? "null") + }, + sendChannelData: { (id: UInt64, payload: String) -> Void in + sendChannelData(id, payload) +--- /dev/null ++++ b/codenomad_ios_abi_guard.rs +@@ -0,0 +1,24 @@ ++// CodeNomad local integration guard; upstream Tauri is MIT OR Apache-2.0. ++// Runs before the upstream iOS Swift library linker, including direct Cargo builds. ++use sha2::{Digest, Sha256}; ++use std::{env, fs, path::Path}; ++ ++pub fn verify() { ++ let root = env::var("CARGO_MANIFEST_DIR").expect("Tauri manifest directory"); ++ let version = env::var("CARGO_PKG_VERSION").expect("Tauri version"); ++ verify_at(Path::new(&root), &version); ++} ++ ++pub fn verify_at(root: &Path, version: &str) { ++ assert_eq!(version, "2.12.1", "CodeNomad iOS ABI patch version drift"); ++ for line in include_str!("codenomad-ios-abi.sha256").lines() { ++ let (expected, relative) = line.split_once(" ").expect("ABI hash receipt"); ++ let path = root.join(relative); ++ println!("cargo:rerun-if-changed={}", path.display()); ++ let bytes = fs::read(&path).expect("CodeNomad iOS ABI input missing"); ++ let actual = format!("{:x}", Sha256::digest(&bytes)); ++ assert_eq!(actual, expected, "CodeNomad iOS ABI input drift: {relative}"); ++ } ++ println!("cargo:rerun-if-changed=codenomad_ios_abi_guard.rs"); ++ println!("cargo:rerun-if-changed=codenomad-ios-abi.sha256"); ++} +--- /dev/null ++++ b/codenomad-ios-abi.sha256 +@@ -0,0 +1,13 @@ ++b529ac117054acd98fe38f554460efba5acb7f77d6e7c9ae88df832bb9927f9d Cargo.toml ++82903f5593c97180dcf89d6be4d1aa42f771a400cf0b14406256417b7c3f31e8 build.rs ++c5dec514616e24eb39419985f48de428449eb9a4e46884a98130c6c35f938c05 src/ios.rs ++5079199081a851b68a8caec5696f0703aecf79786497700ba5b62bfa2781013d src/plugin/mobile.rs ++77d1eb6e485cdd6e119f98a29624fbb676f8d79df87153fcdde8ef7ffbe860b6 mobile/ios-api/Package.swift ++850e8239ccb561ea7177f0f66b33d5b63e97faeefcb79b60640007934c3894a4 mobile/ios-api/Sources/Tauri/Channel.swift ++afc9ee63daafe1754969b6dfd104d49d2ca09fd41e7c6343063c13d9e56f7d3e mobile/ios-api/Sources/Tauri/Invoke.swift ++a22a4bad5788132955a63160bda00eeb62a6e138993d46504f6f96e2fb94246d mobile/ios-api/Sources/Tauri/JsonValue.swift ++bce9da78d74d495a2d05176cb0b68bd9a40ebfcac8c8e9cb1678d4ca142bd653 mobile/ios-api/Sources/Tauri/JSTypes.swift ++9624719792f6ce0baf6df0aaf2928efc8c0aef77972a3fb638ce67e3a6dfb918 mobile/ios-api/Sources/Tauri/Logger.swift ++e5d2e0e6f6c178b0ea2745dcd88f68b6a142154c3b245845bb6b9026b3a76d69 mobile/ios-api/Sources/Tauri/Plugin/Plugin.swift ++595f7c8bb38e3e29745f2728d356463f87f732f2c3805cf038cfba23248d2433 mobile/ios-api/Sources/Tauri/Tauri.swift ++c12b2a5a3bde289c45dd6c348cc41d7072c81a1559eac76339ea061c4fa5d89c mobile/ios-api/Sources/Tauri/UiUtils.swift diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1-local.json b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1-local.json new file mode 100644 index 000000000..0fc80200c --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1-local.json @@ -0,0 +1,34 @@ +{ + "upstreamArchiveSHA256": "ed99ee9694a2deb776d91cae48ac7411ddfc89ecae2f9b5041111d8c88f2ace9", + "patchSHA256": "7416bbd978eb088706ad61439b5ac06a2632512c13aa7ebb4794c4138c9f300e", + "modified": { + "build.rs": { + "upstreamSHA256": "98109a67415599af635edd9c041ccdd9b92525556d012369e59cb4b4df25ca29", + "patchedSHA256": "82903f5593c97180dcf89d6be4d1aa42f771a400cf0b14406256417b7c3f31e8" + }, + "Cargo.toml": { + "upstreamSHA256": "c7a328551449e54a9cc5c8ddc61a2b9eef3c6f85980bbccc7e1a5212133bafad", + "patchedSHA256": "b529ac117054acd98fe38f554460efba5acb7f77d6e7c9ae88df832bb9927f9d" + }, + "mobile/ios-api/Package.swift": { + "upstreamSHA256": "bd322622d84f3538b512646267fcf063734ad3dcede35480950cfd95c2ed392a", + "patchedSHA256": "77d1eb6e485cdd6e119f98a29624fbb676f8d79df87153fcdde8ef7ffbe860b6" + }, + "mobile/ios-api/Sources/Tauri/Tauri.swift": { + "upstreamSHA256": "4c7aa0b830e17851784fd14ca6941d2d53bd3b902e0477e7416f90da4488333c", + "patchedSHA256": "595f7c8bb38e3e29745f2728d356463f87f732f2c3805cf038cfba23248d2433" + } + }, + "added": { + "codenomad_ios_abi_guard.rs": "d4bf74812a1c77d70a3d65db9d83ebaa478a72f4b21e6255f9bbf6b752d18225", + "codenomad-ios-abi.sha256": "e00c1a905579e35e744b211902e9558774c9c92b366d290726253205640a82d0" + }, + "swiftRs": { + "swiftVersion": "1.0.7", + "swiftRevision": "f64a4514de07f450ec5b6aa297624cd3479d9579", + "swiftSourceSHA256": "d666f824f48ed780eddefc213c3def0faa8b9cf017ea0febbde3829f4efd58e3", + "rustVersion": "1.0.8", + "rustArchiveSHA256": "e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e" + }, + "scope": "Local separate mobile workspace; no upstream Rust ABI/Android source edits, no SwiftRs buffer fix." +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1-upstream.json b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1-upstream.json new file mode 100644 index 000000000..b45666f46 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1-upstream.json @@ -0,0 +1,576 @@ +{ + "crate": "tauri", + "version": "2.12.1", + "archiveSHA256": "ed99ee9694a2deb776d91cae48ac7411ddfc89ecae2f9b5041111d8c88f2ace9", + "officialURL": "https://static.crates.io/crates/tauri/tauri-2.12.1.crate", + "files": { + ".cargo_vcs_info.json": { + "sha256": "50bfb789b1151bcdaf60ff8e093bea4b0b3db8d1542533ce9ab4dfe71c410035", + "bytes": 106 + }, + "build.rs": { + "sha256": "98109a67415599af635edd9c041ccdd9b92525556d012369e59cb4b4df25ca29", + "bytes": 15862 + }, + "Cargo.lock": { + "sha256": "7cd34aebfe6e3ffa1a7b97662eac6a9830ba5a4003782179e7d00b97c1acff59", + "bytes": 130816 + }, + "Cargo.toml": { + "sha256": "c7a328551449e54a9cc5c8ddc61a2b9eef3c6f85980bbccc7e1a5212133bafad", + "bytes": 9165 + }, + "Cargo.toml.orig": { + "sha256": "4403b34d20b4c790e117ecf4388b0244af1ab6b231e300f2d889a7bf36f93d79", + "bytes": 7668 + }, + "LICENSE-APACHE-2.0": { + "sha256": "0d542e0c8804e39aa7f37eb00da5a762149dc682d7829451287e11b938e94594", + "bytes": 10174 + }, + "LICENSE-MIT": { + "sha256": "9dd42ea92cff2ede5cd477cbfcce051b2d0115c0ac7f368ee88cb545055dff1d", + "bytes": 1090 + }, + "README.md": { + "sha256": "d53737b76e569ea9d7af01d69be6fb729959b97bc08d0564a421c3917b584070", + "bytes": 3517 + }, + "mobile/proguard-tauri.pro": { + "sha256": "9f6f2555912c6b984ab9be8e4766da646b175dd33c3a193a4d72c4a56a1b6a2c", + "bytes": 165 + }, + "mobile/android/.gitignore": { + "sha256": "5c3174b74edc6f2b9e0743594b8bd3c7f31e4f4405758cc9872b3fe1de1d28b4", + "bytes": 6 + }, + "mobile/android/build.gradle.kts": { + "sha256": "7cd902e29c3d5e495600f98669fb67a2b7983d58e69e627fc7d8778be58802d4", + "bytes": 1394 + }, + "mobile/android/consumer-rules.pro": { + "sha256": "07ae2ffe37f0f484c46401f510ef003e1bbe9f708d1d2597546c729eb9956cca", + "bytes": 984 + }, + "mobile/android/src/androidTest/java/app/tauri/ExampleInstrumentedTest.kt": { + "sha256": "1e7c0c6f613431962750a2f80dcf8a0c231c4b5fb2f569bfd55b5fa0e80f2e7d", + "bytes": 793 + }, + "mobile/android/src/main/AndroidManifest.xml": { + "sha256": "8ef94a6aec2218c5fd02408d018f6d52602c0ef4473dd1c115c5c6964f1e63e1", + "bytes": 121 + }, + "mobile/android/src/main/java/app/tauri/AppPlugin.kt": { + "sha256": "087ae5fb739031d886365aeed1c27bde043c5f7d00f892050662bc602e7451b4", + "bytes": 1732 + }, + "mobile/android/src/main/java/app/tauri/FsUtils.kt": { + "sha256": "217851c9539c6171c46354fb24c8af9501babd045a122e239f01009794d122c9", + "bytes": 7219 + }, + "mobile/android/src/main/java/app/tauri/JniMethod.kt": { + "sha256": "ece2f2b7179edeb9cf0a62acb116390f53c23a32e51520b6e4c2d7601a581a88", + "bytes": 237 + }, + "mobile/android/src/main/java/app/tauri/Logger.kt": { + "sha256": "771fa4822dec7cc95aab726ee1c176c17970c4ffee737ad53959fea1fa26ef7d", + "bytes": 1855 + }, + "mobile/android/src/main/java/app/tauri/PathPlugin.kt": { + "sha256": "fe5a98a6ed0ca6e34aa1ea0ee190c16e8e07766e59938f5fb22cf1b5e8f8ed41", + "bytes": 3890 + }, + "mobile/android/src/main/java/app/tauri/PermissionHelper.kt": { + "sha256": "7df0ea86bfa5159a5fbbf7660687c36d15d7cf2cd167d051144e4447a650cb8e", + "bytes": 3895 + }, + "mobile/android/src/main/java/app/tauri/PermissionState.kt": { + "sha256": "c38e1f200db71faf05f8d7acf643b2f1d6dd8970b8ae51a6a7c842743a596be2", + "bytes": 554 + }, + "mobile/android/src/main/java/app/tauri/annotation/ActivityCallback.kt": { + "sha256": "aec0ebccfe66d7b857606a9244e717d5aa3b02324f50fbdfe4b4ee1a67ad8637", + "bytes": 281 + }, + "mobile/android/src/main/java/app/tauri/annotation/InvokeArg.kt": { + "sha256": "6b747509a75a7c8f1c31c03a23a80b4f83cd226f87033a318db1782505b2aeab", + "bytes": 271 + }, + "mobile/android/src/main/java/app/tauri/annotation/Permission.kt": { + "sha256": "bca04adb46a3de1c36da755fb62a5efb40481a7a529dd0f727eadd95466ecbd7", + "bytes": 551 + }, + "mobile/android/src/main/java/app/tauri/annotation/PermissionCallback.kt": { + "sha256": "d0a63179ca048629d37e9efe696d2c6554095b7de64bbbc1450a8f07d3888d8e", + "bytes": 283 + }, + "mobile/android/src/main/java/app/tauri/annotation/PluginMethod.kt": { + "sha256": "aee7aef7a3f59ad67ca3a2cfbcb2a2c2cacfa28bddf4b2f565ed6e930f92c8c1", + "bytes": 237 + }, + "mobile/android/src/main/java/app/tauri/annotation/TauriPlugin.kt": { + "sha256": "361783a7baf30a33192c22f922f50fb2c97d8a7d9ae9dc774d8b637f9eeea04a", + "bytes": 516 + }, + "mobile/android/src/main/java/app/tauri/plugin/Channel.kt": { + "sha256": "f6f5c2dcc442b9e81165f55f80d363129bc91b130586abb8489cd3006af00011", + "bytes": 1306 + }, + "mobile/android/src/main/java/app/tauri/plugin/InvalidPluginMethodException.kt": { + "sha256": "90d2dddfb918447ed8fd35c44be7f00e41b89fcd586eeca995e0ead56c38b6c4", + "bytes": 364 + }, + "mobile/android/src/main/java/app/tauri/plugin/Invoke.kt": { + "sha256": "05cadb94907dbd3979d8c7eaad7bd8f540c38cc9d988a5513777eadd18330028", + "bytes": 2131 + }, + "mobile/android/src/main/java/app/tauri/plugin/JSArray.kt": { + "sha256": "fca8b72a6bf357b6cc57aa21ac787dbe3d69fca27f927b0c3f598567dc17f52f", + "bytes": 1155 + }, + "mobile/android/src/main/java/app/tauri/plugin/JSObject.kt": { + "sha256": "234a9afa4da858b3dbd8d5a688209a640984166482e040ab908b34ed577726b5", + "bytes": 3569 + }, + "mobile/android/src/main/java/app/tauri/plugin/Plugin.kt": { + "sha256": "c5dc0d1b59ac2024399afd0893f1182ae583b4d4f2cc88a5ba7864d365f77f7c", + "bytes": 17634 + }, + "mobile/android/src/main/java/app/tauri/plugin/PluginHandle.kt": { + "sha256": "628b69f19d03b861765a2f7f3140e5b6a79bd991966019bb250bf7ae79bfd301", + "bytes": 5529 + }, + "mobile/android/src/main/java/app/tauri/plugin/PluginManager.kt": { + "sha256": "053958be1e83d8633d26348ded26bbd6d4ab21121e662985194fa3342eef1e95", + "bytes": 8588 + }, + "mobile/android/src/main/java/app/tauri/plugin/PluginMethodData.kt": { + "sha256": "c3024eb23b038fdd1566e69db725c8c5d39fb66909baf45d258ccfe8449daedf", + "bytes": 371 + }, + "mobile/android/src/main/java/app/tauri/plugin/PluginResult.kt": { + "sha256": "437f44a6ccf5354a5ec1a3a3c894609f6df90999f397e52dfaafb6e74d30c006", + "bytes": 1629 + }, + "mobile/android/src/test/java/app/tauri/ExampleUnitTest.kt": { + "sha256": "c0b0b313769a0e3f519c203bf0862f35bd6128a6eeef67ca4965c12b4ef0ac4b", + "bytes": 476 + }, + "mobile/android-codegen/TauriActivity.kt": { + "sha256": "575a5890676bcc363294d2e3ec6ebe0a3943358fac4bc168e7ea2df0b7bc3640", + "bytes": 1373 + }, + "mobile/ios-api/.gitignore": { + "sha256": "bc9466a4cd82cf38d2a4471c15c5c6b8153182f361a72c9926a9f422d966e3b0", + "bytes": 182 + }, + "mobile/ios-api/Package.swift": { + "sha256": "bd322622d84f3538b512646267fcf063734ad3dcede35480950cfd95c2ed392a", + "bytes": 1142 + }, + "mobile/ios-api/README.md": { + "sha256": "a06a181db430708640f970ac98611e3feea64430d5b4ab82c15d778604937591", + "bytes": 24 + }, + "mobile/ios-api/Sources/Tauri/Channel.swift": { + "sha256": "850e8239ccb561ea7177f0f66b33d5b63e97faeefcb79b60640007934c3894a4", + "bytes": 1821 + }, + "mobile/ios-api/Sources/Tauri/Invoke.swift": { + "sha256": "afc9ee63daafe1754969b6dfd104d49d2ca09fd41e7c6343063c13d9e56f7d3e", + "bytes": 2928 + }, + "mobile/ios-api/Sources/Tauri/JsonValue.swift": { + "sha256": "a22a4bad5788132955a63160bda00eeb62a6e138993d46504f6f96e2fb94246d", + "bytes": 1692 + }, + "mobile/ios-api/Sources/Tauri/JSTypes.swift": { + "sha256": "bce9da78d74d495a2d05176cb0b68bd9a40ebfcac8c8e9cb1678d4ca142bd653", + "bytes": 3614 + }, + "mobile/ios-api/Sources/Tauri/Logger.swift": { + "sha256": "9624719792f6ce0baf6df0aaf2928efc8c0aef77972a3fb638ce67e3a6dfb918", + "bytes": 3739 + }, + "mobile/ios-api/Sources/Tauri/Tauri.swift": { + "sha256": "4c7aa0b830e17851784fd14ca6941d2d53bd3b902e0477e7416f90da4488333c", + "bytes": 4879 + }, + "mobile/ios-api/Sources/Tauri/UiUtils.swift": { + "sha256": "c12b2a5a3bde289c45dd6c348cc41d7072c81a1559eac76339ea061c4fa5d89c", + "bytes": 742 + }, + "mobile/ios-api/Sources/Tauri/Plugin/Plugin.swift": { + "sha256": "e5d2e0e6f6c178b0ea2745dcd88f68b6a142154c3b245845bb6b9026b3a76d69", + "bytes": 2036 + }, + "mobile/ios-api/Tests/TauriTests/TauriTests.swift": { + "sha256": "ef1e10dafc147b1d6b145f19ab0a81cb44eb80d7b989abfae4e2447209a88a34", + "bytes": 476 + }, + "permissions/app/autogenerated/reference.md": { + "sha256": "f8e0917f509f1607e16d6bbdc6218a96c69fc7113f143a0df03d943251268a84", + "bytes": 4792 + }, + "permissions/event/autogenerated/reference.md": { + "sha256": "018766e1817884ed384b525480e58e63d1860994441c114d1a2517967a267d49", + "bytes": 1303 + }, + "permissions/image/autogenerated/reference.md": { + "sha256": "e3fe93fbb7e62c0139592f4b030d50254c1fbbcc518785bb98ab1f037e251e5c", + "bytes": 1563 + }, + "permissions/menu/autogenerated/reference.md": { + "sha256": "4bed65930223d505c3fc3295d299f07165d5b355b7a5ba910501fc089b2f0044", + "bytes": 6509 + }, + "permissions/path/autogenerated/reference.md": { + "sha256": "8be6a806aaaf1a081fe25aadabf253b74337c5f31b5f356c078ce43ddd47a098", + "bytes": 2452 + }, + "permissions/resources/autogenerated/reference.md": { + "sha256": "593941263d28df065d95a9db260220fad053d4c5aca3b74a9d58977199127210", + "bytes": 506 + }, + "permissions/tray/autogenerated/reference.md": { + "sha256": "f17969a556be8c3788821cc5eaffa3a2604ab78f647aaae294305a93b67c0178", + "bytes": 3834 + }, + "permissions/webview/autogenerated/reference.md": { + "sha256": "5ee143504a878ca35dcd1277310c152fc93668da50573c1acfab051fa9cfe8ec", + "bytes": 5610 + }, + "permissions/window/autogenerated/reference.md": { + "sha256": "9c688eabe29028b8e183b656fdcefd06a0d43f0903607c80a61886f38015483b", + "bytes": 23033 + }, + "scripts/bundle.global.js": { + "sha256": "a5354c0a20fdc756bd5357695aac0620964a42241cd397530437fb8991c5bd74", + "bytes": 41061 + }, + "scripts/core.js": { + "sha256": "71056442a732cca1f03572ccae96d68e16104c3a591c64de9618cd304e063070", + "bytes": 2981 + }, + "scripts/freeze_prototype.js": { + "sha256": "599745f0da51f0ba6c7dcbb7c770d6f538f337f6df7f1bf420e4043b9e4b6bf9", + "bytes": 174 + }, + "scripts/init.js": { + "sha256": "2a062abc65bc99ac458dd9a78701a8a9e3f473aa1f25afa64dc810036932f973", + "bytes": 300 + }, + "scripts/ipc-protocol.js": { + "sha256": "68ae690606006f733bde75dd8bb6748464954a22c7e83914a93b1abc221d7072", + "bytes": 3390 + }, + "scripts/ipc.js": { + "sha256": "3e3766fc90efa68003096cbf0a07c6a00cb31c5756d55b5a68ebbfd2930c4865", + "bytes": 5832 + }, + "scripts/isolation.js": { + "sha256": "58e79828ba6d68c79eceb276d62f766710f77df70c5fec1f79176bd6aa6d0ae1", + "bytes": 584 + }, + "scripts/pattern.js": { + "sha256": "149570459687f1636c02e41ede8b40efe52378965eeddb6dbfebc0c525a3551b", + "bytes": 558 + }, + "scripts/process-ipc-message-fn.js": { + "sha256": "d51917f89e4d3d318d63f1f4f5a7ac7ea1399a5b85c81a840c4d606597f29132", + "bytes": 1187 + }, + "src/app.rs": { + "sha256": "ef71e5c2da6696f9bef817f5f32d34c7710b678ca7ec8a00e539527bca5ad1d4", + "bytes": 94329 + }, + "src/async_runtime.rs": { + "sha256": "2e46673c2b396b34cc70e8a09cabce4ea9ef0eb517b782d1825299d2e998209c", + "bytes": 9124 + }, + "src/error.rs": { + "sha256": "dfe50c5983d770d846d88ea3b0b83d156d36fb8ae1636fbb90d8f5bf481bc8b7", + "bytes": 7164 + }, + "src/ios.rs": { + "sha256": "c5dec514616e24eb39419985f48de428449eb9a4e46884a98130c6c35f938c05", + "bytes": 1313 + }, + "src/lib.rs": { + "sha256": "fda874d750f7ce7959b5914707257ebd49b4a619d2e1c23c74b52821371c2bd0", + "bytes": 44438 + }, + "src/pattern.rs": { + "sha256": "0d4a4f4c88fa93a590a3e9ce3e0438dd4263b42ab27cc2424bb83de7c8a56edd", + "bytes": 2428 + }, + "src/plugin.rs": { + "sha256": "8c8a9c3cdb93d1d149f1f1822e5bf8cd5e454f1ce7f3e57a427b97ba59f52a9f", + "bytes": 34621 + }, + "src/process.rs": { + "sha256": "9f64dab90f86a47aa83f6e8cb449fa2b11a0e214038524a82f5da6cfc1d4ad4a", + "bytes": 3794 + }, + "src/state.rs": { + "sha256": "2a6982ffa78274c476d2af7af785c94f36a033de030fa382cd727b18595481b8", + "bytes": 8579 + }, + "src/app/plugin.rs": { + "sha256": "68ae251a4c9abbbef89d618784ce81e6557396a0f1e0fcdc9b5c0653e3782c6d", + "bytes": 3933 + }, + "src/event/event-system-spec.md": { + "sha256": "ca15ed691a62d531b790f1fab1d3b07f6e5dad3711c14e6c821d64b802038e02", + "bytes": 1003 + }, + "src/event/event_name.rs": { + "sha256": "982908c70b4d51378d2298f8446fbbf1dbb253856b46c591c680303affd4765e", + "bytes": 1920 + }, + "src/event/init.js": { + "sha256": "8946488895cc9879f2bf8fee98be454d0f21949b946c264b47d2a2fc84eb1df0", + "bytes": 317 + }, + "src/event/listener.rs": { + "sha256": "e42359ff576ae46fc6c09c31574155aff4bdb880c166182330469deee37ae6b8", + "bytes": 14740 + }, + "src/event/mod.rs": { + "sha256": "dad05d91113bb2c932318db93a2dcf534d995c49039cd24cc66463690c82d6d3", + "bytes": 6049 + }, + "src/event/plugin.rs": { + "sha256": "32f3b01840e02b21b34e8ec7e35fdc27123717ee4a27d44f26c6814e7df44f0f", + "bytes": 2096 + }, + "src/image/mod.rs": { + "sha256": "3ea357f4b38c35bb48007fa10fb9180f90dd4a072995c84f4b6e8dae5d431334", + "bytes": 17262 + }, + "src/image/plugin.rs": { + "sha256": "921e0dc65f9923fd742c7d431162ca2166aa43994eb454d9cc9ae8f534daa60e", + "bytes": 2680 + }, + "src/ipc/authority.rs": { + "sha256": "e0d98da404549cc74fe4c5a7b038a3b45f8886fe11a38fb690e9a13e0ce5e6d8", + "bytes": 46398 + }, + "src/ipc/capability_builder.rs": { + "sha256": "835d94cfb7d021a7cdacb1210bf4e01eb587c4d71317878ebde40080f72a28cd", + "bytes": 4374 + }, + "src/ipc/channel.rs": { + "sha256": "9d20fad897f8377484c7bd7f63b030653501a08b36f03def9cc5123c2a98623e", + "bytes": 20293 + }, + "src/ipc/command.rs": { + "sha256": "f5ee670a88bb1ac69e51c2c7ca22d82b11dc183d447c74720a7354a9b9e37fd2", + "bytes": 9912 + }, + "src/ipc/format_callback.rs": { + "sha256": "e9de68956b9437f440291d37292bb9544b6c0b180029e6d2772685aa768a494d", + "bytes": 12517 + }, + "src/ipc/mod.rs": { + "sha256": "6aa3624871ec604883e799e728346e6f474808589ab5b67c1db7bc600ef0068b", + "bytes": 15919 + }, + "src/ipc/protocol.rs": { + "sha256": "e4b16a8800a249011753665eb28a004d8d6712af66c2fedbff094bc4fb99515e", + "bytes": 24671 + }, + "src/manager/menu.rs": { + "sha256": "3bb2b10650ed22166eaf28f8d7dbee1333a9f2a1908be4532d3fd4e621b9603d", + "bytes": 3046 + }, + "src/manager/mod.rs": { + "sha256": "6e39aff7d45d40f163a26f2b9484f1aab6977209762204a0650b9d3ad9636793", + "bytes": 29822 + }, + "src/manager/tray.rs": { + "sha256": "b84c32a8921e5a9da80ff388d6c529e45713a5f7fc8f6be1b09795748a04edc7", + "bytes": 2542 + }, + "src/manager/webview.rs": { + "sha256": "3bfb8bba5dbd61d08ec5ff4ce2fa882997df78666b840a8f13b30aea2c2e7381", + "bytes": 25200 + }, + "src/manager/window.rs": { + "sha256": "01afc333b4d626650a3e23cd526018bb6dd01e7dab8b45680b9f99404dc63b25", + "bytes": 9808 + }, + "src/menu/check.rs": { + "sha256": "c18429d823a153631295ea1b7abf206ca7be5e3d273f87d3b3dbf4a4e844d03a", + "bytes": 4160 + }, + "src/menu/icon.rs": { + "sha256": "b9e556a6387dadc723d3db1b66cfea035134dd3aab6323cb2e73c01d78b0a830", + "bytes": 6621 + }, + "src/menu/menu.rs": { + "sha256": "c0b07c3f251c98be5afe843c014909105034281029dcd46d205f9ec044889502", + "bytes": 11760 + }, + "src/menu/mod.rs": { + "sha256": "17e8cecf17829fe106f6e8ee60c8b9ac2fd50d8d4f7d9cc2b04fcf07af6bbf6b", + "bytes": 23749 + }, + "src/menu/normal.rs": { + "sha256": "cd8243024d763935c0c3c8d521b6513c0b7a02eae029bdf83b3f0614c68f1485", + "bytes": 3670 + }, + "src/menu/plugin.rs": { + "sha256": "af225e605a40a4e360fecffaa3c2cf508ac7ada8a368ceadb5144e8f743162f0", + "bytes": 27271 + }, + "src/menu/predefined.rs": { + "sha256": "dd082dcd3b4f77bd3130c567856ab2cd575b3fcb3b502e554457115411a18a07", + "bytes": 10376 + }, + "src/menu/submenu.rs": { + "sha256": "5dd083a2f62642604f9fb075c2488b32178f0e3f004c70a3efd41ab0de90cb33", + "bytes": 12979 + }, + "src/menu/builders/check.rs": { + "sha256": "b50d23149fbf6ef12f9ef126f0c9e6ec759bb0126ef2f5b669069c44fc07cb74", + "bytes": 2433 + }, + "src/menu/builders/icon.rs": { + "sha256": "5acb00bab97cf8ed585d2c0a97872a4a8fc7f170272e5ef77262b9b9aefb43f2", + "bytes": 3435 + }, + "src/menu/builders/menu.rs": { + "sha256": "270946a7cd242bd14ce993ffe2df8faa08e2984dc6a362ba882c20b019632716", + "bytes": 19997 + }, + "src/menu/builders/mod.rs": { + "sha256": "8e47f079550864a58a0a9b3e0dcce452ccc210ac303861bb91b92b7553b497ea", + "bytes": 409 + }, + "src/menu/builders/normal.rs": { + "sha256": "2b93c00d50b9bf4f01dea4be90bbe902997cc6319f9a01e201ca324f61dce783", + "bytes": 2063 + }, + "src/path/android.rs": { + "sha256": "8d82a2cb98a062bc115f0f68fdcfff0ec7a12771c7278e5cd6fe549f9e282214", + "bytes": 6911 + }, + "src/path/desktop.rs": { + "sha256": "9fab956b1c533eff123146fd481bee169c3d6efc48a43dbce6dc74e5469d9c24", + "bytes": 20481 + }, + "src/path/init.js": { + "sha256": "6ecfef8c9078a591f04c5eb7ed355721efcba1eb820a0b5067be1dcca7748ef3", + "bytes": 293 + }, + "src/path/mod.rs": { + "sha256": "342c7b8fe55de2284cad6667c5d86296f143539aa3c8b4dba645a449b26dcf05", + "bytes": 17183 + }, + "src/path/plugin.rs": { + "sha256": "b6e69d7dfe1490cd21125abef6aad35a5f56661a0a54484e5276a2ff1d436381", + "bytes": 9541 + }, + "src/plugin/mobile.rs": { + "sha256": "5079199081a851b68a8caec5696f0703aecf79786497700ba5b62bfa2781013d", + "bytes": 14396 + }, + "src/protocol/asset.rs": { + "sha256": "37b792260b5eaee9367abfa59034ef4feb8d1db02362299a296a734fdced3264", + "bytes": 10396 + }, + "src/protocol/isolation.rs": { + "sha256": "65ae247b18dd551e7ce9a1c535beb5114891d73f33ddef50f3c8c9d3de7062af", + "bytes": 3462 + }, + "src/protocol/mod.rs": { + "sha256": "4c5bffce1f2b4a493a0a8daca3c84551ccc78a3d06a1c8bbe17dcc1dfd7f31fa", + "bytes": 1586 + }, + "src/protocol/tauri.rs": { + "sha256": "c8c532b5a80e745fd8a99d52aaed9c4555351b7385b036361b25e75c56cf2be6", + "bytes": 8039 + }, + "src/resources/mod.rs": { + "sha256": "c70407d96ebe8851ed7d269cdd7ee167c148d542dc08026086ddb8473f46a4ce", + "bytes": 8322 + }, + "src/resources/plugin.rs": { + "sha256": "4aa9ad316ace48bf9821f4cfb7a88134f92f97ed5d73d9e7ff8e33b628ac0c6e", + "bytes": 806 + }, + "src/scope/fs.rs": { + "sha256": "13f7132cc2cb4c2985a97bb5a60bfb5551e712992eec0977445de861e7440c15", + "bytes": 23053 + }, + "src/scope/mod.rs": { + "sha256": "f72963f21cdfddeac4a60dd6609765b699ec86cbde293314a55f09779c4583b1", + "bytes": 3067 + }, + "src/test/mock_runtime.rs": { + "sha256": "ef04c3634879fd347d994c73666becbe85fe5001347e73ac5d35cde64dc05ee5", + "bytes": 32962 + }, + "src/test/mod.rs": { + "sha256": "99dbbe5445c5a8619624b2a3d9ff90ca95d4f4bdcd431e1f5e11157d431c4618", + "bytes": 10302 + }, + "src/tray/mod.rs": { + "sha256": "ff073c5dec626463e013e133bed97553fce3f7b9fa1b007dd6e16a5886ced052", + "bytes": 20606 + }, + "src/tray/plugin.rs": { + "sha256": "b59e76ce21cb806037744997af9d15e13bc023aeb9a4755506429847f86aab43", + "bytes": 7056 + }, + "src/vibrancy/macos.rs": { + "sha256": "053b272e9eeb4c99356486d3afe8ead9cc6f8827ed6894c1cfe875083add468f", + "bytes": 4095 + }, + "src/vibrancy/mod.rs": { + "sha256": "dbf1cfbc0783cec7af0f8156bed76b9e737d80f17720ad0f46a8fa209fe95f40", + "bytes": 760 + }, + "src/vibrancy/windows.rs": { + "sha256": "7cc46026788b31a9d08f7820d73fe69957ed5289b7ef6e3692cea885a1245522", + "bytes": 1742 + }, + "src/webview/mod.rs": { + "sha256": "b5319b171c09a8e652d75585be39d92b387df370340ca90b58141b501c17009a", + "bytes": 96498 + }, + "src/webview/plugin.rs": { + "sha256": "5a7ac2c1401c8c06946e00b6bc8956d3efcdcb4f494aeb794e5ed7053ac0fc71", + "bytes": 7053 + }, + "src/webview/webview_window.rs": { + "sha256": "5cf28b52b0ed7179926b1087df08d8765bf2c31805fb532f9260908a57422230", + "bytes": 100324 + }, + "src/webview/scripts/print.js": { + "sha256": "2a7c344c3c7285cc30ae14b741484db1acf24f03a22bda6ffe6c6d16d8acbf0b", + "bytes": 240 + }, + "src/webview/scripts/toggle-devtools.js": { + "sha256": "433c08901992823a37396ce72a695636bd034c1b960364b7d9c75f261f771770", + "bytes": 864 + }, + "src/webview/scripts/zoom-hotkey.js": { + "sha256": "3a29990c1551bbb7bbca34e318dd6a044963c6d8b7cc14c1600db6e6f45fdc97", + "bytes": 1157 + }, + "src/window/mod.rs": { + "sha256": "58734d4a9165cd270b021bee75310b897d7d3f4ad64644b1b574b898a48fa153", + "bytes": 79698 + }, + "src/window/plugin.rs": { + "sha256": "125ce6fffffdd5ba7aec2b0bd4c75fb83a4fad868e11f93cf725a2ec7b664eab", + "bytes": 11192 + }, + "src/window/scripts/drag.js": { + "sha256": "2d47d2e302512024af4ded95b89f8b1f5c8d83530d287c252052112901a59888", + "bytes": 4367 + } + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/.cargo_vcs_info.json b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/.cargo_vcs_info.json new file mode 100644 index 000000000..0628fcdfb --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "30da1fd6e17de6107ecc850c95dfb16b5729f2dd" + }, + "path_in_vcs": "crates/tauri" +} \ No newline at end of file diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/Cargo.lock b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/Cargo.lock new file mode 100644 index 000000000..d457de81f --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/Cargo.lock @@ -0,0 +1,5328 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2fb6cfd47bf496ff64095c20eaba0c201404ee38714d4142fcfa1dc334fcc7a" + +[[package]] +name = "alloc-stdlib" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5c1865780388bfa186411ab5f247819487fc4864c6e9c3106611fa347586e1" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + +[[package]] +name = "brotli" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b851b75c23ca7873623d612fe49bd1989aeb03d08fb9432187eb253d3d4c6b" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46087801e40b785410c352bee60e9b6226312709cf52d5fff92776cdf5cf6490" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc309ed89476c8957c50fb818f56fe894db857866c3e163335faa91dc34eb85" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "cargo_toml" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa61aec073ec94791433ddf3df2323ff9d1711557c2a0eefb0f99cb4f8dca520" +dependencies = [ + "semver", + "serde", + "toml 1.0.6+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.1.1", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.0", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.0", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.0", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[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.117", +] + +[[package]] +name = "ctor" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01334b89b69ff726750c5ce5073fc8bd860e99aa9a8fc5ca11b04730e3aee97a" + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "data-url" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" + +[[package]] +name = "dbus" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.0", + "block2 0.6.2", + "libc", + "objc2 0.6.4", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dlopen2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dom_query" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac5fca71e65e94cc718a6e2af65d6e0f9c6027751c2aa562fbb5087fda639bc" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + +[[package]] +name = "embed-resource" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.0.6+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "erased-serde" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" + +[[package]] +name = "flate2" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.1", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core 0.52.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png 0.18.1", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4200d433cbd5178df7797c9c2e75b348b728e39631cf14520d1e2fc424201f4" +dependencies = [ + "cfb", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7421438de105a0827e44fadd05377727847d717c80ce29a229f85fd04c427b72" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 2.0.12", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "jsonptr" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d" +dependencies = [ + "bitflags 2.13.0", + "serde", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.13.0", + "libc", +] + +[[package]] +name = "libxdo" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db" +dependencies = [ + "libxdo-sys", +] + +[[package]] +name = "libxdo-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212" +dependencies = [ + "libc", + "x11", +] + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "muda" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cca139e57da4383727f189e43e66e84fa372347027fc996f9f1a007ec0a37996" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "libxdo", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.12", + "windows-sys 0.61.2", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe 0.1.5", + "openssl-sys", + "schannel", + "security-framework 2.11.1", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.0", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.0", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.13.0", + "block2 0.5.1", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.0", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.0", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.13.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.13.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.0", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.0", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-probe" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" + +[[package]] +name = "openssl-src" +version = "300.4.1+3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faa4eac4138c62414b5622d1b31c5c304f34b406b013c079c2bbc652fdd6678c" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" +dependencies = [ + "memchr", + "thiserror 2.0.12", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pest_meta" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "plist" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" +dependencies = [ + "base64 0.22.1", + "indexmap 2.11.4", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +dependencies = [ + "toml_edit 0.20.7", +] + +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit 0.22.24", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.13.0", + "lazy_static", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quickcheck" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" +dependencies = [ + "env_logger", + "log", + "rand", +] + +[[package]] +name = "quickcheck_macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b22a693222d716a9587786f37ac3f6b4faedb5b80c23914e7303ff5a1d8016e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror 2.0.12", +] + +[[package]] +name = "ref-cast" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.15", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "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 0.2.0", + "rustls-pki-types", + "schannel", + "security-framework 3.5.1", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.0", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework 3.5.1", + "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]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.0", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8adfa1c298912827b8a28b223b3b874357397ae706e6190acd9bf28cee99114d" +dependencies = [ + "bitflags 2.13.0", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2676ba99bd82f75cae5cbd2c8eda6fa0b8760f18978ea840e980dd5567b5c5b6" +dependencies = [ + "erased-serde", + "serde", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.11.4", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" +dependencies = [ + "bytemuck", + "cfg_aliases", + "core-graphics 0.24.0", + "foreign-types 0.5.0", + "js-sys", + "log", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", + "raw-window-handle", + "redox_syscall", + "wasm-bindgen", + "web-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "specta" +version = "2.0.0-rc.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ccbb212565d2dc177bc15ecb7b039d66c4490da892436a4eee5b394d620c9bc" +dependencies = [ + "paste", + "specta-macros", + "thiserror 1.0.69", +] + +[[package]] +name = "specta-macros" +version = "2.0.0-rc.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68999d29816965eb9e5201f60aec02a76512139811661a7e8e653abc810b8f72" +dependencies = [ + "Inflector", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-rs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.20", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d12c8607b01516c5d4a37e471a9cfb40b4a25cebce8fecbc6a559e125e51b6b" +dependencies = [ + "bitflags 2.13.0", + "block2 0.6.2", + "core-foundation 0.10.0", + "core-graphics 0.25.0", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-foundation 0.3.2", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.62.0", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.12.1" +dependencies = [ + "anyhow", + "bytes", + "cargo_toml", + "cookie", + "data-url", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "http-range", + "image", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-foundation 0.3.2", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "proptest", + "quickcheck", + "quickcheck_macros", + "raw-window-handle", + "reqwest", + "rustls", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "specta", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.12", + "tokio", + "tracing", + "tray-icon", + "url", + "uuid", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59563ca5b331fd97f27ee9672ec8d36d33bb3c9e2c9f6710a1a5687256052e45" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e50213e463086ecc283d7e9f51c30cc0fd304ad457425158018be26dbf0d0a80" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.18.1", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.117", + "tauri-utils", + "thiserror 2.0.12", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8129fb43bb6e0d6b73ad3315cd172beaab17cd28899c594b1b32c80edbbfd99c" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-runtime" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a57a2a9b8c8b7fc00d35b510fe808ded7f9f1b44fc1cdda6aee38632ea5413" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2 0.6.4", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.12", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e7e0c9f80a5130773cfabb6df4dc924518fb1b2c923726966804634b57d22" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2 0.6.4", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "tracing", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ff55a614843b9a3f010f211df637c94df2607175b8d0f6e0f4a7c19e18621ee" +dependencies = [ + "aes-gcm", + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "getrandom 0.3.4", + "glob", + "http", + "infer", + "json-patch", + "json5", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "serialize-to-javascript", + "swift-rs", + "thiserror 2.0.12", + "toml 1.0.6+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.0.6+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "time" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.5.8", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +dependencies = [ + "serde", + "serde_spanned 0.6.8", + "toml_datetime 0.6.8", + "toml_edit 0.22.24", +] + +[[package]] +name = "toml" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc" +dependencies = [ + "indexmap 2.11.4", + "serde_core", + "serde_spanned 1.0.4", + "toml_datetime 1.0.0+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.14", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.0.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.11.4", + "toml_datetime 0.6.8", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +dependencies = [ + "indexmap 2.11.4", + "toml_datetime 0.6.8", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +dependencies = [ + "indexmap 2.11.4", + "serde", + "serde_spanned 0.6.8", + "toml_datetime 0.6.8", + "winnow 0.7.14", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow 0.7.14", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.13.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b9c52859a94554803ccd4a24b98f74148ebc73b90676d783f3490b1bff9d72" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.12", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df16f50ef4cc145211879a3867ba757076b25dfee812040dcb0658bd9ae7904b" +dependencies = [ + "icu_properties", + "regex", + "serde", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom 0.2.15", + "serde", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" +dependencies = [ + "js-sys", + "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 = "web_atoms" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webview2-com" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.62.0", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "webview2-com-sys" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a07132775117d6065853d9d1178157b8c90e228de47129d6bce2c7edebedfb" +dependencies = [ + "thiserror 2.0.12", + "windows", + "windows-core 0.62.0", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "111e51caca442cafd9bab396628ac4d814880eaea4e63dfd76a12e9f342b5580" +dependencies = [ + "objc2 0.6.4", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", + "raw-window-handle", + "windows-sys 0.61.2", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9579d0e6970fd5250aa29aba5994052385ff55cf7b28a059e484bb79ea842e42" +dependencies = [ + "windows-collections", + "windows-core 0.62.0", + "windows-future", + "windows-link 0.2.1", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90dd7a7b86859ec4cdf864658b311545ef19dbcf17a672b52ab7cefe80c336f" +dependencies = [ + "windows-core 0.62.0", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2194dee901458cb79e1148a4e9aac2b164cc95fa431891e7b296ff0b2f1d8a6" +dependencies = [ + "windows-core 0.62.0", + "windows-link 0.2.1", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce3498fe0aba81e62e477408383196b4b0363db5e0c27646f932676283b43d8" +dependencies = [ + "windows-core 0.62.0", + "windows-link 0.2.1", +] + +[[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.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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-version" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c12476c23a74725c539b24eae8bfc0dac4029c39cdb561d9f23616accd4ae26d" +dependencies = [ + "windows-targets 0.53.2", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a819957a01b3119af85e638a38d242af76dbc87d130dca67bfd0441072e21ff0" +dependencies = [ + "base64 0.22.1", + "block2 0.6.2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.12", + "tracing", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.62.0", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/Cargo.toml b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/Cargo.toml new file mode 100644 index 000000000..4c0a1fe9a --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/Cargo.toml @@ -0,0 +1,433 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2024" +rust-version = "1.90" +name = "tauri" +version = "2.12.1" +authors = ["Tauri Programme within The Commons Conservancy"] +build = "build.rs" +links = "Tauri" +exclude = [ + "/test", + "/.scripts", + "CHANGELOG.md", + "/target", +] +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Make tiny, secure apps for all desktop platforms with Tauri" +homepage = "https://tauri.app/" +readme = "README.md" +categories = [ + "gui", + "web-programming", +] +license = "Apache-2.0 OR MIT" +repository = "https://github.com/tauri-apps/tauri" + +[package.metadata.docs.rs] +no-default-features = true +features = [ + "wry", + "unstable", + "custom-protocol", + "tray-icon", + "devtools", + "image-png", + "protocol-asset", + "test", + "specta", + "dynamic-acl", + "isolation", +] +default-target = "x86_64-unknown-linux-gnu" +targets = [ + "x86_64-pc-windows-msvc", + "x86_64-unknown-linux-gnu", + "x86_64-apple-darwin", + "x86_64-linux-android", + "x86_64-apple-ios", +] + +[package.metadata.cargo-udeps.ignore] +normal = ["reqwest"] +build = ["tauri-build"] +development = ["quickcheck_macros"] + +[features] +common-controls-v6 = [ + "tray-icon?/muda-common-controls-v6", + "muda/common-controls-v6", + "tauri-runtime-wry?/common-controls-v6", +] +compression = [ + "tauri-macros/compression", + "tauri-utils/compression", +] +config-json5 = ["tauri-macros/config-json5"] +config-toml = ["tauri-macros/config-toml"] +custom-protocol = ["tauri-macros/custom-protocol"] +dbus = ["tauri-runtime-wry?/dbus"] +default = [ + "wry", + "compression", + "common-controls-v6", + "dynamic-acl", + "x11", + "dbus", +] +devtools = [ + "tauri-runtime/devtools", + "tauri-runtime-wry?/devtools", +] +dynamic-acl = [] +image-ico = ["image/ico"] +image-png = ["image/png"] +isolation = [ + "tauri-utils/isolation", + "tauri-macros/isolation", + "uuid", +] +linux-libxdo = [ + "tray-icon/muda-libxdo", + "muda/libxdo", +] +macos-private-api = [ + "tauri-runtime/macos-private-api", + "tauri-runtime-wry?/macos-private-api", +] +macos-proxy = ["tauri-runtime-wry?/macos-proxy"] +native-tls = ["reqwest/native-tls"] +native-tls-vendored = ["reqwest/native-tls-vendored"] +objc-exception = [] +process-relaunch-dangerous-allow-symlink-macos = ["tauri-utils/process-relaunch-dangerous-allow-symlink-macos"] +protocol-asset = ["http-range"] +rustls-tls = [ + "reqwest/rustls-no-provider", + "dep:rustls", +] +specta = ["dep:specta"] +test = [] +tracing = [ + "dep:tracing", + "tauri-macros/tracing", + "tauri-runtime-wry?/tracing", +] +tray-icon = ["dep:tray-icon"] +unstable = ["tauri-runtime-wry?/unstable"] +webview-data-url = [ + "data-url", + "tauri-utils/html-manipulation-2", +] +wry = [ + "webview2-com", + "webkit2gtk", + "tauri-runtime-wry", +] +x11 = ["tauri-runtime-wry?/x11"] + +[lib] +name = "tauri" +path = "src/lib.rs" + +[dependencies.anyhow] +version = "1" + +[dependencies.cookie] +version = "0.18" + +[dependencies.data-url] +version = "0.3" +optional = true + +[dependencies.dirs] +version = "7" + +[dependencies.dunce] +version = "1" + +[dependencies.getrandom] +version = "0.3" + +[dependencies.glob] +version = "0.3" + +[dependencies.heck] +version = "0.5" + +[dependencies.http] +version = "1" + +[dependencies.http-range] +version = "0.1" +optional = true + +[dependencies.image] +version = "0.25" +optional = true +default-features = false + +[dependencies.log] +version = "0.4.21" + +[dependencies.mime] +version = "0.3" + +[dependencies.percent-encoding] +version = "2" + +[dependencies.raw-window-handle] +version = "0.6" +features = ["std"] + +[dependencies.serde] +version = "1" +features = [ + "derive", + "rc", +] + +[dependencies.serde_json] +version = "1" +features = ["raw_value"] + +[dependencies.serde_repr] +version = "0.1" + +[dependencies.serialize-to-javascript] +version = "0.1.2" + +[dependencies.specta] +version = "^2.0.0-rc.16" +features = [ + "function", + "derive", +] +optional = true +default-features = false + +[dependencies.tauri-macros] +version = "~2.7.1" + +[dependencies.tauri-runtime] +version = "~2.12.1" + +[dependencies.tauri-runtime-wry] +version = "~2.12.1" +optional = true +default-features = false + +[dependencies.tauri-utils] +version = "~2.10.1" +features = ["resources"] + +[dependencies.thiserror] +version = "2" + +[dependencies.tokio] +version = "1" +features = [ + "rt", + "rt-multi-thread", + "sync", + "fs", + "io-util", +] + +[dependencies.tracing] +version = "0.1" +optional = true + +[dependencies.url] +version = "2" + +[dependencies.uuid] +version = "1" +features = ["v4"] +optional = true + +[dev-dependencies.cargo_toml] +version = "1.0" + +[dev-dependencies.http-range] +version = "0.1.5" + +[dev-dependencies.proptest] +version = "1.6.0" + +[dev-dependencies.quickcheck] +version = "1.0.3" + +[dev-dependencies.quickcheck_macros] +version = "1.0.0" + +[dev-dependencies.serde] +version = "1" +features = ["derive"] + +[dev-dependencies.serde_json] +version = "1" + +[dev-dependencies.tokio] +version = "1" +features = ["full"] + +[build-dependencies.glob] +version = "0.3" + +[build-dependencies.heck] +version = "0.5" + +[build-dependencies.sha2] +version = "=0.10.9" + +[build-dependencies.tauri-build] +version = "2.7.1" +default-features = false + +[build-dependencies.tauri-utils] +version = "~2.10.1" +features = ["build-2"] + +[target.'cfg(all(target_vendor = "apple", not(target_os = "macos")))'.dependencies.libc] +version = "0.2" + +[target.'cfg(all(target_vendor = "apple", not(target_os = "macos")))'.dependencies.objc2-ui-kit] +version = "0.3.2" +features = [ + "UIApplication", + "UIResponder", + "UIView", +] +default-features = false + +[target.'cfg(all(target_vendor = "apple", not(target_os = "macos")))'.dependencies.swift-rs] +version = "1.0.8" + +[target.'cfg(any(target_os = "android", all(target_vendor = "apple", not(target_os = "macos"))))'.dependencies.bytes] +version = "1" +features = ["serde"] + +[target.'cfg(any(target_os = "android", all(target_vendor = "apple", not(target_os = "macos"))))'.dependencies.reqwest] +version = "0.13" +features = [ + "json", + "stream", +] +default-features = false + +[target.'cfg(any(target_os = "android", all(target_vendor = "apple", not(target_os = "macos"))))'.dependencies.rustls] +version = "0.23" +features = ["ring"] +optional = true +default-features = false + +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.gtk] +version = "0.18" +features = ["v3_24"] + +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.webkit2gtk] +version = "2" +features = ["v2_40"] +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "windows", target_os = "macos"))'.dependencies.muda] +version = "0.20" +features = [ + "serde", + "gtk3", +] +default-features = false + +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "windows", target_os = "macos"))'.dependencies.tray-icon] +version = "0.25" +features = [ + "serde", + "libappindicator", +] +optional = true +default-features = false + +[target.'cfg(target_os = "android")'.dependencies.jni] +version = "0.21" + +[target.'cfg(target_os = "macos")'.dependencies.embed_plist] +version = "1.2" + +[target.'cfg(target_os = "macos")'.dependencies.objc2-app-kit] +version = "0.3.2" +features = [ + "std", + "NSApplication", + "NSColor", + "NSResponder", + "NSView", + "NSWindow", + "NSImage", +] +default-features = false + +[target.'cfg(target_os = "macos")'.dependencies.objc2-foundation] +version = "0.3.2" +features = [ + "std", + "NSData", + "NSEnumerator", + "NSThread", +] +default-features = false + +[target.'cfg(target_os = "macos")'.dependencies.objc2-web-kit] +version = "0.3.2" +features = [ + "objc2-app-kit", + "WKWebView", + "WKWebViewConfiguration", + "WKUserContentController", +] +default-features = false + +[target.'cfg(target_os = "macos")'.dependencies.plist] +version = "1" + +[target.'cfg(target_os = "macos")'.dependencies.window-vibrancy] +version = "0.8.1" + +[target.'cfg(target_vendor = "apple")'.dependencies.objc2] +version = "0.6" + +[target."cfg(windows)".dependencies.webview2-com] +version = "0.39" +optional = true + +[target."cfg(windows)".dependencies.window-vibrancy] +version = "0.8.1" + +[target."cfg(windows)".dependencies.windows] +version = "0.62" +features = [ + "Win32_Foundation", + "Win32_UI", + "Win32_UI_WindowsAndMessaging", + "Win32_System_LibraryLoader", + "Win32_Graphics_Gdi", +] + +[lints.clippy] +assigning_clones = "warn" +collapsible_if = "allow" +or_fun_call = "warn" +redundant_clone = "warn" diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/Cargo.toml.orig b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/Cargo.toml.orig new file mode 100644 index 000000000..0000939c1 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/Cargo.toml.orig @@ -0,0 +1,279 @@ +[package] +name = "tauri" +version = "2.12.1" +description = "Make tiny, secure apps for all desktop platforms with Tauri" +exclude = ["/test", "/.scripts", "CHANGELOG.md", "/target"] +links = "Tauri" +authors.workspace = true +homepage.workspace = true +repository.workspace = true +categories.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true + +[package.metadata.docs.rs] +no-default-features = true +features = [ + "wry", + "unstable", + "custom-protocol", + "tray-icon", + "devtools", + "image-png", + "protocol-asset", + "test", + "specta", + "dynamic-acl", + "isolation", +] +default-target = "x86_64-unknown-linux-gnu" +targets = [ + "x86_64-pc-windows-msvc", + "x86_64-unknown-linux-gnu", + "x86_64-apple-darwin", + "x86_64-linux-android", + "x86_64-apple-ios", +] + +[package.metadata.cargo-udeps.ignore] +normal = ["reqwest"] +build = ["tauri-build"] +development = ["quickcheck_macros"] + +[dependencies] +serde_json = { version = "1", features = ["raw_value"] } +serde = { version = "1", features = ["derive", "rc"] } +tokio = { version = "1", features = [ + "rt", + "rt-multi-thread", + "sync", + "fs", + "io-util", +] } +uuid = { version = "1", features = ["v4"], optional = true } +url = "2" +anyhow = "1" +thiserror = "2" +tauri-runtime = { workspace = true } +tauri-macros = { workspace = true } +tauri-utils = { workspace = true, features = ["resources"] } +tauri-runtime-wry = { workspace = true, optional = true } +getrandom = "0.3" +serde_repr = "0.1" +http = "1" +dirs = "7" +percent-encoding = "2" +raw-window-handle = { version = "0.6", features = ["std"] } +glob = "0.3" +mime = "0.3" +data-url = { version = "0.3", optional = true } +serialize-to-javascript = "0.1.2" +image = { version = "0.25", default-features = false, optional = true } +http-range = { version = "0.1", optional = true } +tracing = { version = "0.1", optional = true } +heck = "0.5" +log = "0.4.21" +dunce = "1" +specta = { version = "^2.0.0-rc.16", optional = true, default-features = false, features = [ + "function", + "derive", +] } +# WARNING: cookie::Cookie is re-exported so bumping this is a breaking change, documented to be done as a minor bump +cookie = "0.18" + +# desktop +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "windows", target_os = "macos"))'.dependencies] +muda = { version = "0.20", default-features = false, features = [ + "serde", + "gtk3", +] } +tray-icon = { version = "0.25", default-features = false, features = [ + "serde", + "libappindicator", +], optional = true } + +# linux +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies] +gtk = { version = "0.18", features = ["v3_24"] } +webkit2gtk = { version = "2", features = ["v2_40"], optional = true } + +# darwin +[target.'cfg(target_vendor = "apple")'.dependencies] +objc2 = "0.6" + +# macOS +[target.'cfg(target_os = "macos")'.dependencies] +embed_plist = "1.2" +plist = "1" +objc2-foundation = { version = "0.3.2", default-features = false, features = [ + "std", + "NSData", + "NSEnumerator", + "NSThread", +] } +objc2-app-kit = { version = "0.3.2", default-features = false, features = [ + "std", + "NSApplication", + "NSColor", + "NSResponder", + "NSView", + "NSWindow", + "NSImage", +] } +objc2-web-kit = { version = "0.3.2", default-features = false, features = [ + "objc2-app-kit", + "WKWebView", + "WKWebViewConfiguration", + "WKUserContentController", +] } +window-vibrancy = "0.8.1" + +# windows +[target."cfg(windows)".dependencies] +webview2-com = { version = "0.39", optional = true } +window-vibrancy = "0.8.1" +windows = { version = "0.62", features = [ + "Win32_Foundation", + "Win32_UI", + "Win32_UI_WindowsAndMessaging", + "Win32_System_LibraryLoader", + "Win32_Graphics_Gdi", +] } + +# mobile +[target.'cfg(any(target_os = "android", all(target_vendor = "apple", not(target_os = "macos"))))'.dependencies] +bytes = { version = "1", features = ["serde"] } +reqwest = { version = "0.13", default-features = false, features = [ + "json", + "stream", +] } +rustls = { version = "0.23", default-features = false, features = [ + "ring", +], optional = true } + +# android +[target.'cfg(target_os = "android")'.dependencies] +jni = "0.21" + +# UIKit, i.e. iOS/tvOS/watchOS/visionOS +[target.'cfg(all(target_vendor = "apple", not(target_os = "macos")))'.dependencies] +libc = "0.2" +swift-rs = "1.0.8" +objc2-ui-kit = { version = "0.3.2", default-features = false, features = [ + "UIApplication", + "UIResponder", + "UIView", +] } + +[build-dependencies] +glob = "0.3" +heck = "0.5" +tauri-build = { workspace = true } +tauri-utils = { workspace = true, features = ["build-2"] } + +[dev-dependencies] +proptest = "1.6.0" +quickcheck = "1.0.3" +quickcheck_macros = "1.0.0" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tauri = { path = ".", default-features = false, features = ["wry"] } +tokio = { version = "1", features = ["full"] } +cargo_toml = "1.0" +http-range = "0.1.5" + +[features] +default = [ + "wry", + "compression", + "common-controls-v6", + "dynamic-acl", + "x11", + "dbus", +] +unstable = ["tauri-runtime-wry?/unstable"] +x11 = ["tauri-runtime-wry?/x11"] +dbus = ["tauri-runtime-wry?/dbus"] +common-controls-v6 = [ + "tray-icon?/muda-common-controls-v6", + "muda/common-controls-v6", + "tauri-runtime-wry?/common-controls-v6", +] +tray-icon = ["dep:tray-icon"] +tracing = ["dep:tracing", "tauri-macros/tracing", "tauri-runtime-wry?/tracing"] +test = [] +compression = ["tauri-macros/compression", "tauri-utils/compression"] +wry = ["webview2-com", "webkit2gtk", "tauri-runtime-wry"] +# TODO: Remove in v3 - wry does not have this feature anymore +objc-exception = [] +linux-libxdo = ["tray-icon/muda-libxdo", "muda/libxdo"] +isolation = ["tauri-utils/isolation", "tauri-macros/isolation", "uuid"] +custom-protocol = ["tauri-macros/custom-protocol"] +# TODO: Remove these flags in v3 and/or enable them by default behind a mobile flag https://github.com/tauri-apps/tauri/issues/12384 +native-tls = ["reqwest/native-tls"] +native-tls-vendored = ["reqwest/native-tls-vendored"] +rustls-tls = ["reqwest/rustls-no-provider", "dep:rustls"] +devtools = ["tauri-runtime/devtools", "tauri-runtime-wry?/devtools"] +process-relaunch-dangerous-allow-symlink-macos = [ + "tauri-utils/process-relaunch-dangerous-allow-symlink-macos", +] +# TODO: Remove in v3 - wry does not have this feature anymore +macos-private-api = [ + "tauri-runtime/macos-private-api", + "tauri-runtime-wry?/macos-private-api", +] +webview-data-url = ["data-url", "tauri-utils/html-manipulation-2"] +protocol-asset = ["http-range"] +config-json5 = ["tauri-macros/config-json5"] +config-toml = ["tauri-macros/config-toml"] +image-ico = ["image/ico"] +image-png = ["image/png"] +macos-proxy = ["tauri-runtime-wry?/macos-proxy"] +dynamic-acl = [] +specta = ["dep:specta"] + +[[example]] +name = "commands" +path = "../../examples/commands/main.rs" + +[[example]] +name = "helloworld" +path = "../../examples/helloworld/main.rs" + +[[example]] +name = "drag" +path = "../../examples/drag/main.rs" + +[[example]] +name = "multiwebview" +path = "../../examples/multiwebview/main.rs" +required-features = ["unstable"] + +[[example]] +name = "multiwindow" +path = "../../examples/multiwindow/main.rs" + +[[example]] +name = "run-return" +path = "../../examples/run-return/main.rs" + +[[example]] +name = "splashscreen" +path = "../../examples/splashscreen/main.rs" + +[[example]] +name = "state" +path = "../../examples/state/main.rs" + +[[example]] +name = "streaming" +path = "../../examples/streaming/main.rs" + +[[example]] +name = "isolation" +path = "../../examples/isolation/main.rs" +required-features = ["isolation"] + +[lints] +workspace = true diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/LICENSE-APACHE-2.0 b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/LICENSE-APACHE-2.0 new file mode 100644 index 000000000..f433b1a53 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/LICENSE-APACHE-2.0 @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/LICENSE-MIT b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/LICENSE-MIT new file mode 100644 index 000000000..b08530d59 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/README.md b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/README.md new file mode 100644 index 000000000..449653079 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/README.md @@ -0,0 +1,42 @@ +# Tauri + + + +[![status](https://img.shields.io/badge/status-stable-blue.svg)](https://github.com/tauri-apps/tauri/tree/dev) +[![License](https://img.shields.io/badge/License-MIT%20or%20Apache%202-green.svg)](https://opencollective.com/tauri) +[![test core](https://img.shields.io/github/actions/workflow/status/tauri-apps/tauri/test-core.yml?label=test%20core&logo=github)](https://github.com/tauri-apps/tauri/actions/workflows/test-core.yml) +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Ftauri-apps%2Ftauri.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Ftauri-apps%2Ftauri?ref=badge_shield) +[![Chat Server](https://img.shields.io/badge/chat-discord-7289da.svg)](https://discord.gg/SpmNs4S) +[![website](https://img.shields.io/badge/website-tauri.app-purple.svg)](https://tauri.app) +[![https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg](https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg)](https://good-labs.github.io/greater-good-affirmation) +[![support](https://img.shields.io/badge/sponsor-Open%20Collective-blue.svg)](https://opencollective.com/tauri) + +| Component | Version | +| --------- | ---------------------------------------------------------------------------------------------- | +| tauri | [![](https://img.shields.io/crates/v/tauri?style=flat-square)](https://crates.io/crates/tauri) | + +## About Tauri + +Tauri is a polyglot and generic system that is very composable and allows engineers to make a wide variety of applications. It is used for building applications for Desktop Computers using a combination of Rust tools and HTML rendered in a Webview. Apps built with Tauri can ship with any number of pieces of an optional JS API / Rust API so that webviews can control the system via message passing. In fact, developers can extend the default API with their own functionality and bridge the Webview and Rust-based backend easily. + +Tauri apps can have custom menus and have tray-type interfaces. They can be updated, and are managed by the user's operating system as expected. They are very small, because they use the system's webview. They do not ship a runtime, since the final binary is compiled from rust. This makes the reversing of Tauri apps not a trivial task. + +## This module + +This is the glue crate that holds everything together. It brings the runtimes, macros, utilities and API into one final product. It reads the `tauri.conf.json` file at compile time in order to bring in features and undertake actual configuration of the app (and even the `Cargo.toml` file in the project's folder). It handles script injection (for polyfills / prototype revision) at runtime, hosts the API for systems interaction, and even manages updating. + +To learn more about the details of how all of these pieces fit together, please consult this [ARCHITECTURE.md](https://github.com/tauri-apps/tauri/blob/dev/ARCHITECTURE.md) document. + +## Semver + +**tauri** is following [Semantic Versioning 2.0](https://semver.org/). + +## Licenses + +Code: (c) 2019 - 2021 - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. + +Logo: CC-BY-NC-ND + +- Original Tauri Logo Designs by [Daniel Thompson-Yvetot](https://github.com/nothingismagick) and [Guillaume Chau](https://github.com/akryum) diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/build.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/build.rs new file mode 100644 index 000000000..90c9979d1 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/build.rs @@ -0,0 +1,505 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use heck::AsShoutySnakeCase; +use tauri_utils::write_if_changed; + +mod codenomad_ios_abi_guard; + +use std::{ + collections::BTreeMap, + env, fs, + path::{Path, PathBuf}, +}; + +const PLUGINS: &[(&str, &[(&str, bool)])] = &[ + // (plugin_name, &[(command, enabled-by_default)]) + // TODO: Enable this in v3 + // ("core:channel", &[("fetch", true)]), + ( + "core:path", + &[ + ("resolve_directory", true), + ("resolve", true), + ("normalize", true), + ("join", true), + ("dirname", true), + ("extname", true), + ("basename", true), + ("is_absolute", true), + ], + ), + ( + "core:event", + &[ + ("listen", true), + ("unlisten", true), + ("emit", true), + ("emit_to", true), + ], + ), + ( + "core:window", + &[ + ("create", false), + // getters + ("get_all_windows", true), + ("scale_factor", true), + ("inner_position", true), + ("outer_position", true), + ("inner_size", true), + ("outer_size", true), + ("is_fullscreen", true), + ("is_minimized", true), + ("is_maximized", true), + ("is_focused", true), + ("is_decorated", true), + ("is_resizable", true), + ("is_maximizable", true), + ("is_minimizable", true), + ("is_closable", true), + ("is_visible", true), + ("is_enabled", true), + ("title", true), + ("current_monitor", true), + ("primary_monitor", true), + ("monitor_from_point", true), + ("available_monitors", true), + ("cursor_position", true), + ("theme", true), + ("is_always_on_top", true), + ("activity_name", true), + ("scene_identifier", true), + // setters + ("center", false), + ("request_user_attention", false), + ("set_enabled", false), + ("set_resizable", false), + ("set_maximizable", false), + ("set_minimizable", false), + ("set_closable", false), + ("set_title", false), + ("maximize", false), + ("unmaximize", false), + ("minimize", false), + ("unminimize", false), + ("show", false), + ("hide", false), + ("close", false), + ("destroy", false), + ("set_decorations", false), + ("set_shadow", false), + ("set_effects", false), + ("set_always_on_top", false), + ("set_always_on_bottom", false), + ("set_visible_on_all_workspaces", false), + ("set_content_protected", false), + ("set_size", false), + ("set_min_size", false), + ("set_size_constraints", false), + ("set_max_size", false), + ("set_position", false), + ("set_fullscreen", false), + ("set_fullscreen_on_monitor", false), + ("set_simple_fullscreen", false), + ("set_focus", false), + ("set_focusable", false), + ("set_skip_taskbar", false), + ("set_cursor_grab", false), + ("set_cursor_visible", false), + ("set_cursor_icon", false), + ("set_cursor_position", false), + ("set_ignore_cursor_events", false), + ("start_dragging", false), + ("start_resize_dragging", false), + ("set_progress_bar", false), + ("set_badge_count", false), + ("set_overlay_icon", false), + ("set_badge_label", false), + ("set_icon", false), + ("set_title_bar_style", false), + ("set_theme", false), + ("toggle_maximize", false), + ("set_background_color", false), + // internal + ("internal_toggle_maximize", true), + ], + ), + ( + "core:webview", + &[ + ("create_webview", false), + ("create_webview_window", false), + // getters + ("get_all_webviews", true), + ("webview_position", true), + ("webview_size", true), + // setters + ("webview_close", false), + ("set_webview_size", false), + ("set_webview_position", false), + ("set_webview_focus", false), + ("set_webview_auto_resize", false), + ("set_webview_zoom", false), + ("webview_hide", false), + ("webview_show", false), + ("print", false), + ("reparent", false), + ("clear_all_browsing_data", false), + ("set_webview_background_color", false), + // internal + ("internal_toggle_devtools", true), + ], + ), + ( + "core:app", + &[ + ("version", true), + ("name", true), + ("tauri_version", true), + ("identifier", true), + ("app_show", false), + ("app_hide", false), + ("exit", false), + ("fetch_data_store_identifiers", false), + ("remove_data_store", false), + ("default_window_icon", false), + ("set_app_theme", false), + ("set_dock_visibility", false), + ("bundle_type", true), + ("register_listener", true), + ("remove_listener", true), + ("supports_multiple_windows", true), + ], + ), + ( + "core:image", + &[ + ("new", true), + ("from_bytes", true), + ("from_path", true), + ("rgba", true), + ("size", true), + ], + ), + ("core:resources", &[("close", true)]), + ( + "core:menu", + &[ + ("new", true), + ("append", true), + ("prepend", true), + ("insert", true), + ("remove", true), + ("remove_at", true), + ("items", true), + ("get", true), + ("popup", true), + ("create_default", true), + ("set_as_app_menu", true), + ("set_as_window_menu", true), + ("text", true), + ("set_text", true), + ("is_enabled", true), + ("set_enabled", true), + ("set_accelerator", true), + ("set_as_windows_menu_for_nsapp", true), + ("set_as_help_menu_for_nsapp", true), + ("is_checked", true), + ("set_checked", true), + ("set_icon", true), + ], + ), + ( + "core:tray", + &[ + ("new", true), + ("get_by_id", true), + ("remove_by_id", true), + ("set_icon", true), + ("set_menu", true), + ("set_tooltip", true), + ("set_title", true), + ("set_visible", true), + ("set_temp_dir_path", true), + ("set_icon_as_template", true), + ("set_icon_with_as_template", true), + ("set_show_menu_on_left_click", true), + ], + ), +]; + +// checks if the given Cargo feature is enabled. +fn has_feature(checked_features: &mut Vec<&'static str>, feature: &'static str) -> bool { + checked_features.push(feature); + // when a feature is enabled, Cargo sets the `CARGO_FEATURE_` env var to 1 + // + std::env::var_os(format!("CARGO_FEATURE_{}", AsShoutySnakeCase(feature))) + .map(|x| x == "1") + .unwrap_or(false) +} + +// creates a cfg alias if `has_feature` is true. +// `alias` must be a snake case string. +fn alias(alias: &str, has_feature: bool) { + println!("cargo:rustc-check-cfg=cfg({alias})"); + if has_feature { + println!("cargo:rustc-cfg={alias}"); + } +} + +fn main() { + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("ios") { + codenomad_ios_abi_guard::verify(); + } + let mut checked_features = Vec::new(); + let custom_protocol = has_feature(&mut checked_features, "custom-protocol"); + let dev = !custom_protocol; + alias("custom_protocol", custom_protocol); + alias("dev", dev); + + println!("cargo:dev={dev}"); + + let target_os = std::env::var_os("CARGO_CFG_TARGET_OS").unwrap(); + let mobile = target_os == "ios" || target_os == "android"; + alias("desktop", !mobile); + alias("mobile", mobile); + + let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); + + let checked_features_out_path = out_dir.join("checked_features"); + // file is used by a unit test in src/lib.rs to check that the features checked do exist in the Cargo.toml + std::fs::write(checked_features_out_path, checked_features.join(",")) + .expect("failed to write checked_features file"); + + // workaround needed to prevent `STATUS_ENTRYPOINT_NOT_FOUND` error in tests + // see https://github.com/tauri-apps/tauri/pull/4383#issuecomment-1212221864 + let target_env = std::env::var_os("CARGO_CFG_TARGET_ENV"); + let is_tauri_workspace = std::env::var_os("__TAURI_WORKSPACE__").is_some_and(|v| v == "true"); + if is_tauri_workspace && target_os == "windows" && target_env.is_some_and(|trgt| trgt == "msvc") { + embed_manifest_for_tests(); + } + + if target_os == "android" { + fn env_var(var: &str) -> String { + std::env::var(var).unwrap_or_else(|_| { + panic!("`{var}` is not set, which is needed to generate the kotlin files for android.") + }) + } + + if let Some(kotlin_out_dir) = std::env::var_os("WRY_ANDROID_KOTLIN_FILES_OUT_DIR") { + let package = env_var("WRY_ANDROID_PACKAGE"); + let library = env_var("WRY_ANDROID_LIBRARY"); + + let kotlin_out_dir = PathBuf::from(&kotlin_out_dir) + .canonicalize() + .unwrap_or_else(move |_| { + panic!( + "Failed to canonicalize `WRY_ANDROID_KOTLIN_FILES_OUT_DIR` path {kotlin_out_dir:?}" + ) + }); + + let kotlin_files_path = + PathBuf::from(env_var("CARGO_MANIFEST_DIR")).join("mobile/android-codegen"); + println!("cargo:rerun-if-changed={}", kotlin_files_path.display()); + let kotlin_files = + fs::read_dir(kotlin_files_path).expect("failed to read Android codegen directory"); + + for file in kotlin_files { + let file = file.unwrap(); + + let content = fs::read_to_string(file.path()) + .expect("failed to read kotlin file as string") + .replace("{{package}}", &package) + .replace("{{library}}", &library); + + let out_path = kotlin_out_dir.join(file.file_name()); + // Overwrite only if changed to not trigger rebuilds + write_if_changed(&out_path, &content).expect("Failed to write kotlin file"); + + println!("cargo:rerun-if-changed={}", out_path.display()); + } + } + + if let Some(project_dir) = env::var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) { + let package_unescaped = env::var("TAURI_ANDROID_PACKAGE_UNESCAPED") + .unwrap_or_else(|_| env_var("WRY_ANDROID_PACKAGE").replace('`', "")); + let tauri_proguard = + include_str!("./mobile/proguard-tauri.pro").replace("$PACKAGE", &package_unescaped); + std::fs::write( + project_dir.join("app").join("proguard-tauri.pro"), + tauri_proguard, + ) + .expect("failed to write proguard-tauri.pro"); + } + + let lib_path = + PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap()).join("mobile/android"); + println!("cargo:android_library_path={}", lib_path.display()); + } + + #[cfg(target_os = "macos")] + { + if target_os == "ios" { + let lib_path = + PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap()).join("mobile/ios-api"); + tauri_utils::build::link_apple_library("Tauri", &lib_path); + println!("cargo:ios_library_path={}", lib_path.display()); + } + } + + let tauri_global_scripts = PathBuf::from("./scripts/bundle.global.js") + .canonicalize() + .expect("failed to canonicalize tauri global API script path"); + tauri_utils::plugin::define_global_api_script_path(&tauri_global_scripts); + // This should usually be done in `tauri-build`, + // but we need to do this here for the examples in this workspace to work as they don't have build scripts + if is_tauri_workspace { + tauri_utils::plugin::save_global_api_scripts_paths(&out_dir, Some(tauri_global_scripts)); + } + + let permissions = define_permissions(&out_dir); + tauri_utils::acl::build::generate_allowed_commands(&out_dir, None, permissions).unwrap(); +} + +const LICENSE_HEADER: &str = r"# Copyright 2019-2024 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT +"; + +fn define_permissions( + out_dir: &Path, +) -> BTreeMap> { + let mut all_permissions = BTreeMap::new(); + for (plugin, commands) in PLUGINS { + let plugin_directory_name = plugin.strip_prefix("core:").unwrap_or(plugin); + let permissions_out_dir = out_dir.join("permissions").join(plugin_directory_name); + let autogenerated = + permissions_out_dir.join(tauri_utils::acl::build::AUTOGENERATED_FOLDER_NAME); + let commands_dir = autogenerated.join("commands"); + + tauri_utils::acl::build::autogenerate_command_permissions( + &commands_dir, + &commands.iter().map(|(cmd, _)| *cmd).collect::>(), + LICENSE_HEADER, + false, + ); + let default_permissions: Vec<_> = commands.iter().filter(|(_cmd, default)| *default).collect(); + let all_commands_enabled_by_default = commands.len() == default_permissions.len(); + let default_permissions = default_permissions + .into_iter() + .map(|(cmd, _)| { + let slugified_command = cmd.replace('_', "-"); + format!("\"allow-{slugified_command}\"") + }) + .collect::>() + .join(", "); + + let all_enable_by_default = if all_commands_enabled_by_default { + ", which enables all commands" + } else { + "" + }; + + let default_toml = format!( + r#"{LICENSE_HEADER}# Automatically generated - DO NOT EDIT! + +[default] +description = "Default permissions for the plugin{all_enable_by_default}." +permissions = [{default_permissions}] +"#, + ); + + let out_path = autogenerated.join("default.toml"); + write_if_changed(out_path, default_toml) + .unwrap_or_else(|_| panic!("unable to autogenerate default permissions")); + + let permissions = tauri_utils::acl::build::define_permissions( + &PathBuf::from(glob::Pattern::escape( + &permissions_out_dir.to_string_lossy(), + )) + .join("**") + .join("*.toml") + .to_string_lossy(), + &format!("tauri:{plugin}"), + out_dir, + |_| true, + ) + .unwrap_or_else(|e| panic!("failed to define permissions for {plugin}: {e}")); + + let docs_out_dir = Path::new("permissions") + .join(plugin_directory_name) + .join("autogenerated"); + fs::create_dir_all(&docs_out_dir).expect("failed to create plugin documentation directory"); + tauri_utils::acl::build::generate_docs( + &permissions, + &docs_out_dir, + plugin.strip_prefix("tauri-plugin-").unwrap_or(plugin), + ) + .expect("failed to generate plugin documentation page"); + all_permissions.insert(plugin.to_string(), permissions); + } + + let default_permissions = define_default_permission_set(out_dir); + all_permissions.insert("core".to_string(), default_permissions); + + all_permissions +} + +fn define_default_permission_set( + out_dir: &Path, +) -> Vec { + let permissions_out_dir = out_dir.join("permissions"); + fs::create_dir_all(&permissions_out_dir) + .expect("failed to create core:default permissions directory"); + + let default_toml = permissions_out_dir.join("default.toml"); + let toml_content = format!( + r#"{LICENSE_HEADER} + +[default] +description = "Default core plugins set." +permissions = [{}] +"#, + PLUGINS + .iter() + .map(|(k, _)| format!("\"{k}:default\"")) + .collect::>() + .join(",") + ); + + write_if_changed(default_toml, toml_content) + .unwrap_or_else(|_| panic!("unable to autogenerate core:default set")); + + tauri_utils::acl::build::define_permissions( + &PathBuf::from(glob::Pattern::escape( + &permissions_out_dir.to_string_lossy(), + )) + .join("*.toml") + .to_string_lossy(), + "tauri:core", + out_dir, + |_| true, + ) + .unwrap_or_else(|e| panic!("failed to define permissions for `core:default` : {e}")) +} + +fn embed_manifest_for_tests() { + static WINDOWS_MANIFEST_FILE: &str = "windows-app-manifest.xml"; + + let manifest = std::env::current_dir() + .unwrap() + .join("../tauri-build/src") + .join(WINDOWS_MANIFEST_FILE); + + println!("cargo:rerun-if-changed={}", manifest.display()); + // Embed the Windows application manifest file. + println!("cargo:rustc-link-arg=/MANIFEST:EMBED"); + println!( + "cargo:rustc-link-arg=/MANIFESTINPUT:{}", + manifest.to_str().unwrap() + ); + // Turn linker warnings into errors. + println!("cargo:rustc-link-arg=/WX"); +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/codenomad-ios-abi.sha256 b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/codenomad-ios-abi.sha256 new file mode 100644 index 000000000..fe84431e6 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/codenomad-ios-abi.sha256 @@ -0,0 +1,13 @@ +b529ac117054acd98fe38f554460efba5acb7f77d6e7c9ae88df832bb9927f9d Cargo.toml +82903f5593c97180dcf89d6be4d1aa42f771a400cf0b14406256417b7c3f31e8 build.rs +c5dec514616e24eb39419985f48de428449eb9a4e46884a98130c6c35f938c05 src/ios.rs +5079199081a851b68a8caec5696f0703aecf79786497700ba5b62bfa2781013d src/plugin/mobile.rs +77d1eb6e485cdd6e119f98a29624fbb676f8d79df87153fcdde8ef7ffbe860b6 mobile/ios-api/Package.swift +850e8239ccb561ea7177f0f66b33d5b63e97faeefcb79b60640007934c3894a4 mobile/ios-api/Sources/Tauri/Channel.swift +afc9ee63daafe1754969b6dfd104d49d2ca09fd41e7c6343063c13d9e56f7d3e mobile/ios-api/Sources/Tauri/Invoke.swift +a22a4bad5788132955a63160bda00eeb62a6e138993d46504f6f96e2fb94246d mobile/ios-api/Sources/Tauri/JsonValue.swift +bce9da78d74d495a2d05176cb0b68bd9a40ebfcac8c8e9cb1678d4ca142bd653 mobile/ios-api/Sources/Tauri/JSTypes.swift +9624719792f6ce0baf6df0aaf2928efc8c0aef77972a3fb638ce67e3a6dfb918 mobile/ios-api/Sources/Tauri/Logger.swift +e5d2e0e6f6c178b0ea2745dcd88f68b6a142154c3b245845bb6b9026b3a76d69 mobile/ios-api/Sources/Tauri/Plugin/Plugin.swift +595f7c8bb38e3e29745f2728d356463f87f732f2c3805cf038cfba23248d2433 mobile/ios-api/Sources/Tauri/Tauri.swift +c12b2a5a3bde289c45dd6c348cc41d7072c81a1559eac76339ea061c4fa5d89c mobile/ios-api/Sources/Tauri/UiUtils.swift diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/codenomad_ios_abi_guard.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/codenomad_ios_abi_guard.rs new file mode 100644 index 000000000..27a74e050 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/codenomad_ios_abi_guard.rs @@ -0,0 +1,24 @@ +// CodeNomad local integration guard; upstream Tauri is MIT OR Apache-2.0. +// Runs before the upstream iOS Swift library linker, including direct Cargo builds. +use sha2::{Digest, Sha256}; +use std::{env, fs, path::Path}; + +pub fn verify() { + let root = env::var("CARGO_MANIFEST_DIR").expect("Tauri manifest directory"); + let version = env::var("CARGO_PKG_VERSION").expect("Tauri version"); + verify_at(Path::new(&root), &version); +} + +pub fn verify_at(root: &Path, version: &str) { + assert_eq!(version, "2.12.1", "CodeNomad iOS ABI patch version drift"); + for line in include_str!("codenomad-ios-abi.sha256").lines() { + let (expected, relative) = line.split_once(" ").expect("ABI hash receipt"); + let path = root.join(relative); + println!("cargo:rerun-if-changed={}", path.display()); + let bytes = fs::read(&path).expect("CodeNomad iOS ABI input missing"); + let actual = format!("{:x}", Sha256::digest(&bytes)); + assert_eq!(actual, expected, "CodeNomad iOS ABI input drift: {relative}"); + } + println!("cargo:rerun-if-changed=codenomad_ios_abi_guard.rs"); + println!("cargo:rerun-if-changed=codenomad-ios-abi.sha256"); +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android-codegen/TauriActivity.kt b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android-codegen/TauriActivity.kt new file mode 100644 index 000000000..0cfbe3f51 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android-codegen/TauriActivity.kt @@ -0,0 +1,60 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */ + +package {{package}} + +import android.content.Intent +import android.content.res.Configuration +import android.os.Bundle +import app.tauri.plugin.PluginManager + +abstract class TauriActivity : WryActivity() { + override val handleBackNavigation: Boolean = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + PluginManager.onCreate(this) + } + + fun getPluginManager(): PluginManager { + return PluginManager + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + PluginManager.onNewIntent(intent) + } + + override fun onRestart() { + super.onRestart() + PluginManager.onRestart(this) + } + + override fun onResume() { + super.onResume() + PluginManager.onResume(this) + } + + override fun onPause() { + super.onPause() + PluginManager.onPause(this) + } + + override fun onStop() { + super.onStop() + PluginManager.onStop(this) + } + + override fun onDestroy() { + super.onDestroy() + PluginManager.onDestroy(this) + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + PluginManager.onConfigurationChanged(newConfig) + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/.gitignore b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/build.gradle.kts b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/build.gradle.kts new file mode 100644 index 000000000..124c36a7a --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/build.gradle.kts @@ -0,0 +1,48 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "app.tauri" + compileSdk = 36 + + defaultConfig { + minSdk = 21 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + buildFeatures { + buildConfig = true + } +} + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_1_8 + } +} + +dependencies { + + implementation("androidx.core:core-ktx:1.7.0") + implementation("androidx.appcompat:appcompat:1.6.0") + implementation("com.google.android.material:material:1.7.0") + implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3") + // (de)serializes org.json values (JSObject, JSArray) as JSON objects and arrays + // instead of as beans; org.json itself is part of the Android platform + implementation("com.fasterxml.jackson.datatype:jackson-datatype-json-org:2.15.3") { + exclude(group = "org.json", module = "json") + } + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/consumer-rules.pro b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/consumer-rules.pro new file mode 100644 index 000000000..e21b5beb4 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/consumer-rules.pro @@ -0,0 +1,41 @@ +-keep class app.tauri.** { + @app.tauri.JniMethod public ; + native ; +} + +-keep class app.tauri.plugin.JSArray { + public (...); +} + +-keepclassmembers class org.json.JSONArray { + public put(...); +} + +-keep class app.tauri.plugin.JSObject { + public (...); + public put(...); +} + +-keep @app.tauri.annotation.TauriPlugin public class * { + @app.tauri.annotation.Command public ; + @app.tauri.annotation.PermissionCallback ; + @app.tauri.annotation.ActivityCallback ; + @app.tauri.annotation.Permission ; + public (...); +} + +-keep @app.tauri.annotation.InvokeArg public class * { + *; +} + +-keep @com.fasterxml.jackson.databind.annotation.JsonDeserialize public class * { + *; +} + +-keep @com.fasterxml.jackson.databind.annotation.JsonSerialize public class * { + *; +} + +-keep class * extends com.fasterxml.jackson.databind.JsonDeserializer { *; } + +-keep class * extends com.fasterxml.jackson.databind.JsonSerializer { *; } diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/androidTest/java/app/tauri/ExampleInstrumentedTest.kt b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/androidTest/java/app/tauri/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..77ce904cf --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/androidTest/java/app/tauri/ExampleInstrumentedTest.kt @@ -0,0 +1,28 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("app.tauri.test", appContext.packageName) + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/AndroidManifest.xml b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..9a40236b9 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/AppPlugin.kt b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/AppPlugin.kt new file mode 100644 index 000000000..7bbd09ab4 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/AppPlugin.kt @@ -0,0 +1,56 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri + +import android.app.Activity +import android.webkit.WebView +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AppCompatActivity +import app.tauri.annotation.Command +import app.tauri.annotation.TauriPlugin +import app.tauri.plugin.Plugin +import app.tauri.plugin.Invoke +import app.tauri.plugin.JSObject + +@TauriPlugin +class AppPlugin(private val activity: Activity): Plugin(activity) { + private val BACK_BUTTON_EVENT = "back-button" + + private var webView: WebView? = null + + override fun load(webView: WebView) { + this.webView = webView + } + + init { + val callback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (!hasListener(BACK_BUTTON_EVENT)) { + if (this@AppPlugin.webView?.canGoBack() == true) { + this@AppPlugin.webView!!.goBack() + } else { + this.isEnabled = false + this@AppPlugin.activity.onBackPressed() + this.isEnabled = true + } + } else { + val data = JSObject().apply { + put("canGoBack", this@AppPlugin.webView?.canGoBack() ?: false) + } + trigger(BACK_BUTTON_EVENT, data) + } + } + } + (activity as AppCompatActivity).onBackPressedDispatcher.addCallback(activity, callback) + } + + // Called by the Rust side when the app exits so the activity (and any other activity + // of the task, i.e. other windows) is closed gracefully instead of the process being killed + @Command + fun exit(invoke: Invoke) { + invoke.resolve() + activity.finishAffinity() + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/FsUtils.kt b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/FsUtils.kt new file mode 100644 index 000000000..235775cb1 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/FsUtils.kt @@ -0,0 +1,208 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri + +import android.content.ContentUris +import android.content.Context +import android.content.res.AssetManager +import android.database.Cursor +import android.net.Uri +import android.os.Environment +import android.provider.DocumentsContract +import android.provider.MediaStore +import android.provider.OpenableColumns +import java.io.File +import java.io.FileOutputStream +import kotlin.math.min + +internal class FsUtils { + companion object { + fun readAsset(assetManager: AssetManager, fileName: String): String { + assetManager.open(fileName).bufferedReader().use { + return it.readText() + } + } + + fun getFileUrlForUri(context: Context, uri: Uri): String? { + // DocumentProvider + if (DocumentsContract.isDocumentUri(context, uri)) { + // ExternalStorageProvider + if (isExternalStorageDocument(uri)) { + val docId: String = DocumentsContract.getDocumentId(uri) + val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + val type = split[0] + if ("primary".equals(type, ignoreCase = true)) { + return legacyPrimaryPath(split[1]) + } else { + val splitIndex = docId.indexOf(':', 1) + val tag = docId.substring(0, splitIndex) + val path = docId.substring(splitIndex + 1) + val nonPrimaryVolume = getPathToNonPrimaryVolume(context, tag) + if (nonPrimaryVolume != null) { + val result = "$nonPrimaryVolume/$path" + val file = File(result) + return if (file.exists() && file.canRead()) { + result + } else null + } + } + } else if (isDownloadsDocument(uri)) { + val id: String = DocumentsContract.getDocumentId(uri) + val contentUri: Uri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), + java.lang.Long.valueOf(id) + ) + return getDataColumn(context, contentUri, null, null) + } else if (isMediaDocument(uri)) { + val docId: String = DocumentsContract.getDocumentId(uri) + val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + val type = split[0] + var contentUri: Uri? = null + when (type) { + "image" -> { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI + } + "video" -> { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI + } + "audio" -> { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + } + } + val selection = "_id=?" + val selectionArgs = arrayOf(split[1]) + if (contentUri != null) { + return getDataColumn(context, contentUri, selection, selectionArgs) + } + } + } else if ("content".equals(uri.scheme, ignoreCase = true)) { + // Return the remote address + return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn( + context, + uri, + null, + null + ) + } else if ("file".equals(uri.scheme, ignoreCase = true)) { + return uri.path + } + return null + } + + /** + * Get the value of the data column for this Uri. This is useful for + * MediaStore Uris, and other file-based ContentProviders. + * + * @param context The context. + * @param uri The Uri to query. + * @param selection (Optional) Filter used in the query. + * @param selectionArgs (Optional) Selection arguments used in the query. + * @return The value of the _data column, which is typically a file path. + */ + private fun getDataColumn( + context: Context, + uri: Uri, + selection: String?, + selectionArgs: Array? + ): String? { + var path: String? = null + var cursor: Cursor? = null + val column = "_data" + val projection = arrayOf(column) + try { + cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null) + if (cursor != null && cursor.moveToFirst()) { + val index = cursor.getColumnIndexOrThrow(column) + path = cursor.getString(index) + } + } catch (ex: IllegalArgumentException) { + return getCopyFilePath(uri, context) + } finally { + cursor?.close() + } + return path ?: getCopyFilePath(uri, context) + } + + private fun getCopyFilePath(uri: Uri, context: Context): String? { + val cursor = context.contentResolver.query(uri, null, null, null, null)!! + val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + cursor.moveToFirst() + val name = cursor.getString(nameIndex) + val file = File(context.filesDir, name) + try { + val inputStream = context.contentResolver.openInputStream(uri) + val outputStream = FileOutputStream(file) + var read: Int + val maxBufferSize = 1024 * 1024 + val bufferSize = min(inputStream!!.available(), maxBufferSize) + val buffers = ByteArray(bufferSize) + while (inputStream.read(buffers).also { read = it } != -1) { + outputStream.write(buffers, 0, read) + } + inputStream.close() + outputStream.close() + } catch (e: Exception) { + return null + } finally { + cursor.close() + } + return file.path + } + + private fun legacyPrimaryPath(pathPart: String): String { + return Environment.getExternalStorageDirectory().toString() + "/" + pathPart + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + */ + private fun isExternalStorageDocument(uri: Uri): Boolean { + return "com.android.externalstorage.documents" == uri.authority + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + */ + private fun isDownloadsDocument(uri: Uri): Boolean { + return "com.android.providers.downloads.documents" == uri.authority + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + */ + private fun isMediaDocument(uri: Uri): Boolean { + return "com.android.providers.media.documents" == uri.authority + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is Google Photos. + */ + private fun isGooglePhotosUri(uri: Uri): Boolean { + return "com.google.android.apps.photos.content" == uri.authority + } + + private fun getPathToNonPrimaryVolume(context: Context, tag: String): String? { + val volumes = context.externalCacheDirs + if (volumes != null) { + for (volume in volumes) { + if (volume != null) { + val path = volume.absolutePath + val index = path.indexOf(tag) + if (index != -1) { + return path.substring(0, index) + tag + } + } + } + } + return null + } + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/JniMethod.kt b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/JniMethod.kt new file mode 100644 index 000000000..41301d407 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/JniMethod.kt @@ -0,0 +1,8 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri + +@Retention(AnnotationRetention.RUNTIME) +internal annotation class JniMethod diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/Logger.kt b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/Logger.kt new file mode 100644 index 000000000..c0473788b --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/Logger.kt @@ -0,0 +1,85 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri + +// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/Logger.java + +import android.text.TextUtils; +import android.util.Log; + +class Logger { + companion object { + private const val LOG_TAG_CORE = "Tauri" + + fun tags(vararg subtags: String): String { + return if (subtags.isNotEmpty()) { + LOG_TAG_CORE + "/" + TextUtils.join("/", subtags) + } else LOG_TAG_CORE + } + + fun verbose(message: String) { + verbose(LOG_TAG_CORE, message) + } + + fun verbose(tag: String, message: String) { + if (!shouldLog()) { + return + } + Log.v(tag, message) + } + + fun debug(message: String) { + debug(LOG_TAG_CORE, message) + } + + fun debug(tag: String, message: String) { + if (!shouldLog()) { + return + } + Log.d(tag, message) + } + + fun info(message: String) { + info(LOG_TAG_CORE, message) + } + + fun info(tag: String, message: String) { + if (!shouldLog()) { + return + } + Log.i(tag, message) + } + + fun warn(message: String) { + warn(LOG_TAG_CORE, message) + } + + fun warn(tag: String, message: String) { + if (!shouldLog()) { + return + } + Log.w(tag, message) + } + + fun error(message: String) { + error(LOG_TAG_CORE, message, null) + } + + fun error(message: String, e: Throwable?) { + error(LOG_TAG_CORE, message, e) + } + + fun error(tag: String, message: String, e: Throwable?) { + if (!shouldLog()) { + return + } + Log.e(tag, message, e) + } + + private fun shouldLog(): Boolean { + return BuildConfig.DEBUG + } + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/PathPlugin.kt b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/PathPlugin.kt new file mode 100644 index 000000000..7b2f38d1b --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/PathPlugin.kt @@ -0,0 +1,132 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri + +import android.app.Activity +import android.database.Cursor +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.OpenableColumns +import app.tauri.annotation.Command +import app.tauri.annotation.InvokeArg +import app.tauri.annotation.TauriPlugin +import app.tauri.plugin.Plugin +import app.tauri.plugin.Invoke +import app.tauri.plugin.JSObject + +const val TAURI_ASSETS_DIRECTORY_URI = "asset://localhost/" + +@InvokeArg +class GetFileNameFromUriArgs { + lateinit var uri: String +} + +@TauriPlugin +class PathPlugin(private val activity: Activity): Plugin(activity) { + private fun resolvePath(invoke: Invoke, path: String?) { + val obj = JSObject() + obj.put("path", path) + invoke.resolve(obj) + } + + @Command + fun getFileNameFromUri(invoke: Invoke) { + val args = invoke.parseArgs(GetFileNameFromUriArgs::class.java) + val name = getRealNameFromURI(activity, Uri.parse(args.uri)) + val res = JSObject() + res.put("name", name) + invoke.resolve(res) + } + + @Command + fun getAudioDir(invoke: Invoke) { + resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_MUSIC)?.absolutePath) + } + + @Command + fun getExternalCacheDir(invoke: Invoke) { + resolvePath(invoke, activity.externalCacheDir?.absolutePath) + } + + @Command + fun getConfigDir(invoke: Invoke) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + resolvePath(invoke, activity.dataDir.absolutePath) + } else { + resolvePath(invoke, activity.applicationInfo.dataDir) + } + } + + @Command + fun getDataDir(invoke: Invoke) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + resolvePath(invoke, activity.dataDir.absolutePath) + } else { + resolvePath(invoke, activity.applicationInfo.dataDir) + } + } + + @Command + fun getDocumentDir(invoke: Invoke) { + resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)?.absolutePath) + } + + @Command + fun getDownloadDir(invoke: Invoke) { + resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath) + } + + @Command + fun getPictureDir(invoke: Invoke) { + resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES)?.absolutePath) + } + + @Command + fun getPublicDir(invoke: Invoke) { + resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_DCIM)?.absolutePath) + } + + @Command + fun getVideoDir(invoke: Invoke) { + resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_MOVIES)?.absolutePath) + } + + @Command + fun getResourcesDir(invoke: Invoke) { + resolvePath(invoke, TAURI_ASSETS_DIRECTORY_URI) + } + + @Command + fun getCacheDir(invoke: Invoke) { + resolvePath(invoke, activity.cacheDir.absolutePath) + } + + @Command + fun getHomeDir(invoke: Invoke) { + resolvePath(invoke, Environment.getExternalStorageDirectory().absolutePath) + } +} + +fun getRealNameFromURI(activity: Activity, contentUri: Uri): String? { + var cursor: Cursor? = null + try { + val projection = arrayOf(OpenableColumns.DISPLAY_NAME) + cursor = activity.contentResolver.query(contentUri, projection, null, null, null) + + cursor?.let { + val columnIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME) + if (it.moveToFirst()) { + return it.getString(columnIndex) + } + } + } catch (e: Exception) { + Logger.error("failed to get real name from URI $e") + } finally { + cursor?.close() + } + + return null // Return null if no file name could be resolved +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/PermissionHelper.kt b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/PermissionHelper.kt new file mode 100644 index 000000000..d9a5cd489 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/PermissionHelper.kt @@ -0,0 +1,107 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri + +// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/PermissionHelper.java + +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; +import androidx.core.app.ActivityCompat; +import java.util.ArrayList; + +object PermissionHelper { + /** + * Checks if a list of given permissions are all granted by the user + * + * @param permissions Permissions to check. + * @return True if all permissions are granted, false if at least one is not. + */ + fun hasPermissions(context: Context?, permissions: Array): Boolean { + for (perm in permissions) { + if (ActivityCompat.checkSelfPermission( + context!!, + perm + ) != PackageManager.PERMISSION_GRANTED + ) { + return false + } + } + return true + } + + /** + * Check whether the given permission has been defined in the AndroidManifest.xml + * + * @param permission A permission to check. + * @return True if the permission has been defined in the Manifest, false if not. + */ + fun hasDefinedPermission(context: Context, permission: String): Boolean { + val requestedPermissions = getManifestPermissions(context) + if (!requestedPermissions.isNullOrEmpty()) { + val requestedPermissionsList = listOf(*requestedPermissions) + val requestedPermissionsArrayList = ArrayList(requestedPermissionsList) + if (requestedPermissionsArrayList.contains(permission)) { + return true + } + } + return false + } + + /** + * Check whether all of the given permissions have been defined in the AndroidManifest.xml + * @param context the app context + * @param permissions a list of permissions + * @return true only if all permissions are defined in the AndroidManifest.xml + */ + fun hasDefinedPermissions(context: Context, permissions: Array): Boolean { + return permissions.all { hasDefinedPermission(context, it) } + } + + /** + * Get the permissions defined in AndroidManifest.xml + * + * @return The permissions defined in AndroidManifest.xml + */ + private fun getManifestPermissions(context: Context): Array? { + var requestedPermissions: Array? = null + try { + val pm = context.packageManager + val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + pm.getPackageInfo(context.packageName, PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong())) + } else { + @Suppress("DEPRECATION") + pm.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS) + } + if (packageInfo != null) { + requestedPermissions = packageInfo.requestedPermissions + } + } catch (_: Exception) { + } + return requestedPermissions + } + + /** + * Given a list of permissions, return a new list with the ones not present in AndroidManifest.xml + * + * @param neededPermissions The permissions needed. + * @return The permissions not present in AndroidManifest.xml + */ + fun getUndefinedPermissions(context: Context, neededPermissions: Array): Array { + val requestedPermissions = getManifestPermissions(context) + if (!requestedPermissions.isNullOrEmpty()) { + val undefinedPermissions = ArrayList() + val requestedPermissionsList = listOf(*requestedPermissions) + val requestedPermissionsArrayList = ArrayList(requestedPermissionsList) + for (permission in neededPermissions) { + if (!requestedPermissionsArrayList.contains(permission)) { + undefinedPermissions.add(permission) + } + } + return undefinedPermissions.toTypedArray() + } + return neededPermissions + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/PermissionState.kt b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/PermissionState.kt new file mode 100644 index 000000000..33092ed55 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/PermissionState.kt @@ -0,0 +1,21 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri + +import java.util.* + +enum class PermissionState(private val state: String) { + GRANTED("granted"), DENIED("denied"), PROMPT("prompt"), PROMPT_WITH_RATIONALE("prompt-with-rationale"); + + override fun toString(): String { + return state + } + + companion object { + fun byState(state: String): PermissionState { + return valueOf(state.uppercase(Locale.ROOT).replace('-', '_')) + } + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/annotation/ActivityCallback.kt b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/annotation/ActivityCallback.kt new file mode 100644 index 000000000..d68093994 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/annotation/ActivityCallback.kt @@ -0,0 +1,9 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.annotation + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION) +annotation class ActivityCallback diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/annotation/InvokeArg.kt b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/annotation/InvokeArg.kt new file mode 100644 index 000000000..08a2f7524 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/annotation/InvokeArg.kt @@ -0,0 +1,9 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.annotation + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.CLASS) +annotation class InvokeArg diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/annotation/Permission.kt b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/annotation/Permission.kt new file mode 100644 index 000000000..bd5ab5ddf --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/annotation/Permission.kt @@ -0,0 +1,19 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.annotation + +@Retention(AnnotationRetention.RUNTIME) +annotation class Permission( + /** + * An array of Android permission strings. + * Eg: {Manifest.permission.ACCESS_COARSE_LOCATION} + * or {"android.permission.ACCESS_COARSE_LOCATION"} + */ + val strings: Array = [], + /** + * An optional name to use instead of the Android permission string. + */ + val alias: String = "" +) diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/annotation/PermissionCallback.kt b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/annotation/PermissionCallback.kt new file mode 100644 index 000000000..5fdeddf9d --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/annotation/PermissionCallback.kt @@ -0,0 +1,9 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.annotation + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION) +annotation class PermissionCallback diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/annotation/PluginMethod.kt b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/annotation/PluginMethod.kt new file mode 100644 index 000000000..7a73045e4 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/annotation/PluginMethod.kt @@ -0,0 +1,8 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.annotation + +@Retention(AnnotationRetention.RUNTIME) +annotation class Command diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/annotation/TauriPlugin.kt b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/annotation/TauriPlugin.kt new file mode 100644 index 000000000..4e8bc786f --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/annotation/TauriPlugin.kt @@ -0,0 +1,19 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.annotation + +import app.tauri.annotation.Permission + +/** + * Base annotation for all Plugins + */ +@Retention(AnnotationRetention.RUNTIME) +annotation class TauriPlugin( + /** + * Permissions this plugin needs, in order to make permission requests + * easy if the plugin only needs basic permission prompting + */ + val permissions: Array = [] +) diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/Channel.kt b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/Channel.kt new file mode 100644 index 000000000..581b074a4 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/Channel.kt @@ -0,0 +1,33 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.ObjectMapper + +const val CHANNEL_PREFIX = "__CHANNEL__:" + +internal class ChannelDeserializer(val sendChannelData: (channelId: Long, data: String) -> Unit, private val objectMapper: ObjectMapper): JsonDeserializer() { + override fun deserialize( + jsonParser: JsonParser?, + deserializationContext: DeserializationContext + ): Channel { + val channelDef = deserializationContext.readValue(jsonParser, String::class.java) + val callback = channelDef.substring(CHANNEL_PREFIX.length).toLongOrNull() ?: throw Error("unexpected channel value $channelDef") + return Channel(callback, { res -> sendChannelData(callback, res) }, objectMapper) + } +} + +class Channel(val id: Long, private val handler: (data: String) -> Unit, private val objectMapper: ObjectMapper) { + fun send(data: JSObject) { + handler(PluginResult(data).toString()) + } + + fun sendObject(data: Any) { + handler(objectMapper.writeValueAsString(data)) + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/InvalidPluginMethodException.kt b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/InvalidPluginMethodException.kt new file mode 100644 index 000000000..4325313fc --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/InvalidPluginMethodException.kt @@ -0,0 +1,11 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +internal class InvalidCommandException : Exception { + constructor(s: String?) : super(s) {} + constructor(t: Throwable?) : super(t) {} + constructor(s: String?, t: Throwable?) : super(s, t) {} +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/Invoke.kt b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/Invoke.kt new file mode 100644 index 000000000..1f9d08007 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/Invoke.kt @@ -0,0 +1,93 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +import app.tauri.Logger +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper + +class Invoke( + val id: Long, + val command: String, + val callback: Long, + val error: Long, + private val sendResponse: (callback: Long, data: String) -> Unit, + private val argsJson: String, + private val jsonMapper: ObjectMapper +) { + fun getRawArgs(): String { + return argsJson + } + + fun getArgs(): JSObject { + return JSObject(argsJson) + } + + fun parseArgs(cls: Class): T { + return jsonMapper.readValue(argsJson, cls) + } + + fun parseArgs(ref: TypeReference): T { + return jsonMapper.readValue(argsJson, ref) + } + + fun resolve(data: JSObject?) { + sendResponse(callback, PluginResult(data).toString()) + } + + fun resolveObject(data: Any) { + sendResponse(callback, jsonMapper.writeValueAsString(data)) + } + + fun resolve() { + sendResponse(callback, "null") + } + + fun reject(msg: String?, code: String?, ex: Exception?, data: JSObject?) { + val errorResult = PluginResult() + + if (ex != null) { + Logger.error(Logger.tags("Plugin"), msg!!, ex) + } + + errorResult.put("message", msg) + if (code != null) { + errorResult.put("code", code) + } + if (data != null) { + errorResult.put("data", data) + } + + sendResponse(error, errorResult.toString()) + } + + fun reject(msg: String?, ex: Exception?, data: JSObject?) { + reject(msg, null, ex, data) + } + + fun reject(msg: String?, code: String?, data: JSObject?) { + reject(msg, code, null, data) + } + + fun reject(msg: String?, code: String?, ex: Exception?) { + reject(msg, code, ex, null) + } + + fun reject(msg: String?, data: JSObject?) { + reject(msg, null, null, data) + } + + fun reject(msg: String?, ex: Exception?) { + reject(msg, null, ex, null) + } + + fun reject(msg: String?, code: String?) { + reject(msg, code, null, null) + } + + fun reject(msg: String?) { + reject(msg, null, null, null) + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/JSArray.kt b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/JSArray.kt new file mode 100644 index 000000000..84bdf4017 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/JSArray.kt @@ -0,0 +1,45 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +import org.json.JSONArray +import org.json.JSONException + +class JSArray : JSONArray { + constructor() : super() {} + constructor(json: String?) : super(json) {} + constructor(copyFrom: Collection<*>?) : super(copyFrom) {} + constructor(array: Any?) : super(array) {} + + @Suppress("UNCHECKED_CAST", "ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") + @Throws(JSONException::class) + fun toList(): List { + val items: MutableList = ArrayList() + var o: Any? = null + for (i in 0 until this.length()) { + this.get(i).also { o = it } + try { + items.add(this.get(i) as E) + } catch (ex: Exception) { + throw JSONException("Not all items are instances of the given type") + } + } + return items + } + + companion object { + /** + * Create a new JSArray without throwing a error + */ + fun from(array: Any?): JSArray? { + try { + return JSArray(array) + } catch (ex: JSONException) { + // + } + return null + } + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/JSObject.kt b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/JSObject.kt new file mode 100644 index 000000000..2d4056108 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/JSObject.kt @@ -0,0 +1,152 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +import org.json.JSONException +import org.json.JSONObject + +class JSObject : JSONObject { + constructor() : super() + constructor(json: String) : super(json) + constructor(obj: JSONObject, names: Array) : super(obj, names) + + override fun getString(key: String): String { + return getString(key, "")!! + } + + fun getString(key: String, defaultValue: String?): String? { + try { + if (!super.isNull(key)) { + return super.getString(key) + } + } catch (_: JSONException) { + } + return defaultValue + } + + fun getInteger(key: String): Int? { + return getIntegerInternal(key, null) + } + + fun getInteger(key: String, defaultValue: Int): Int { + return getIntegerInternal(key, defaultValue)!! + } + + private fun getIntegerInternal(key: String, defaultValue: Int?): Int? { + try { + return super.getInt(key) + } catch (_: JSONException) { + } + return defaultValue + } + + override fun getBoolean(key: String): Boolean { + return getBooleanInternal(key, false)!! + } + + fun getBoolean(key: String, defaultValue: Boolean?): Boolean { + return getBooleanInternal(key, defaultValue)!! + } + + private fun getBooleanInternal(key: String, defaultValue: Boolean?): Boolean? { + try { + return super.getBoolean(key) + } catch (_: JSONException) { + } + return defaultValue + } + + fun getJSObject(name: String): JSObject? { + try { + return getJSObjectInternal(name, null) + } catch (_: JSONException) { + } + return null + } + + fun getJSObject(name: String, defaultValue: JSObject): JSObject { + return getJSObjectInternal(name, defaultValue)!! + } + + private fun getJSObjectInternal(name: String, defaultValue: JSObject?): JSObject? { + try { + val obj = get(name) + if (obj is JSONObject) { + val keysIter = obj.keys() + val keys: MutableList = ArrayList() + while (keysIter.hasNext()) { + keys.add(keysIter.next()) + } + return JSObject(obj, keys.toTypedArray()) + } + } catch (_: JSONException) { + } + return defaultValue + } + + override fun put(key: String, value: Boolean): JSObject { + try { + super.put(key, value) + } catch (_: JSONException) { + } + return this + } + + override fun put(key: String, value: Int): JSObject { + try { + super.put(key, value) + } catch (_: JSONException) { + } + return this + } + + override fun put(key: String, value: Long): JSObject { + try { + super.put(key, value) + } catch (_: JSONException) { + } + return this + } + + override fun put(key: String, value: Double): JSObject { + try { + super.put(key, value) + } catch (_: JSONException) { + } + return this + } + + override fun put(key: String, value: Any?): JSObject { + try { + super.put(key, value) + } catch (_: JSONException) { + } + return this + } + + fun put(key: String, value: String?): JSObject { + try { + super.put(key, value) + } catch (_: JSONException) { + } + return this + } + + companion object { + /** + * Convert a pathetic JSONObject into a JSObject + * @param obj + */ + @Throws(JSONException::class) + fun fromJSONObject(obj: JSONObject): JSObject { + val keysIter = obj.keys() + val keys: MutableList = ArrayList() + while (keysIter.hasNext()) { + keys.add(keysIter.next()) + } + return JSObject(obj, keys.toTypedArray()) + } + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/Plugin.kt b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/Plugin.kt new file mode 100644 index 000000000..589db2fd0 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/Plugin.kt @@ -0,0 +1,536 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +import android.app.Activity +import android.content.res.Configuration +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.webkit.WebView +import androidx.activity.result.IntentSenderRequest +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import app.tauri.FsUtils +import app.tauri.Logger +import app.tauri.PermissionHelper +import app.tauri.PermissionState +import app.tauri.annotation.ActivityCallback +import app.tauri.annotation.Command +import app.tauri.annotation.InvokeArg +import app.tauri.annotation.PermissionCallback +import app.tauri.annotation.TauriPlugin +import com.fasterxml.jackson.databind.ObjectMapper +import java.util.* +import java.util.concurrent.CopyOnWriteArrayList + +@InvokeArg +internal class RegisterListenerArgs { + lateinit var event: String + lateinit var handler: Channel +} + +@InvokeArg +internal class RemoveListenerArgs { + lateinit var event: String + var channelId: Long = 0 +} + +@InvokeArg internal class RequestPermissionsArgs { + var permissions: List? = null +} + +abstract class Plugin(private val activity: Activity) { + var handle: PluginHandle? = null + private val listeners: MutableMap> = mutableMapOf() + + open fun load(webView: WebView) {} + + fun jsonMapper(): ObjectMapper { + return handle!!.jsonMapper + } + + fun getConfig(cls: Class): T { + return jsonMapper().readValue(handle!!.config, cls) + } + + /** + * Handle a new intent being received by the application + */ + open fun onNewIntent(intent: Intent) {} + + + /** + * This event is called just before another activity comes into the foreground. + */ + open fun onPause(activity: AppCompatActivity) {} + + /** + * This event is called just before another activity comes into the foreground. + */ + @Deprecated("use onPause(activity: AppCompatActivity) instead") + open fun onPause() {} + + /** + * This event is called when the user returns to the activity. It is also called on cold starts. + */ + open fun onResume(activity: AppCompatActivity) {} + + /** + * This event is called when the user returns to the activity. It is also called on cold starts. + */ + @Deprecated("use onResume(activity: AppCompatActivity) instead") + open fun onResume() {} + + /** + * This event is called after onStop() when the current activity is being re-displayed to the user (the user has navigated back to it). + * It will be followed by onStart() and then onResume(). + */ + open fun onRestart(activity: AppCompatActivity) {} + + /** + * This event is called after onStop() when the current activity is being re-displayed to the user (the user has navigated back to it). + * It will be followed by onStart() and then onResume(). + */ + @Deprecated("use onRestart(activity: AppCompatActivity) instead") + open fun onRestart() {} + + /** + * This event is called when the app is no longer visible to the user. + * You will next receive either onRestart(), onDestroy(), or nothing, depending on later user activity. + */ + open fun onStop(activity: AppCompatActivity) {} + + /** + * This event is called when the app is no longer visible to the user. + * You will next receive either onRestart(), onDestroy(), or nothing, depending on later user activity. + */ + @Deprecated("use onStop(activity: AppCompatActivity) instead") + open fun onStop() {} + + /** + * This event is called before the activity is destroyed. + */ + open fun onDestroy(activity: AppCompatActivity) {} + /** + * This event is called before an activity is destroyed. + */ + @Deprecated("use onDestroy(activity: AppCompatActivity) instead") + open fun onDestroy() {} + + internal fun triggerOnDestroy(activity: AppCompatActivity) { + onDestroy(activity) + onDestroy() + } + + internal fun triggerOnRestart(activity: AppCompatActivity) { + onRestart(activity) + onRestart() + } + + internal fun triggerOnPause(activity: AppCompatActivity) { + onPause(activity) + onPause() + } + + internal fun triggerOnResume(activity: AppCompatActivity) { + onResume(activity) + onResume() + } + + internal fun triggerOnStop(activity: AppCompatActivity) { + onStop(activity) + onStop() + } + + /** + * This event is called when a configuration change occurs but the app does not recreate the activity. + */ + open fun onConfigurationChanged(newConfig: Configuration) {} + + /** + * Start activity for result with the provided Intent and resolve calling the provided callback method name. + * + * If there is no registered activity callback for the method name passed in, the call will + * be rejected. Make sure a valid activity result callback method is registered using the + * [ActivityCallback] annotation. + * + * @param invoke the invoke object + * @param intent the intent used to start an activity + * @param callbackName the name of the callback to run when the launched activity is finished + */ + fun startActivityForResult(invoke: Invoke, intent: Intent, callbackName: String) { + handle!!.startActivityForResult(invoke, intent, callbackName) + } + + /** + * Like startActivityForResult() but taking an IntentSender to describe the activity to be started. + * + * If there is no registered activity callback for the method name passed in, the call will + * be rejected. Make sure a valid activity result callback method is registered using the + * [ActivityCallback] annotation. + * + * @param invoke the invoke object + * @param intentSender the intent used to start an activity + * @param callbackName the name of the callback to run when the launched activity is finished + */ + fun startIntentSenderForResult(invoke: Invoke, intentSender: IntentSenderRequest, callbackName: String) { + handle!!.startIntentSenderForResult(invoke, intentSender, callbackName) + } + + /** + * Get the plugin log tags. + * @param subTags + */ + protected fun getLogTag(vararg subTags: String): String { + return Logger.tags(*subTags) + } + + /** + * Gets a log tag with the plugin's class name as subTag. + */ + protected fun getLogTag(): String { + return Logger.tags(this.javaClass.simpleName) + } + + /** + * Convert an URI to an URL that can be loaded by the webview. + */ + fun assetUrl(u: Uri): String { + var path = FsUtils.getFileUrlForUri(activity, u) + if (path?.startsWith("file://") == true) { + path = path.replace("file://", "") + } + return "asset://localhost$path" + } + + fun trigger(event: String, payload: JSObject) { + val eventListeners = listeners[event] + if (!eventListeners.isNullOrEmpty()) { + val listeners = CopyOnWriteArrayList(eventListeners) + for (channel in listeners) { + channel.send(payload) + } + } + } + + fun triggerObject(event: String, payload: Any) { + val eventListeners = listeners[event] + if (!eventListeners.isNullOrEmpty()) { + val listeners = CopyOnWriteArrayList(eventListeners) + for (channel in listeners) { + channel.sendObject(payload) + } + } + } + + fun hasListener(event: String): Boolean { + return !listeners[event].isNullOrEmpty() + } + + @Command + open fun registerListener(invoke: Invoke) { + val args = invoke.parseArgs(RegisterListenerArgs::class.java) + + val eventListeners = listeners[args.event] + if (eventListeners.isNullOrEmpty()) { + listeners[args.event] = mutableListOf(args.handler) + } else { + eventListeners.add(args.handler) + } + + invoke.resolve() + } + + @Command + open fun removeListener(invoke: Invoke) { + val args = invoke.parseArgs(RemoveListenerArgs::class.java) + + val eventListeners = listeners[args.event] + if (!eventListeners.isNullOrEmpty()) { + val c = eventListeners.find { c -> c.id == args.channelId } + if (c != null) { + eventListeners.remove(c) + } + } + + invoke.resolve() + } + + /** + * Exported plugin method for checking the granted status for each permission + * declared on the plugin. This plugin call responds with a mapping of permissions to + * the associated granted status. + */ + @Command + @PermissionCallback + open fun checkPermissions(invoke: Invoke) { + val permissionsResult: Map = getPermissionStates() + if (permissionsResult.isEmpty()) { + // if no permissions are defined on the plugin, resolve undefined + invoke.resolve() + } else { + val permissionsResultJSON = JSObject() + for ((key, value) in permissionsResult) { + permissionsResultJSON.put(key, value) + } + invoke.resolve(permissionsResultJSON) + } + } + + /** + * Exported plugin method to request all permissions for this plugin. + * To manually request permissions within a plugin use: + * [.requestAllPermissions], or + * [.requestPermissionForAlias], or + * [.requestPermissionForAliases] + * + * @param invoke + */ + @Command + open fun requestPermissions(invoke: Invoke) { + val annotation = handle?.annotation + if (annotation != null) { + // handle permission requests for plugins defined with @TauriPlugin + var permAliases: Array? = null + val autoGrantPerms: MutableSet = HashSet() + + val args = invoke.parseArgs(RequestPermissionsArgs::class.java) + + args.permissions?.let { + val aliasSet: MutableSet = HashSet() + + for (perm in annotation.permissions) { + if (it.contains(perm.alias)) { + aliasSet.add(perm.alias) + } + } + if (aliasSet.isEmpty()) { + invoke.reject("No valid permission alias was requested of this plugin.") + return + } else { + permAliases = aliasSet.toTypedArray() + } + } ?: run { + val aliasSet: MutableSet = HashSet() + + for (perm in annotation.permissions) { + // If a permission is defined with no permission strings, separate it for auto-granting. + // Otherwise, the alias is added to the list to be requested. + if (perm.strings.isEmpty() || perm.strings.size == 1 && perm.strings[0] + .isEmpty() + ) { + if (perm.alias.isNotEmpty()) { + autoGrantPerms.add(perm.alias) + } + } else { + aliasSet.add(perm.alias) + } + } + permAliases = aliasSet.toTypedArray() + } + + permAliases?.let { + // request permissions using provided aliases or all defined on the plugin + requestPermissionForAliases(it, invoke, "checkPermissions") + } ?: run { + if (autoGrantPerms.isNotEmpty()) { + // if the plugin only has auto-grant permissions, return all as GRANTED + val permissionsResults = JSObject() + for (perm in autoGrantPerms) { + permissionsResults.put(perm, PermissionState.GRANTED.toString()) + } + invoke.resolve(permissionsResults) + } else { + // no permissions are defined on the plugin, resolve undefined + invoke.resolve() + } + } + } + } + + /** + * Checks if the given permission alias is correctly declared in AndroidManifest.xml + * @param alias a permission alias defined on the plugin + * @return true only if all permissions associated with the given alias are declared in the manifest + */ + fun isPermissionDeclared(alias: String): Boolean { + for (perm in handle?.annotation?.permissions.orEmpty()) { + if (alias.equals(perm.alias, ignoreCase = true)) { + var result = true + for (permString in perm.strings) { + result = result && PermissionHelper.hasDefinedPermission(activity, permString) + } + return result + } + } + Logger.error( + String.format( + "isPermissionDeclared: No alias defined for %s " + "or missing @TauriPlugin annotation.", + alias + ) + ) + return false + } + + private fun permissionActivityResult( + invoke: Invoke, + permissionStrings: Array, + callbackName: String + ) { + handle!!.requestPermissions(invoke, permissionStrings, callbackName) + } + + /** + * Request all of the specified permissions in the TauriPlugin annotation (if any) + * + * If there is no registered permission callback for the Invoke passed in, the call will + * be rejected. Make sure a valid permission callback method is registered using the + * [PermissionCallback] annotation. + * + * @param invoke + * @param callbackName the name of the callback to run when the permission request is complete + */ + protected fun requestAllPermissions( + invoke: Invoke, + callbackName: String + ) { + val annotation = handle!!.annotation + if (annotation != null) { + val perms: HashSet = HashSet() + for (perm in annotation.permissions) { + perms.addAll(perm.strings) + } + permissionActivityResult(invoke, perms.toArray(arrayOfNulls(0)), callbackName) + } + } + + /** + * Request permissions using an alias defined on the plugin. + * + * If there is no registered permission callback for the Invoke passed in, the call will + * be rejected. Make sure a valid permission callback method is registered using the + * [PermissionCallback] annotation. + * + * @param alias an alias defined on the plugin + * @param invoke the invoke involved in originating the request + * @param callbackName the name of the callback to run when the permission request is complete + */ + protected fun requestPermissionForAlias( + alias: String, + invoke: Invoke, + callbackName: String + ) { + requestPermissionForAliases(arrayOf(alias), invoke, callbackName) + } + + /** + * Request permissions using aliases defined on the plugin. + * + * If there is no registered permission callback for the Invoke passed in, the call will + * be rejected. Make sure a valid permission callback method is registered using the + * [PermissionCallback] annotation. + * + * @param aliases a set of aliases defined on the plugin + * @param invoke the invoke involved in originating the request + * @param callbackName the name of the callback to run when the permission request is complete + */ + fun requestPermissionForAliases( + aliases: Array, + invoke: Invoke, + callbackName: String + ) { + if (aliases.isEmpty()) { + Logger.error("No permission alias was provided") + return + } + val permissions = getPermissionStringsForAliases(aliases) + if (permissions.isNotEmpty()) { + permissionActivityResult(invoke, permissions, callbackName) + } + } + + /** + * Gets the Android permission strings defined on the [TauriPlugin] annotation with + * the provided aliases. + * + * @param aliases aliases for permissions defined on the plugin + * @return Android permission strings associated with the provided aliases, if exists + */ + private fun getPermissionStringsForAliases(aliases: Array): Array { + val perms: HashSet = HashSet() + for (perm in handle?.annotation?.permissions.orEmpty()) { + if (aliases.contains(perm.alias)) { + perms.addAll(perm.strings) + } + } + return perms.toArray(arrayOfNulls(0)) + } + + /** + * Get the permission state for the provided permission alias. + * + * @param alias the permission alias to get + * @return the state of the provided permission alias or null + */ + fun getPermissionState(alias: String): PermissionState? { + return getPermissionStates()[alias] + } + + /** + * Helper to check all permissions defined on a plugin and see the state of each. + * + * @return A mapping of permission aliases to the associated granted status. + */ + open fun getPermissionStates(): Map { + val permissionsResults: MutableMap = HashMap() + for (perm in handle?.annotation?.permissions.orEmpty()) { + // If a permission is defined with no permission constants, return GRANTED for it. + // Otherwise, get its true state. + if (perm.strings.isEmpty() || perm.strings.size == 1 && perm.strings[0].isEmpty()) { + val key = perm.alias + if (key.isNotEmpty()) { + val existingResult = permissionsResults[key] + + // auto set permission state to GRANTED if the alias is empty. + if (existingResult == null) { + permissionsResults[key] = PermissionState.GRANTED + } + } + } else { + for (permString in perm.strings) { + val key = perm.alias.ifEmpty { permString } + var permissionStatus: PermissionState + if (ActivityCompat.checkSelfPermission( + activity, + permString + ) == PackageManager.PERMISSION_GRANTED + ) { + permissionStatus = PermissionState.GRANTED + } else { + permissionStatus = PermissionState.PROMPT + + // Check if there is a cached permission state for the "Never ask again" state + val prefs = + activity.getSharedPreferences("PluginPermStates", Activity.MODE_PRIVATE) + val state = prefs.getString(permString, null) + if (state != null) { + permissionStatus = PermissionState.byState(state) + } + } + val existingResult = permissionsResults[key] + + // multiple permissions with the same alias must all be true, otherwise all false. + if (existingResult == null || existingResult === PermissionState.GRANTED) { + permissionsResults[key] = permissionStatus + } + } + } + } + + return permissionsResults + } + +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/PluginHandle.kt b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/PluginHandle.kt new file mode 100644 index 000000000..594a96f4b --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/PluginHandle.kt @@ -0,0 +1,178 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +import android.app.Activity +import android.content.Intent +import android.webkit.WebView +import androidx.activity.result.IntentSenderRequest +import androidx.core.app.ActivityCompat +import app.tauri.PermissionHelper +import app.tauri.PermissionState +import app.tauri.annotation.ActivityCallback +import app.tauri.annotation.Command +import app.tauri.annotation.PermissionCallback +import app.tauri.annotation.TauriPlugin +import com.fasterxml.jackson.databind.ObjectMapper +import java.lang.reflect.Method +import androidx.core.content.edit + +class PluginHandle( + private val manager: PluginManager, + val name: String, + val instance: Plugin, + val config: String, + val jsonMapper: ObjectMapper +) { + private val commands: HashMap = HashMap() + private val permissionCallbackMethods: HashMap = HashMap() + private val startActivityCallbackMethods: HashMap = HashMap() + var annotation: TauriPlugin? + var loaded = false + + init { + indexMethods() + instance.handle = this + annotation = instance.javaClass.getAnnotation(TauriPlugin::class.java) + } + + fun load(webView: WebView) { + instance.load(webView) + loaded = true + } + + fun startActivityForResult(invoke: Invoke, intent: Intent, callbackName: String) { + manager.startActivityForResult(intent) { result -> + val method = startActivityCallbackMethods[callbackName] + if (method != null) { + method.isAccessible = true + method(instance, invoke, result) + } + } + } + + fun startIntentSenderForResult( + invoke: Invoke, + intentSender: IntentSenderRequest, + callbackName: String + ) { + manager.startIntentSenderForResult(intentSender) { result -> + val method = startActivityCallbackMethods[callbackName] + if (method != null) { + method.isAccessible = true + method(instance, invoke, result) + } + } + } + + fun requestPermissions( + invoke: Invoke, + permissions: Array, + callbackName: String + ) { + manager.requestPermissions(permissions) { result -> + if (validatePermissions(invoke, result)) { + val method = permissionCallbackMethods[callbackName] + if (method != null) { + method.isAccessible = true + method(instance, invoke) + } + } + } + } + + /** + * Saves permission states and rejects if permissions were not correctly defined in + * the AndroidManifest.xml file. + * + * @param permissions + * @return true if permissions were saved and defined correctly, false if not + */ + private fun validatePermissions( + invoke: Invoke, + permissions: Map + ): Boolean { + val activity = manager.activity!! + val prefs = + activity.getSharedPreferences("PluginPermStates", Activity.MODE_PRIVATE) + for ((permString, isGranted) in permissions) { + if (isGranted) { + // Permission granted. If previously denied, remove cached state + val state = prefs.getString(permString, null) + if (state != null) { + prefs.edit { + remove(permString) + } + } + } else { + prefs.edit { + if (ActivityCompat.shouldShowRequestPermissionRationale( + activity, + permString + ) + ) { + // Permission denied, can prompt again with rationale + putString(permString, PermissionState.PROMPT_WITH_RATIONALE.toString()) + } else { + // Permission denied permanently, store this state for future reference + putString(permString, PermissionState.DENIED.toString()) + } + } + } + } + val permStrings = permissions.keys.toTypedArray() + if (!PermissionHelper.hasDefinedPermissions(activity, permStrings)) { + val builder = StringBuilder() + builder.append("Missing the following permissions in AndroidManifest.xml:\n") + val missing = PermissionHelper.getUndefinedPermissions(activity, permStrings) + for (perm in missing) { + builder.append( + """ + $perm + + """.trimIndent() + ) + } + invoke.reject(builder.toString()) + return false + } + return true + } + + @Throws( + InvalidCommandException::class, + IllegalAccessException::class + ) + fun invoke(invoke: Invoke) { + val methodMeta = commands[invoke.command] + ?: throw InvalidCommandException("No command " + invoke.command + " found for plugin " + instance.javaClass.name) + methodMeta.method.invoke(instance, invoke) + } + + private fun indexMethods() { + val methods = mutableListOf() + var pluginCursor: Class<*> = instance.javaClass + while (pluginCursor.name != Any::class.java.name) { + methods.addAll(listOf(*pluginCursor.declaredMethods)) + pluginCursor = pluginCursor.superclass + } + + for (method in methods) { + if (method.isAnnotationPresent(Command::class.java)) { + val command = method.getAnnotation(Command::class.java) ?: continue + val methodMeta = CommandData(method, command) + commands[method.name] = methodMeta + } + + if (method.isAnnotationPresent(ActivityCallback::class.java)) { + startActivityCallbackMethods[method.name] = method + } + + if (method.isAnnotationPresent(PermissionCallback::class.java)) { + permissionCallbackMethods[method.name] = method + } + } + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/PluginManager.kt b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/PluginManager.kt new file mode 100644 index 000000000..367fd8e9d --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/PluginManager.kt @@ -0,0 +1,245 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +import android.content.res.Configuration +import android.content.Context +import android.content.Intent +import android.webkit.WebView +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.IntentSenderRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import app.tauri.annotation.InvokeArg +import app.tauri.FsUtils +import app.tauri.JniMethod +import app.tauri.Logger +import com.fasterxml.jackson.annotation.JsonAutoDetect +import com.fasterxml.jackson.annotation.PropertyAccessor +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsonorg.JsonOrgModule +import com.fasterxml.jackson.databind.module.SimpleModule +import java.lang.reflect.InvocationTargetException + +object PluginManager { + fun interface RequestPermissionsCallback { + fun onResult(permissions: Map) + } + + fun interface ActivityResultCallback { + fun onResult(result: ActivityResult) + } + + /** The result launchers belonging to one activity. */ + private class ResultLaunchers( + val startActivityForResult: ActivityResultLauncher, + val startIntentSenderForResult: ActivityResultLauncher, + val requestPermissions: ActivityResultLauncher> + ) + + // Insertion ordered, so the activity taken over when the current one goes away is the oldest + // surviving one rather than an arbitrary member of a hash set. + private val launchers: LinkedHashMap = LinkedHashMap() + var activity: AppCompatActivity? = null + private val plugins: HashMap = HashMap() + private var requestPermissionsCallback: RequestPermissionsCallback? = null + private var startActivityForResultCallback: ActivityResultCallback? = null + private var startIntentSenderForResultCallback: ActivityResultCallback? = null + private var jsonMapper: ObjectMapper = ObjectMapper() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) + .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) + + init { + val channelDeserializer = ChannelDeserializer({ channelId, payload -> + sendChannelData(channelId, payload) + }, jsonMapper) + jsonMapper + .registerModule(SimpleModule().addDeserializer(Channel::class.java, channelDeserializer)) + // Without it, the mapper's field visibility serializes an org.json value through + // its private fields: a `JSArray` as `{"values":[...]}` and a `JSObject` as + // `{"nameValuePairs":{...}}`. + .registerModule(JsonOrgModule()) + } + + fun onCreate(activity: AppCompatActivity) { + // Every activity gets its own launchers, and gets them here: registerForActivityResult must + // be called before its owner reaches STARTED, so an activity that is already running can + // never be given launchers later. + launchers[activity] = registerResultLaunchers(activity) + if (this.activity == null) { + this.activity = activity + } + } + + private fun registerResultLaunchers(activity: AppCompatActivity): ResultLaunchers = + ResultLaunchers( + activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult() + ) { result -> + startActivityForResultCallback?.onResult(result) + }, + + activity.registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult() + ) { result -> + startIntentSenderForResultCallback?.onResult(result) + }, + + activity.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions() + ) { result -> + requestPermissionsCallback?.onResult(result) + } + ) + + private val currentLaunchers: ResultLaunchers + get() = launchers[activity] + ?: throw IllegalStateException("the plugin manager has no activity to launch from") + + fun onNewIntent(intent: Intent) { + for (plugin in plugins.values) { + plugin.instance.onNewIntent(intent) + } + } + + fun onPause(activity: AppCompatActivity) { + for (plugin in plugins.values) { + plugin.instance.triggerOnPause(activity) + } + } + + fun onResume(activity: AppCompatActivity) { + for (plugin in plugins.values) { + plugin.instance.triggerOnResume(activity) + } + } + + fun onRestart(activity: AppCompatActivity) { + for (plugin in plugins.values) { + plugin.instance.triggerOnRestart(activity) + } + } + + fun onStop(activity: AppCompatActivity) { + for (plugin in plugins.values) { + plugin.instance.triggerOnStop(activity) + } + } + + fun onDestroy(activity: AppCompatActivity) { + for (plugin in plugins.values) { + plugin.instance.triggerOnDestroy(activity) + } + + launchers.remove(activity) + if (this.activity == activity) { + // Whatever is left already holds its own launchers, registered when it was created. Moving + // this activity's launchers over instead would mean registering against an activity that is + // already running, which registerForActivityResult rejects with an IllegalStateException. + this.activity = launchers.keys.firstOrNull() + } + } + + fun onConfigurationChanged(newConfig: Configuration) { + for (plugin in plugins.values) { + plugin.instance.onConfigurationChanged(newConfig) + } + } + + fun startActivityForResult(intent: Intent, callback: ActivityResultCallback) { + startActivityForResultCallback = callback + currentLaunchers.startActivityForResult.launch(intent) + } + + fun startIntentSenderForResult(intent: IntentSenderRequest, callback: ActivityResultCallback) { + startIntentSenderForResultCallback = callback + currentLaunchers.startIntentSenderForResult.launch(intent) + } + + fun requestPermissions( + permissionStrings: Array, + callback: RequestPermissionsCallback + ) { + requestPermissionsCallback = callback + currentLaunchers.requestPermissions.launch(permissionStrings) + } + + @JniMethod + fun onWebViewCreated(webView: WebView) { + for ((_, plugin) in plugins) { + if (!plugin.loaded) { + plugin.load(webView) + } + } + } + + @JniMethod + fun load(webView: WebView?, name: String, plugin: Plugin, config: String) { + val handle = PluginHandle(this, name, plugin, config, jsonMapper) + plugins[name] = handle + if (webView != null) { + plugin.load(webView) + } + } + + @JniMethod + fun runCommand(id: Int, pluginId: String, command: String, data: String) { + val successId = 0L + val errorId = 1L + val invoke = Invoke(id.toLong(), command, successId, errorId, { fn, result -> + var success: String? = null + var error: String? = null + if (fn == successId) { + success = result + } else { + error = result + } + handlePluginResponse(id, success, error) + }, data, jsonMapper) + + dispatchPluginMessage(invoke, pluginId) + } + + private fun dispatchPluginMessage(invoke: Invoke, pluginId: String) { + Logger.verbose( + Logger.tags("Plugin"), + "Tauri plugin: pluginId: $pluginId, command: ${invoke.command}" + ) + + try { + val plugin = plugins[pluginId] + if (plugin == null) { + invoke.reject("Plugin $pluginId not initialized") + } else { + plugins[pluginId]?.invoke(invoke) + } + } catch (e: Exception) { + var exception: Throwable = e + if (exception.message?.isEmpty() != false) { + if (e is InvocationTargetException) { + exception = e.targetException + } + } + invoke.reject(if (exception.message?.isEmpty() != false) { exception.toString() } else { exception.message }) + } + } + + fun loadConfig(context: Context, plugin: String, cls: Class): T { + val tauriConfigJson = FsUtils.readAsset(context.assets, "tauri.conf.json") + val mapper = ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + val config = mapper.readValue(tauriConfigJson, Config::class.java) + return mapper.readValue(config.plugins[plugin].toString(), cls) + } + + private external fun handlePluginResponse(id: Int, success: String?, error: String?) + private external fun sendChannelData(id: Long, data: String) +} + +@InvokeArg +internal class Config { + lateinit var plugins: Map +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/PluginMethodData.kt b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/PluginMethodData.kt new file mode 100644 index 000000000..acb09477e --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/PluginMethodData.kt @@ -0,0 +1,16 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +import app.tauri.annotation.Command +import java.lang.reflect.Method + +class CommandData( + val method: Method, methodDecorator: Command +) { + + // The name of the method + val name: String = method.name +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/PluginResult.kt b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/PluginResult.kt new file mode 100644 index 000000000..601cd48ec --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/main/java/app/tauri/plugin/PluginResult.kt @@ -0,0 +1,67 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +import android.annotation.SuppressLint +import app.tauri.Logger +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.* + +class PluginResult @JvmOverloads constructor(json: JSObject? = JSObject()) { + private val json: JSObject + + init { + this.json = json ?: JSObject() + } + + fun put(name: String, value: Boolean): PluginResult { + return jsonPut(name, value) + } + + fun put(name: String, value: Double): PluginResult { + return jsonPut(name, value) + } + + fun put(name: String, value: Int): PluginResult { + return jsonPut(name, value) + } + + fun put(name: String, value: Long): PluginResult { + return jsonPut(name, value) + } + + /** + * Format a date as an ISO string + */ + @SuppressLint("SimpleDateFormat") + fun put(name: String, value: Date): PluginResult { + val tz: TimeZone = TimeZone.getTimeZone("UTC") + val df: DateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'") + df.timeZone = tz + return jsonPut(name, df.format(value)) + } + + fun put(name: String, value: Any?): PluginResult { + return jsonPut(name, value) + } + + fun put(name: String, value: PluginResult): PluginResult { + return jsonPut(name, value.json) + } + + private fun jsonPut(name: String, value: Any?): PluginResult { + try { + json.put(name, value) + } catch (ex: Exception) { + Logger.error(Logger.tags("Plugin"), "", ex) + } + return this + } + + override fun toString(): String { + return json.toString() + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/test/java/app/tauri/ExampleUnitTest.kt b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/test/java/app/tauri/ExampleUnitTest.kt new file mode 100644 index 000000000..16022f41a --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/android/src/test/java/app/tauri/ExampleUnitTest.kt @@ -0,0 +1,21 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/.gitignore b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/.gitignore new file mode 100644 index 000000000..5922fdaa5 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +Package.resolved diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Package.swift b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Package.swift new file mode 100644 index 000000000..c82963880 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Package.swift @@ -0,0 +1,40 @@ +// swift-tools-version:5.3 +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import PackageDescription + +let package = Package( + name: "Tauri", + platforms: [ + .macOS(.v10_13), + .iOS(.v11), + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "Tauri", + type: .static, + targets: ["Tauri"]) + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package(name: "SwiftRs", url: "https://github.com/Brendonovich/swift-rs", .revision("f64a4514de07f450ec5b6aa297624cd3479d9579")) + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "Tauri", + dependencies: [ + .byName(name: "SwiftRs") + ], + path: "Sources" + ), + .testTarget( + name: "TauriTests", + dependencies: ["Tauri"] + ), + ] +) diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/README.md b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/README.md new file mode 100644 index 000000000..52c3f1c7f --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/README.md @@ -0,0 +1,3 @@ +# Tauri + +Tauri iOS API. diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/Channel.swift b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/Channel.swift new file mode 100644 index 000000000..add065c74 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/Channel.swift @@ -0,0 +1,65 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import Foundation + +let CHANNEL_PREFIX = "__CHANNEL__:" +let channelDataKey = CodingUserInfoKey(rawValue: "sendChannelData")! + +public class Channel: Decodable { + public let id: UInt64 + let handler: (UInt64, String) -> Void + + public required init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let channelDef = try container.decode(String.self) + + let components = channelDef.components(separatedBy: CHANNEL_PREFIX) + if components.count < 2 { + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Invalid channel definition from \(channelDef)" + ) + + } + guard let channelId = UInt64(components[1]) else { + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Invalid channel ID from \(channelDef)" + ) + } + + guard let handler = decoder.userInfo[channelDataKey] as? (UInt64, String) -> Void else { + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "missing userInfo for Channel handler. This is a Tauri issue" + ) + } + + self.id = channelId + self.handler = handler + } + + func serialize(_ data: JsonValue) -> String { + do { + return try data.jsonRepresentation() ?? "\"Failed to serialize payload\"" + } catch { + return "\"\(error)\"" + } + } + + public func send(_ data: JsonObject) { + send(.dictionary(data)) + } + + public func send(_ data: JsonValue) { + handler(id, serialize(data)) + } + + public func send(_ data: T) throws { + let json = try JSONEncoder().encode(data) + handler(id, String(decoding: json, as: UTF8.self)) + } + +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/Invoke.swift b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/Invoke.swift new file mode 100644 index 000000000..6f810868f --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/Invoke.swift @@ -0,0 +1,118 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import Foundation +import UIKit + +@objc public class Invoke: NSObject { + public let command: String + let callback: UInt64 + let error: UInt64 + let data: String + let sendResponse: (UInt64, String?) -> Void + let sendChannelData: (UInt64, String) -> Void + + public init( + command: String, callback: UInt64, error: UInt64, + sendResponse: @escaping (UInt64, String?) -> Void, + sendChannelData: @escaping (UInt64, String) -> Void, data: String + ) { + self.command = command + self.callback = callback + self.error = error + self.data = data + self.sendResponse = sendResponse + self.sendChannelData = sendChannelData + } + + public func getRawArgs() -> String { + return self.data + } + + public func getArgs() throws -> JSObject { + let jsonData = self.data.data(using: .utf8)! + let data = try JSONSerialization.jsonObject(with: jsonData, options: []) + return JSTypes.coerceDictionaryToJSObject( + (data as! NSDictionary), formattingDatesAsStrings: true)! + } + + public func parseArgs(_ type: T.Type) throws -> T { + let jsonData = self.data.data(using: .utf8)! + let decoder = JSONDecoder() + decoder.userInfo[channelDataKey] = sendChannelData + return try decoder.decode(type, from: jsonData) + } + + func serialize(_ data: JsonValue) -> String { + do { + return try data.jsonRepresentation() ?? "\"Failed to serialize payload\"" + } catch { + return "\"\(error)\"" + } + } + + public func resolve() { + sendResponse(callback, nil) + } + + public func resolve(_ data: JsonObject) { + resolve(.dictionary(data)) + } + + public func resolve(_ data: JsonValue) { + sendResponse(callback, serialize(data)) + } + + public func resolve(_ data: T) { + do { + let json = try JSONEncoder().encode(data) + sendResponse(callback, String(decoding: json, as: UTF8.self)) + } catch { + sendResponse(self.error, "\"\(error)\"") + } + } + + public func reject( + _ message: String, code: String? = nil, error: Error? = nil, data: JsonValue? = nil + ) { + let payload: NSMutableDictionary = [ + "message": message + ] + + if let code = code { + payload["code"] = code + } + + if let error = error { + payload["error"] = error + } + + if let data = data { + switch data { + case .dictionary(let dict): + for entry in dict { + payload[entry.key] = entry.value + } + } + } + + sendResponse(self.error, serialize(.dictionary(payload as! JsonObject))) + } + + public func unimplemented() { + unimplemented("not implemented") + } + + public func unimplemented(_ message: String) { + reject(message) + } + + public func unavailable() { + unavailable("not available") + } + + public func unavailable(_ message: String) { + reject(message) + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/JSTypes.swift b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/JSTypes.swift new file mode 100644 index 000000000..0ac9f2c79 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/JSTypes.swift @@ -0,0 +1,123 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import Foundation + +// declare our empty protocol, and conformance, for typing +public protocol JSValue {} +extension String: JSValue {} +extension Bool: JSValue {} +extension Int: JSValue {} +extension Float: JSValue {} +extension Double: JSValue {} +extension NSNumber: JSValue {} +extension NSNull: JSValue {} +extension Array: JSValue {} +extension Date: JSValue {} +extension Dictionary: JSValue where Key == String, Value == JSValue {} + +// convenience aliases +public typealias JSObject = [String: JSValue] +public typealias JSArray = [JSValue] + +extension Dictionary where Key == String, Value == JSValue { + public func getValue(_ key: String) -> JSValue? { + return self[key] + } + + public func getString(_ key: String) -> String? { + return self[key] as? String + } + + public func getBool(_ key: String) -> Bool? { + return self[key] as? Bool + } + + public func getInt(_ key: String) -> Int? { + return self[key] as? Int + } + + public func getFloat(_ key: String) -> Float? { + if let floatValue = self[key] as? Float { + return floatValue + } else if let doubleValue = self[key] as? Double { + return Float(doubleValue) + } + return nil + } + + public func getDouble(_ key: String) -> Double? { + return self[key] as? Double + } + + public func getArray(_ key: String) -> JSArray? { + return self[key] as? JSArray + } + + public func getObject(_ key: String) -> JSObject? { + return self[key] as? JSObject + } +} + +/* + Simply casting objects from foundation class clusters (such as __NSArrayM) + doesn't work with the JSValue protocol and will always fail. So we need to + recursively and explicitly convert each value in the dictionary. + */ +public enum JSTypes {} +extension JSTypes { + public static func coerceDictionaryToJSObject( + _ dictionary: NSDictionary?, formattingDatesAsStrings: Bool = false + ) -> JSObject? { + return coerceToJSValue(dictionary, formattingDates: formattingDatesAsStrings) as? JSObject + } + + public static func coerceDictionaryToJSObject( + _ dictionary: [AnyHashable: Any]?, formattingDatesAsStrings: Bool = false + ) -> JSObject? { + return coerceToJSValue(dictionary, formattingDates: formattingDatesAsStrings) as? JSObject + } +} + +private let dateStringFormatter = ISO8601DateFormatter() + +// We need a large switch statement because we have a lot of types. +// swiftlint:disable:next cyclomatic_complexity +private func coerceToJSValue(_ value: Any?, formattingDates: Bool) -> JSValue? { + guard let value = value else { + return nil + } + switch value { + case let stringValue as String: + return stringValue + case let numberValue as NSNumber: + return numberValue + case let boolValue as Bool: + return boolValue + case let intValue as Int: + return intValue + case let floatValue as Float: + return floatValue + case let doubleValue as Double: + return doubleValue + case let dateValue as Date: + if formattingDates { + return dateStringFormatter.string(from: dateValue) + } + return dateValue + case let nullValue as NSNull: + return nullValue + case let arrayValue as NSArray: + return arrayValue.compactMap { coerceToJSValue($0, formattingDates: formattingDates) } + case let dictionaryValue as NSDictionary: + let keys = dictionaryValue.allKeys.compactMap { $0 as? String } + var result: JSObject = [:] + for key in keys { + result[key] = coerceToJSValue(dictionaryValue[key], formattingDates: formattingDates) + } + return result + default: + return nil + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/JsonValue.swift b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/JsonValue.swift new file mode 100644 index 000000000..ae6d96f77 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/JsonValue.swift @@ -0,0 +1,58 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import Foundation + +public typealias JsonObject = [String: Any?] + +public enum JsonValue { + case dictionary(JsonObject) + + enum SerializationError: Error { + case invalidObject + } + + public func jsonRepresentation(includingFields: JsonObject? = nil) throws -> String? { + switch self { + case .dictionary(var dictionary): + if let fields = includingFields { + dictionary.merge(fields) { (current, _) in current } + } + dictionary = prepare(dictionary: dictionary) + guard JSONSerialization.isValidJSONObject(dictionary) else { + throw SerializationError.invalidObject + } + let data = try JSONSerialization.data(withJSONObject: dictionary, options: []) + return String(data: data, encoding: .utf8) + } + } + + private static let formatter = ISO8601DateFormatter() + + private func prepare(dictionary: JsonObject) -> JsonObject { + return dictionary.mapValues { (value) -> Any in + if let date = value as? Date { + return JsonValue.formatter.string(from: date) + } else if let aDictionary = value as? JsonObject { + return prepare(dictionary: aDictionary) + } else if let anArray = value as? [Any] { + return prepare(array: anArray) + } + return value + } + } + + private func prepare(array: [Any]) -> [Any] { + return array.map { (value) -> Any in + if let date = value as? Date { + return JsonValue.formatter.string(from: date) + } else if let aDictionary = value as? JsonObject { + return prepare(dictionary: aDictionary) + } else if let anArray = value as? [Any] { + return prepare(array: anArray) + } + return value + } + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/Logger.swift b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/Logger.swift new file mode 100644 index 000000000..d055090dc --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/Logger.swift @@ -0,0 +1,139 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import os.log +import UIKit +import Foundation + +class StdoutRedirector { + private var originalStdout: Int32 = -1 + private var originalStderr: Int32 = -1 + private var stdoutPipe: [Int32] = [-1, -1] + private var stderrPipe: [Int32] = [-1, -1] + private var stdoutReadSource: DispatchSourceRead? + private var stderrReadSource: DispatchSourceRead? + + func start() { + originalStdout = dup(STDOUT_FILENO) + originalStderr = dup(STDERR_FILENO) + + guard Darwin.pipe(&stdoutPipe) == 0, + Darwin.pipe(&stderrPipe) == 0 else { + Logger.error("Failed to create stdout/stderr pipes") + return + } + + dup2(stdoutPipe[1], STDOUT_FILENO) + dup2(stderrPipe[1], STDERR_FILENO) + close(stdoutPipe[1]) + close(stderrPipe[1]) + + stdoutReadSource = createReader( + readPipe: stdoutPipe[0], + writeToOriginal: originalStdout, + label: "stdout" + ) + + stderrReadSource = createReader( + readPipe: stderrPipe[0], + writeToOriginal: originalStderr, + label: "stderr" + ) + } + + private func createReader( + readPipe: Int32, + writeToOriginal: Int32, + label: String + ) -> DispatchSourceRead { + let source = DispatchSource.makeReadSource( + fileDescriptor: readPipe, + queue: .global(qos: .utility) + ) + + source.setEventHandler { + let bufferSize = 4096 + var buffer = [UInt8](repeating: 0, count: bufferSize) + let bytesRead = read(readPipe, &buffer, bufferSize) + + if bytesRead > 0 { + let output = String( + bytes: buffer[0..(_ type: T.Type) throws -> T { + let jsonData = self.config.data(using: .utf8)! + let decoder = JSONDecoder() + return try decoder.decode(type, from: jsonData) + } + + @objc open func load(webview: WKWebView) {} + + @objc open func checkPermissions(_ invoke: Invoke) { + invoke.resolve() + } + + @objc open func requestPermissions(_ invoke: Invoke) { + invoke.resolve() + } + + public func trigger(_ event: String, data: JSObject) { + if let eventListeners = listeners[event] { + for channel in eventListeners { + channel.send(data) + } + } + } + + public func trigger(_ event: String, data: T) throws { + if let eventListeners = listeners[event] { + for channel in eventListeners { + try channel.send(data) + } + } + } + + @objc func registerListener(_ invoke: Invoke) throws { + let args = try invoke.parseArgs(RegisterListenerArgs.self) + + if var eventListeners = listeners[args.event] { + eventListeners.append(args.handler) + listeners[args.event] = eventListeners + } else { + listeners[args.event] = [args.handler] + } + + invoke.resolve() + } + + @objc func removeListener(_ invoke: Invoke) throws { + let args = try invoke.parseArgs(RemoveListenerArgs.self) + + if let eventListeners = listeners[args.event] { + + listeners[args.event] = eventListeners.filter { $0.id != args.channelId } + } + + invoke.resolve() + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/Tauri.swift b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/Tauri.swift new file mode 100644 index 000000000..8d0a183d7 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/Tauri.swift @@ -0,0 +1,157 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import Foundation +import SwiftRs +import UIKit +import WebKit +import os.log + +class PluginHandle { + var instance: Plugin + var loaded = false + + init(plugin: Plugin) { + instance = plugin + } +} + +public class PluginManager { + static let shared: PluginManager = PluginManager() + public var viewController: UIViewController? + var plugins: [String: PluginHandle] = [:] + var ipcDispatchQueue = DispatchQueue(label: "ipc") + public var isSimEnvironment: Bool { + #if targetEnvironment(simulator) + return true + #else + return false + #endif + } + + public func assetUrl(fromLocalURL url: URL?) -> URL? { + guard let inputURL = url else { + return nil + } + + return URL(string: "asset://localhost")!.appendingPathComponent(inputURL.path) + } + + func onWebviewCreated(_ webview: WKWebView) { + for (_, handle) in plugins { + if !handle.loaded { + handle.instance.load(webview: webview) + } + } + } + + func load(name: String, plugin: P, config: String, webview: WKWebView?) { + plugin.setConfig(config) + let handle = PluginHandle(plugin: plugin) + if let webview = webview { + handle.instance.load(webview: webview) + handle.loaded = true + } + plugins[name] = handle + } + + func invoke(name: String, invoke: Invoke) { + if let plugin = plugins[name] { + ipcDispatchQueue.async { + let selectorWithCompletionHandler = Selector(("\(invoke.command):completionHandler:")) + let selectorWithThrows = Selector(("\(invoke.command):error:")) + + if plugin.instance.responds(to: selectorWithCompletionHandler) { + let completion: @convention(block) (NSError?) -> Void = { error in + if let error = error { + invoke.reject("\(error)") + } + } + + let blockObj: AnyObject = unsafeBitCast(completion, to: AnyObject.self) + let imp = plugin.instance.method(for: selectorWithCompletionHandler) + + typealias Fn = @convention(c) (AnyObject, Selector, Invoke, AnyObject) -> Void + let fn = unsafeBitCast(imp, to: Fn.self) + fn(plugin.instance, selectorWithCompletionHandler, invoke, blockObj) + } else if plugin.instance.responds(to: selectorWithThrows) { + var error: NSError? = nil + withUnsafeMutablePointer(to: &error) { + let methodIMP: IMP! = plugin.instance.method(for: selectorWithThrows) + unsafeBitCast( + methodIMP, to: (@convention(c) (Any?, Selector, Invoke, OpaquePointer) -> Void).self)( + plugin.instance, selectorWithThrows, invoke, OpaquePointer($0)) + } + if let error = error { + invoke.reject("\(error)") + // TODO: app crashes without this leak + let _ = Unmanaged.passRetained(error) + } + } else { + let selector = Selector(("\(invoke.command):")) + if plugin.instance.responds(to: selector) { + plugin.instance.perform(selector, with: invoke) + } else { + invoke.reject("No command \(invoke.command) found for plugin \(name)") + } + } + } + } else { + invoke.reject("Plugin \(name) not initialized") + } + } +} + +extension PluginManager: NSCopying { + public func copy(with zone: NSZone? = nil) -> Any { + return self + } +} + +private var stdoutRedirector: StdoutRedirector? + +@_cdecl("log_stdout") +func logStdout() { + stdoutRedirector = StdoutRedirector() + stdoutRedirector!.start() +} + +@_cdecl("register_plugin") +func registerPlugin(name: SRString, plugin: NSObject, config: SRString, webview: WKWebView?) { + PluginManager.shared.load( + name: name.toString(), + plugin: plugin as! Plugin, + config: config.toString(), + webview: webview + ) +} + +@_cdecl("on_webview_created") +func onWebviewCreated(webview: WKWebView, viewController: UIViewController) { + PluginManager.shared.viewController = viewController + PluginManager.shared.onWebviewCreated(webview) +} + +@_cdecl("run_plugin_command") +func runCommand( + id: Int32, + name: SRString, + command: SRString, + data: SRString, + callback: @escaping @convention(c) (Int32, Int32, UnsafePointer) -> Void, + sendChannelData: @escaping @convention(c) (UInt64, UnsafePointer) -> Void +) { + let callbackId: UInt64 = 0 + let errorId: UInt64 = 1 + let invoke = Invoke( + command: command.toString(), callback: callbackId, error: errorId, + sendResponse: { (fn: UInt64, payload: String?) -> Void in + let success = fn == callbackId + callback(id, success ? 1 : 0, payload ?? "null") + }, + sendChannelData: { (id: UInt64, payload: String) -> Void in + sendChannelData(id, payload) + }, data: data.toString()) + PluginManager.shared.invoke(name: name.toString(), invoke: invoke) +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/UiUtils.swift b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/UiUtils.swift new file mode 100644 index 000000000..4097596e2 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/UiUtils.swift @@ -0,0 +1,15 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import UIKit + +public class UIUtils { + public static func centerPopover(rootViewController: UIViewController?, popoverController: UIViewController) { + if let viewController = rootViewController { + popoverController.popoverPresentationController?.sourceRect = CGRect(x: viewController.view.center.x, y: viewController.view.center.y, width: 0, height: 0) + popoverController.popoverPresentationController?.sourceView = viewController.view + popoverController.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection.up + } + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Tests/TauriTests/TauriTests.swift b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Tests/TauriTests/TauriTests.swift new file mode 100644 index 000000000..49f6ee223 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Tests/TauriTests/TauriTests.swift @@ -0,0 +1,15 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import XCTest +@testable import Tauri + +final class TauriTests: XCTestCase { + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + XCTAssertEqual(Tauri().text, "Hello, World!") + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/proguard-tauri.pro b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/proguard-tauri.pro new file mode 100644 index 000000000..6bc0b4b1c --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/mobile/proguard-tauri.pro @@ -0,0 +1,5 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. + +-keep class $PACKAGE.TauriActivity { + public app.tauri.plugin.PluginManager getPluginManager(); +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/app/autogenerated/reference.md b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/app/autogenerated/reference.md new file mode 100644 index 000000000..d6e5858c3 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/app/autogenerated/reference.md @@ -0,0 +1,440 @@ +## Default Permission + +Default permissions for the plugin. + +#### This default permission set includes the following: + +- `allow-version` +- `allow-name` +- `allow-tauri-version` +- `allow-identifier` +- `allow-bundle-type` +- `allow-register-listener` +- `allow-remove-listener` +- `allow-supports-multiple-windows` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`core:app:allow-app-hide` + + + +Enables the app_hide command without any pre-configured scope. + +
+ +`core:app:deny-app-hide` + + + +Denies the app_hide command without any pre-configured scope. + +
+ +`core:app:allow-app-show` + + + +Enables the app_show command without any pre-configured scope. + +
+ +`core:app:deny-app-show` + + + +Denies the app_show command without any pre-configured scope. + +
+ +`core:app:allow-bundle-type` + + + +Enables the bundle_type command without any pre-configured scope. + +
+ +`core:app:deny-bundle-type` + + + +Denies the bundle_type command without any pre-configured scope. + +
+ +`core:app:allow-default-window-icon` + + + +Enables the default_window_icon command without any pre-configured scope. + +
+ +`core:app:deny-default-window-icon` + + + +Denies the default_window_icon command without any pre-configured scope. + +
+ +`core:app:allow-exit` + + + +Enables the exit command without any pre-configured scope. + +
+ +`core:app:deny-exit` + + + +Denies the exit command without any pre-configured scope. + +
+ +`core:app:allow-fetch-data-store-identifiers` + + + +Enables the fetch_data_store_identifiers command without any pre-configured scope. + +
+ +`core:app:deny-fetch-data-store-identifiers` + + + +Denies the fetch_data_store_identifiers command without any pre-configured scope. + +
+ +`core:app:allow-identifier` + + + +Enables the identifier command without any pre-configured scope. + +
+ +`core:app:deny-identifier` + + + +Denies the identifier command without any pre-configured scope. + +
+ +`core:app:allow-name` + + + +Enables the name command without any pre-configured scope. + +
+ +`core:app:deny-name` + + + +Denies the name command without any pre-configured scope. + +
+ +`core:app:allow-register-listener` + + + +Enables the register_listener command without any pre-configured scope. + +
+ +`core:app:deny-register-listener` + + + +Denies the register_listener command without any pre-configured scope. + +
+ +`core:app:allow-remove-data-store` + + + +Enables the remove_data_store command without any pre-configured scope. + +
+ +`core:app:deny-remove-data-store` + + + +Denies the remove_data_store command without any pre-configured scope. + +
+ +`core:app:allow-remove-listener` + + + +Enables the remove_listener command without any pre-configured scope. + +
+ +`core:app:deny-remove-listener` + + + +Denies the remove_listener command without any pre-configured scope. + +
+ +`core:app:allow-set-app-theme` + + + +Enables the set_app_theme command without any pre-configured scope. + +
+ +`core:app:deny-set-app-theme` + + + +Denies the set_app_theme command without any pre-configured scope. + +
+ +`core:app:allow-set-dock-visibility` + + + +Enables the set_dock_visibility command without any pre-configured scope. + +
+ +`core:app:deny-set-dock-visibility` + + + +Denies the set_dock_visibility command without any pre-configured scope. + +
+ +`core:app:allow-supports-multiple-windows` + + + +Enables the supports_multiple_windows command without any pre-configured scope. + +
+ +`core:app:deny-supports-multiple-windows` + + + +Denies the supports_multiple_windows command without any pre-configured scope. + +
+ +`core:app:allow-tauri-version` + + + +Enables the tauri_version command without any pre-configured scope. + +
+ +`core:app:deny-tauri-version` + + + +Denies the tauri_version command without any pre-configured scope. + +
+ +`core:app:allow-version` + + + +Enables the version command without any pre-configured scope. + +
+ +`core:app:deny-version` + + + +Denies the version command without any pre-configured scope. + +
diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/event/autogenerated/reference.md b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/event/autogenerated/reference.md new file mode 100644 index 000000000..1907b4ac8 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/event/autogenerated/reference.md @@ -0,0 +1,124 @@ +## Default Permission + +Default permissions for the plugin, which enables all commands. + +#### This default permission set includes the following: + +- `allow-listen` +- `allow-unlisten` +- `allow-emit` +- `allow-emit-to` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`core:event:allow-emit` + + + +Enables the emit command without any pre-configured scope. + +
+ +`core:event:deny-emit` + + + +Denies the emit command without any pre-configured scope. + +
+ +`core:event:allow-emit-to` + + + +Enables the emit_to command without any pre-configured scope. + +
+ +`core:event:deny-emit-to` + + + +Denies the emit_to command without any pre-configured scope. + +
+ +`core:event:allow-listen` + + + +Enables the listen command without any pre-configured scope. + +
+ +`core:event:deny-listen` + + + +Denies the listen command without any pre-configured scope. + +
+ +`core:event:allow-unlisten` + + + +Enables the unlisten command without any pre-configured scope. + +
+ +`core:event:deny-unlisten` + + + +Denies the unlisten command without any pre-configured scope. + +
diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/image/autogenerated/reference.md b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/image/autogenerated/reference.md new file mode 100644 index 000000000..f66c34cb6 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/image/autogenerated/reference.md @@ -0,0 +1,151 @@ +## Default Permission + +Default permissions for the plugin, which enables all commands. + +#### This default permission set includes the following: + +- `allow-new` +- `allow-from-bytes` +- `allow-from-path` +- `allow-rgba` +- `allow-size` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`core:image:allow-from-bytes` + + + +Enables the from_bytes command without any pre-configured scope. + +
+ +`core:image:deny-from-bytes` + + + +Denies the from_bytes command without any pre-configured scope. + +
+ +`core:image:allow-from-path` + + + +Enables the from_path command without any pre-configured scope. + +
+ +`core:image:deny-from-path` + + + +Denies the from_path command without any pre-configured scope. + +
+ +`core:image:allow-new` + + + +Enables the new command without any pre-configured scope. + +
+ +`core:image:deny-new` + + + +Denies the new command without any pre-configured scope. + +
+ +`core:image:allow-rgba` + + + +Enables the rgba command without any pre-configured scope. + +
+ +`core:image:deny-rgba` + + + +Denies the rgba command without any pre-configured scope. + +
+ +`core:image:allow-size` + + + +Enables the size command without any pre-configured scope. + +
+ +`core:image:deny-size` + + + +Denies the size command without any pre-configured scope. + +
diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/menu/autogenerated/reference.md b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/menu/autogenerated/reference.md new file mode 100644 index 000000000..4638fa9b2 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/menu/autogenerated/reference.md @@ -0,0 +1,610 @@ +## Default Permission + +Default permissions for the plugin, which enables all commands. + +#### This default permission set includes the following: + +- `allow-new` +- `allow-append` +- `allow-prepend` +- `allow-insert` +- `allow-remove` +- `allow-remove-at` +- `allow-items` +- `allow-get` +- `allow-popup` +- `allow-create-default` +- `allow-set-as-app-menu` +- `allow-set-as-window-menu` +- `allow-text` +- `allow-set-text` +- `allow-is-enabled` +- `allow-set-enabled` +- `allow-set-accelerator` +- `allow-set-as-windows-menu-for-nsapp` +- `allow-set-as-help-menu-for-nsapp` +- `allow-is-checked` +- `allow-set-checked` +- `allow-set-icon` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`core:menu:allow-append` + + + +Enables the append command without any pre-configured scope. + +
+ +`core:menu:deny-append` + + + +Denies the append command without any pre-configured scope. + +
+ +`core:menu:allow-create-default` + + + +Enables the create_default command without any pre-configured scope. + +
+ +`core:menu:deny-create-default` + + + +Denies the create_default command without any pre-configured scope. + +
+ +`core:menu:allow-get` + + + +Enables the get command without any pre-configured scope. + +
+ +`core:menu:deny-get` + + + +Denies the get command without any pre-configured scope. + +
+ +`core:menu:allow-insert` + + + +Enables the insert command without any pre-configured scope. + +
+ +`core:menu:deny-insert` + + + +Denies the insert command without any pre-configured scope. + +
+ +`core:menu:allow-is-checked` + + + +Enables the is_checked command without any pre-configured scope. + +
+ +`core:menu:deny-is-checked` + + + +Denies the is_checked command without any pre-configured scope. + +
+ +`core:menu:allow-is-enabled` + + + +Enables the is_enabled command without any pre-configured scope. + +
+ +`core:menu:deny-is-enabled` + + + +Denies the is_enabled command without any pre-configured scope. + +
+ +`core:menu:allow-items` + + + +Enables the items command without any pre-configured scope. + +
+ +`core:menu:deny-items` + + + +Denies the items command without any pre-configured scope. + +
+ +`core:menu:allow-new` + + + +Enables the new command without any pre-configured scope. + +
+ +`core:menu:deny-new` + + + +Denies the new command without any pre-configured scope. + +
+ +`core:menu:allow-popup` + + + +Enables the popup command without any pre-configured scope. + +
+ +`core:menu:deny-popup` + + + +Denies the popup command without any pre-configured scope. + +
+ +`core:menu:allow-prepend` + + + +Enables the prepend command without any pre-configured scope. + +
+ +`core:menu:deny-prepend` + + + +Denies the prepend command without any pre-configured scope. + +
+ +`core:menu:allow-remove` + + + +Enables the remove command without any pre-configured scope. + +
+ +`core:menu:deny-remove` + + + +Denies the remove command without any pre-configured scope. + +
+ +`core:menu:allow-remove-at` + + + +Enables the remove_at command without any pre-configured scope. + +
+ +`core:menu:deny-remove-at` + + + +Denies the remove_at command without any pre-configured scope. + +
+ +`core:menu:allow-set-accelerator` + + + +Enables the set_accelerator command without any pre-configured scope. + +
+ +`core:menu:deny-set-accelerator` + + + +Denies the set_accelerator command without any pre-configured scope. + +
+ +`core:menu:allow-set-as-app-menu` + + + +Enables the set_as_app_menu command without any pre-configured scope. + +
+ +`core:menu:deny-set-as-app-menu` + + + +Denies the set_as_app_menu command without any pre-configured scope. + +
+ +`core:menu:allow-set-as-help-menu-for-nsapp` + + + +Enables the set_as_help_menu_for_nsapp command without any pre-configured scope. + +
+ +`core:menu:deny-set-as-help-menu-for-nsapp` + + + +Denies the set_as_help_menu_for_nsapp command without any pre-configured scope. + +
+ +`core:menu:allow-set-as-window-menu` + + + +Enables the set_as_window_menu command without any pre-configured scope. + +
+ +`core:menu:deny-set-as-window-menu` + + + +Denies the set_as_window_menu command without any pre-configured scope. + +
+ +`core:menu:allow-set-as-windows-menu-for-nsapp` + + + +Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope. + +
+ +`core:menu:deny-set-as-windows-menu-for-nsapp` + + + +Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope. + +
+ +`core:menu:allow-set-checked` + + + +Enables the set_checked command without any pre-configured scope. + +
+ +`core:menu:deny-set-checked` + + + +Denies the set_checked command without any pre-configured scope. + +
+ +`core:menu:allow-set-enabled` + + + +Enables the set_enabled command without any pre-configured scope. + +
+ +`core:menu:deny-set-enabled` + + + +Denies the set_enabled command without any pre-configured scope. + +
+ +`core:menu:allow-set-icon` + + + +Enables the set_icon command without any pre-configured scope. + +
+ +`core:menu:deny-set-icon` + + + +Denies the set_icon command without any pre-configured scope. + +
+ +`core:menu:allow-set-text` + + + +Enables the set_text command without any pre-configured scope. + +
+ +`core:menu:deny-set-text` + + + +Denies the set_text command without any pre-configured scope. + +
+ +`core:menu:allow-text` + + + +Enables the text command without any pre-configured scope. + +
+ +`core:menu:deny-text` + + + +Denies the text command without any pre-configured scope. + +
diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/path/autogenerated/reference.md b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/path/autogenerated/reference.md new file mode 100644 index 000000000..fe6ec6f3d --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/path/autogenerated/reference.md @@ -0,0 +1,232 @@ +## Default Permission + +Default permissions for the plugin, which enables all commands. + +#### This default permission set includes the following: + +- `allow-resolve-directory` +- `allow-resolve` +- `allow-normalize` +- `allow-join` +- `allow-dirname` +- `allow-extname` +- `allow-basename` +- `allow-is-absolute` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`core:path:allow-basename` + + + +Enables the basename command without any pre-configured scope. + +
+ +`core:path:deny-basename` + + + +Denies the basename command without any pre-configured scope. + +
+ +`core:path:allow-dirname` + + + +Enables the dirname command without any pre-configured scope. + +
+ +`core:path:deny-dirname` + + + +Denies the dirname command without any pre-configured scope. + +
+ +`core:path:allow-extname` + + + +Enables the extname command without any pre-configured scope. + +
+ +`core:path:deny-extname` + + + +Denies the extname command without any pre-configured scope. + +
+ +`core:path:allow-is-absolute` + + + +Enables the is_absolute command without any pre-configured scope. + +
+ +`core:path:deny-is-absolute` + + + +Denies the is_absolute command without any pre-configured scope. + +
+ +`core:path:allow-join` + + + +Enables the join command without any pre-configured scope. + +
+ +`core:path:deny-join` + + + +Denies the join command without any pre-configured scope. + +
+ +`core:path:allow-normalize` + + + +Enables the normalize command without any pre-configured scope. + +
+ +`core:path:deny-normalize` + + + +Denies the normalize command without any pre-configured scope. + +
+ +`core:path:allow-resolve` + + + +Enables the resolve command without any pre-configured scope. + +
+ +`core:path:deny-resolve` + + + +Denies the resolve command without any pre-configured scope. + +
+ +`core:path:allow-resolve-directory` + + + +Enables the resolve_directory command without any pre-configured scope. + +
+ +`core:path:deny-resolve-directory` + + + +Denies the resolve_directory command without any pre-configured scope. + +
diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/resources/autogenerated/reference.md b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/resources/autogenerated/reference.md new file mode 100644 index 000000000..42fdf1b28 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/resources/autogenerated/reference.md @@ -0,0 +1,43 @@ +## Default Permission + +Default permissions for the plugin, which enables all commands. + +#### This default permission set includes the following: + +- `allow-close` + +## Permission Table + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`core:resources:allow-close` + + + +Enables the close command without any pre-configured scope. + +
+ +`core:resources:deny-close` + + + +Denies the close command without any pre-configured scope. + +
diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/tray/autogenerated/reference.md b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/tray/autogenerated/reference.md new file mode 100644 index 000000000..341858892 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/tray/autogenerated/reference.md @@ -0,0 +1,340 @@ +## Default Permission + +Default permissions for the plugin, which enables all commands. + +#### This default permission set includes the following: + +- `allow-new` +- `allow-get-by-id` +- `allow-remove-by-id` +- `allow-set-icon` +- `allow-set-menu` +- `allow-set-tooltip` +- `allow-set-title` +- `allow-set-visible` +- `allow-set-temp-dir-path` +- `allow-set-icon-as-template` +- `allow-set-icon-with-as-template` +- `allow-set-show-menu-on-left-click` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`core:tray:allow-get-by-id` + + + +Enables the get_by_id command without any pre-configured scope. + +
+ +`core:tray:deny-get-by-id` + + + +Denies the get_by_id command without any pre-configured scope. + +
+ +`core:tray:allow-new` + + + +Enables the new command without any pre-configured scope. + +
+ +`core:tray:deny-new` + + + +Denies the new command without any pre-configured scope. + +
+ +`core:tray:allow-remove-by-id` + + + +Enables the remove_by_id command without any pre-configured scope. + +
+ +`core:tray:deny-remove-by-id` + + + +Denies the remove_by_id command without any pre-configured scope. + +
+ +`core:tray:allow-set-icon` + + + +Enables the set_icon command without any pre-configured scope. + +
+ +`core:tray:deny-set-icon` + + + +Denies the set_icon command without any pre-configured scope. + +
+ +`core:tray:allow-set-icon-as-template` + + + +Enables the set_icon_as_template command without any pre-configured scope. + +
+ +`core:tray:deny-set-icon-as-template` + + + +Denies the set_icon_as_template command without any pre-configured scope. + +
+ +`core:tray:allow-set-icon-with-as-template` + + + +Enables the set_icon_with_as_template command without any pre-configured scope. + +
+ +`core:tray:deny-set-icon-with-as-template` + + + +Denies the set_icon_with_as_template command without any pre-configured scope. + +
+ +`core:tray:allow-set-menu` + + + +Enables the set_menu command without any pre-configured scope. + +
+ +`core:tray:deny-set-menu` + + + +Denies the set_menu command without any pre-configured scope. + +
+ +`core:tray:allow-set-show-menu-on-left-click` + + + +Enables the set_show_menu_on_left_click command without any pre-configured scope. + +
+ +`core:tray:deny-set-show-menu-on-left-click` + + + +Denies the set_show_menu_on_left_click command without any pre-configured scope. + +
+ +`core:tray:allow-set-temp-dir-path` + + + +Enables the set_temp_dir_path command without any pre-configured scope. + +
+ +`core:tray:deny-set-temp-dir-path` + + + +Denies the set_temp_dir_path command without any pre-configured scope. + +
+ +`core:tray:allow-set-title` + + + +Enables the set_title command without any pre-configured scope. + +
+ +`core:tray:deny-set-title` + + + +Denies the set_title command without any pre-configured scope. + +
+ +`core:tray:allow-set-tooltip` + + + +Enables the set_tooltip command without any pre-configured scope. + +
+ +`core:tray:deny-set-tooltip` + + + +Denies the set_tooltip command without any pre-configured scope. + +
+ +`core:tray:allow-set-visible` + + + +Enables the set_visible command without any pre-configured scope. + +
+ +`core:tray:deny-set-visible` + + + +Denies the set_visible command without any pre-configured scope. + +
diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/webview/autogenerated/reference.md b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/webview/autogenerated/reference.md new file mode 100644 index 000000000..7bad5227a --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/webview/autogenerated/reference.md @@ -0,0 +1,488 @@ +## Default Permission + +Default permissions for the plugin. + +#### This default permission set includes the following: + +- `allow-get-all-webviews` +- `allow-webview-position` +- `allow-webview-size` +- `allow-internal-toggle-devtools` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`core:webview:allow-clear-all-browsing-data` + + + +Enables the clear_all_browsing_data command without any pre-configured scope. + +
+ +`core:webview:deny-clear-all-browsing-data` + + + +Denies the clear_all_browsing_data command without any pre-configured scope. + +
+ +`core:webview:allow-create-webview` + + + +Enables the create_webview command without any pre-configured scope. + +
+ +`core:webview:deny-create-webview` + + + +Denies the create_webview command without any pre-configured scope. + +
+ +`core:webview:allow-create-webview-window` + + + +Enables the create_webview_window command without any pre-configured scope. + +
+ +`core:webview:deny-create-webview-window` + + + +Denies the create_webview_window command without any pre-configured scope. + +
+ +`core:webview:allow-get-all-webviews` + + + +Enables the get_all_webviews command without any pre-configured scope. + +
+ +`core:webview:deny-get-all-webviews` + + + +Denies the get_all_webviews command without any pre-configured scope. + +
+ +`core:webview:allow-internal-toggle-devtools` + + + +Enables the internal_toggle_devtools command without any pre-configured scope. + +
+ +`core:webview:deny-internal-toggle-devtools` + + + +Denies the internal_toggle_devtools command without any pre-configured scope. + +
+ +`core:webview:allow-print` + + + +Enables the print command without any pre-configured scope. + +
+ +`core:webview:deny-print` + + + +Denies the print command without any pre-configured scope. + +
+ +`core:webview:allow-reparent` + + + +Enables the reparent command without any pre-configured scope. + +
+ +`core:webview:deny-reparent` + + + +Denies the reparent command without any pre-configured scope. + +
+ +`core:webview:allow-set-webview-auto-resize` + + + +Enables the set_webview_auto_resize command without any pre-configured scope. + +
+ +`core:webview:deny-set-webview-auto-resize` + + + +Denies the set_webview_auto_resize command without any pre-configured scope. + +
+ +`core:webview:allow-set-webview-background-color` + + + +Enables the set_webview_background_color command without any pre-configured scope. + +
+ +`core:webview:deny-set-webview-background-color` + + + +Denies the set_webview_background_color command without any pre-configured scope. + +
+ +`core:webview:allow-set-webview-focus` + + + +Enables the set_webview_focus command without any pre-configured scope. + +
+ +`core:webview:deny-set-webview-focus` + + + +Denies the set_webview_focus command without any pre-configured scope. + +
+ +`core:webview:allow-set-webview-position` + + + +Enables the set_webview_position command without any pre-configured scope. + +
+ +`core:webview:deny-set-webview-position` + + + +Denies the set_webview_position command without any pre-configured scope. + +
+ +`core:webview:allow-set-webview-size` + + + +Enables the set_webview_size command without any pre-configured scope. + +
+ +`core:webview:deny-set-webview-size` + + + +Denies the set_webview_size command without any pre-configured scope. + +
+ +`core:webview:allow-set-webview-zoom` + + + +Enables the set_webview_zoom command without any pre-configured scope. + +
+ +`core:webview:deny-set-webview-zoom` + + + +Denies the set_webview_zoom command without any pre-configured scope. + +
+ +`core:webview:allow-webview-close` + + + +Enables the webview_close command without any pre-configured scope. + +
+ +`core:webview:deny-webview-close` + + + +Denies the webview_close command without any pre-configured scope. + +
+ +`core:webview:allow-webview-hide` + + + +Enables the webview_hide command without any pre-configured scope. + +
+ +`core:webview:deny-webview-hide` + + + +Denies the webview_hide command without any pre-configured scope. + +
+ +`core:webview:allow-webview-position` + + + +Enables the webview_position command without any pre-configured scope. + +
+ +`core:webview:deny-webview-position` + + + +Denies the webview_position command without any pre-configured scope. + +
+ +`core:webview:allow-webview-show` + + + +Enables the webview_show command without any pre-configured scope. + +
+ +`core:webview:deny-webview-show` + + + +Denies the webview_show command without any pre-configured scope. + +
+ +`core:webview:allow-webview-size` + + + +Enables the webview_size command without any pre-configured scope. + +
+ +`core:webview:deny-webview-size` + + + +Denies the webview_size command without any pre-configured scope. + +
diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/window/autogenerated/reference.md b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/window/autogenerated/reference.md new file mode 100644 index 000000000..585943afd --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/permissions/window/autogenerated/reference.md @@ -0,0 +1,2098 @@ +## Default Permission + +Default permissions for the plugin. + +#### This default permission set includes the following: + +- `allow-get-all-windows` +- `allow-scale-factor` +- `allow-inner-position` +- `allow-outer-position` +- `allow-inner-size` +- `allow-outer-size` +- `allow-is-fullscreen` +- `allow-is-minimized` +- `allow-is-maximized` +- `allow-is-focused` +- `allow-is-decorated` +- `allow-is-resizable` +- `allow-is-maximizable` +- `allow-is-minimizable` +- `allow-is-closable` +- `allow-is-visible` +- `allow-is-enabled` +- `allow-title` +- `allow-current-monitor` +- `allow-primary-monitor` +- `allow-monitor-from-point` +- `allow-available-monitors` +- `allow-cursor-position` +- `allow-theme` +- `allow-is-always-on-top` +- `allow-activity-name` +- `allow-scene-identifier` +- `allow-internal-toggle-maximize` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`core:window:allow-activity-name` + + + +Enables the activity_name command without any pre-configured scope. + +
+ +`core:window:deny-activity-name` + + + +Denies the activity_name command without any pre-configured scope. + +
+ +`core:window:allow-available-monitors` + + + +Enables the available_monitors command without any pre-configured scope. + +
+ +`core:window:deny-available-monitors` + + + +Denies the available_monitors command without any pre-configured scope. + +
+ +`core:window:allow-center` + + + +Enables the center command without any pre-configured scope. + +
+ +`core:window:deny-center` + + + +Denies the center command without any pre-configured scope. + +
+ +`core:window:allow-close` + + + +Enables the close command without any pre-configured scope. + +
+ +`core:window:deny-close` + + + +Denies the close command without any pre-configured scope. + +
+ +`core:window:allow-create` + + + +Enables the create command without any pre-configured scope. + +
+ +`core:window:deny-create` + + + +Denies the create command without any pre-configured scope. + +
+ +`core:window:allow-current-monitor` + + + +Enables the current_monitor command without any pre-configured scope. + +
+ +`core:window:deny-current-monitor` + + + +Denies the current_monitor command without any pre-configured scope. + +
+ +`core:window:allow-cursor-position` + + + +Enables the cursor_position command without any pre-configured scope. + +
+ +`core:window:deny-cursor-position` + + + +Denies the cursor_position command without any pre-configured scope. + +
+ +`core:window:allow-destroy` + + + +Enables the destroy command without any pre-configured scope. + +
+ +`core:window:deny-destroy` + + + +Denies the destroy command without any pre-configured scope. + +
+ +`core:window:allow-get-all-windows` + + + +Enables the get_all_windows command without any pre-configured scope. + +
+ +`core:window:deny-get-all-windows` + + + +Denies the get_all_windows command without any pre-configured scope. + +
+ +`core:window:allow-hide` + + + +Enables the hide command without any pre-configured scope. + +
+ +`core:window:deny-hide` + + + +Denies the hide command without any pre-configured scope. + +
+ +`core:window:allow-inner-position` + + + +Enables the inner_position command without any pre-configured scope. + +
+ +`core:window:deny-inner-position` + + + +Denies the inner_position command without any pre-configured scope. + +
+ +`core:window:allow-inner-size` + + + +Enables the inner_size command without any pre-configured scope. + +
+ +`core:window:deny-inner-size` + + + +Denies the inner_size command without any pre-configured scope. + +
+ +`core:window:allow-internal-toggle-maximize` + + + +Enables the internal_toggle_maximize command without any pre-configured scope. + +
+ +`core:window:deny-internal-toggle-maximize` + + + +Denies the internal_toggle_maximize command without any pre-configured scope. + +
+ +`core:window:allow-is-always-on-top` + + + +Enables the is_always_on_top command without any pre-configured scope. + +
+ +`core:window:deny-is-always-on-top` + + + +Denies the is_always_on_top command without any pre-configured scope. + +
+ +`core:window:allow-is-closable` + + + +Enables the is_closable command without any pre-configured scope. + +
+ +`core:window:deny-is-closable` + + + +Denies the is_closable command without any pre-configured scope. + +
+ +`core:window:allow-is-decorated` + + + +Enables the is_decorated command without any pre-configured scope. + +
+ +`core:window:deny-is-decorated` + + + +Denies the is_decorated command without any pre-configured scope. + +
+ +`core:window:allow-is-enabled` + + + +Enables the is_enabled command without any pre-configured scope. + +
+ +`core:window:deny-is-enabled` + + + +Denies the is_enabled command without any pre-configured scope. + +
+ +`core:window:allow-is-focused` + + + +Enables the is_focused command without any pre-configured scope. + +
+ +`core:window:deny-is-focused` + + + +Denies the is_focused command without any pre-configured scope. + +
+ +`core:window:allow-is-fullscreen` + + + +Enables the is_fullscreen command without any pre-configured scope. + +
+ +`core:window:deny-is-fullscreen` + + + +Denies the is_fullscreen command without any pre-configured scope. + +
+ +`core:window:allow-is-maximizable` + + + +Enables the is_maximizable command without any pre-configured scope. + +
+ +`core:window:deny-is-maximizable` + + + +Denies the is_maximizable command without any pre-configured scope. + +
+ +`core:window:allow-is-maximized` + + + +Enables the is_maximized command without any pre-configured scope. + +
+ +`core:window:deny-is-maximized` + + + +Denies the is_maximized command without any pre-configured scope. + +
+ +`core:window:allow-is-minimizable` + + + +Enables the is_minimizable command without any pre-configured scope. + +
+ +`core:window:deny-is-minimizable` + + + +Denies the is_minimizable command without any pre-configured scope. + +
+ +`core:window:allow-is-minimized` + + + +Enables the is_minimized command without any pre-configured scope. + +
+ +`core:window:deny-is-minimized` + + + +Denies the is_minimized command without any pre-configured scope. + +
+ +`core:window:allow-is-resizable` + + + +Enables the is_resizable command without any pre-configured scope. + +
+ +`core:window:deny-is-resizable` + + + +Denies the is_resizable command without any pre-configured scope. + +
+ +`core:window:allow-is-visible` + + + +Enables the is_visible command without any pre-configured scope. + +
+ +`core:window:deny-is-visible` + + + +Denies the is_visible command without any pre-configured scope. + +
+ +`core:window:allow-maximize` + + + +Enables the maximize command without any pre-configured scope. + +
+ +`core:window:deny-maximize` + + + +Denies the maximize command without any pre-configured scope. + +
+ +`core:window:allow-minimize` + + + +Enables the minimize command without any pre-configured scope. + +
+ +`core:window:deny-minimize` + + + +Denies the minimize command without any pre-configured scope. + +
+ +`core:window:allow-monitor-from-point` + + + +Enables the monitor_from_point command without any pre-configured scope. + +
+ +`core:window:deny-monitor-from-point` + + + +Denies the monitor_from_point command without any pre-configured scope. + +
+ +`core:window:allow-outer-position` + + + +Enables the outer_position command without any pre-configured scope. + +
+ +`core:window:deny-outer-position` + + + +Denies the outer_position command without any pre-configured scope. + +
+ +`core:window:allow-outer-size` + + + +Enables the outer_size command without any pre-configured scope. + +
+ +`core:window:deny-outer-size` + + + +Denies the outer_size command without any pre-configured scope. + +
+ +`core:window:allow-primary-monitor` + + + +Enables the primary_monitor command without any pre-configured scope. + +
+ +`core:window:deny-primary-monitor` + + + +Denies the primary_monitor command without any pre-configured scope. + +
+ +`core:window:allow-request-user-attention` + + + +Enables the request_user_attention command without any pre-configured scope. + +
+ +`core:window:deny-request-user-attention` + + + +Denies the request_user_attention command without any pre-configured scope. + +
+ +`core:window:allow-scale-factor` + + + +Enables the scale_factor command without any pre-configured scope. + +
+ +`core:window:deny-scale-factor` + + + +Denies the scale_factor command without any pre-configured scope. + +
+ +`core:window:allow-scene-identifier` + + + +Enables the scene_identifier command without any pre-configured scope. + +
+ +`core:window:deny-scene-identifier` + + + +Denies the scene_identifier command without any pre-configured scope. + +
+ +`core:window:allow-set-always-on-bottom` + + + +Enables the set_always_on_bottom command without any pre-configured scope. + +
+ +`core:window:deny-set-always-on-bottom` + + + +Denies the set_always_on_bottom command without any pre-configured scope. + +
+ +`core:window:allow-set-always-on-top` + + + +Enables the set_always_on_top command without any pre-configured scope. + +
+ +`core:window:deny-set-always-on-top` + + + +Denies the set_always_on_top command without any pre-configured scope. + +
+ +`core:window:allow-set-background-color` + + + +Enables the set_background_color command without any pre-configured scope. + +
+ +`core:window:deny-set-background-color` + + + +Denies the set_background_color command without any pre-configured scope. + +
+ +`core:window:allow-set-badge-count` + + + +Enables the set_badge_count command without any pre-configured scope. + +
+ +`core:window:deny-set-badge-count` + + + +Denies the set_badge_count command without any pre-configured scope. + +
+ +`core:window:allow-set-badge-label` + + + +Enables the set_badge_label command without any pre-configured scope. + +
+ +`core:window:deny-set-badge-label` + + + +Denies the set_badge_label command without any pre-configured scope. + +
+ +`core:window:allow-set-closable` + + + +Enables the set_closable command without any pre-configured scope. + +
+ +`core:window:deny-set-closable` + + + +Denies the set_closable command without any pre-configured scope. + +
+ +`core:window:allow-set-content-protected` + + + +Enables the set_content_protected command without any pre-configured scope. + +
+ +`core:window:deny-set-content-protected` + + + +Denies the set_content_protected command without any pre-configured scope. + +
+ +`core:window:allow-set-cursor-grab` + + + +Enables the set_cursor_grab command without any pre-configured scope. + +
+ +`core:window:deny-set-cursor-grab` + + + +Denies the set_cursor_grab command without any pre-configured scope. + +
+ +`core:window:allow-set-cursor-icon` + + + +Enables the set_cursor_icon command without any pre-configured scope. + +
+ +`core:window:deny-set-cursor-icon` + + + +Denies the set_cursor_icon command without any pre-configured scope. + +
+ +`core:window:allow-set-cursor-position` + + + +Enables the set_cursor_position command without any pre-configured scope. + +
+ +`core:window:deny-set-cursor-position` + + + +Denies the set_cursor_position command without any pre-configured scope. + +
+ +`core:window:allow-set-cursor-visible` + + + +Enables the set_cursor_visible command without any pre-configured scope. + +
+ +`core:window:deny-set-cursor-visible` + + + +Denies the set_cursor_visible command without any pre-configured scope. + +
+ +`core:window:allow-set-decorations` + + + +Enables the set_decorations command without any pre-configured scope. + +
+ +`core:window:deny-set-decorations` + + + +Denies the set_decorations command without any pre-configured scope. + +
+ +`core:window:allow-set-effects` + + + +Enables the set_effects command without any pre-configured scope. + +
+ +`core:window:deny-set-effects` + + + +Denies the set_effects command without any pre-configured scope. + +
+ +`core:window:allow-set-enabled` + + + +Enables the set_enabled command without any pre-configured scope. + +
+ +`core:window:deny-set-enabled` + + + +Denies the set_enabled command without any pre-configured scope. + +
+ +`core:window:allow-set-focus` + + + +Enables the set_focus command without any pre-configured scope. + +
+ +`core:window:deny-set-focus` + + + +Denies the set_focus command without any pre-configured scope. + +
+ +`core:window:allow-set-focusable` + + + +Enables the set_focusable command without any pre-configured scope. + +
+ +`core:window:deny-set-focusable` + + + +Denies the set_focusable command without any pre-configured scope. + +
+ +`core:window:allow-set-fullscreen` + + + +Enables the set_fullscreen command without any pre-configured scope. + +
+ +`core:window:deny-set-fullscreen` + + + +Denies the set_fullscreen command without any pre-configured scope. + +
+ +`core:window:allow-set-fullscreen-on-monitor` + + + +Enables the set_fullscreen_on_monitor command without any pre-configured scope. + +
+ +`core:window:deny-set-fullscreen-on-monitor` + + + +Denies the set_fullscreen_on_monitor command without any pre-configured scope. + +
+ +`core:window:allow-set-icon` + + + +Enables the set_icon command without any pre-configured scope. + +
+ +`core:window:deny-set-icon` + + + +Denies the set_icon command without any pre-configured scope. + +
+ +`core:window:allow-set-ignore-cursor-events` + + + +Enables the set_ignore_cursor_events command without any pre-configured scope. + +
+ +`core:window:deny-set-ignore-cursor-events` + + + +Denies the set_ignore_cursor_events command without any pre-configured scope. + +
+ +`core:window:allow-set-max-size` + + + +Enables the set_max_size command without any pre-configured scope. + +
+ +`core:window:deny-set-max-size` + + + +Denies the set_max_size command without any pre-configured scope. + +
+ +`core:window:allow-set-maximizable` + + + +Enables the set_maximizable command without any pre-configured scope. + +
+ +`core:window:deny-set-maximizable` + + + +Denies the set_maximizable command without any pre-configured scope. + +
+ +`core:window:allow-set-min-size` + + + +Enables the set_min_size command without any pre-configured scope. + +
+ +`core:window:deny-set-min-size` + + + +Denies the set_min_size command without any pre-configured scope. + +
+ +`core:window:allow-set-minimizable` + + + +Enables the set_minimizable command without any pre-configured scope. + +
+ +`core:window:deny-set-minimizable` + + + +Denies the set_minimizable command without any pre-configured scope. + +
+ +`core:window:allow-set-overlay-icon` + + + +Enables the set_overlay_icon command without any pre-configured scope. + +
+ +`core:window:deny-set-overlay-icon` + + + +Denies the set_overlay_icon command without any pre-configured scope. + +
+ +`core:window:allow-set-position` + + + +Enables the set_position command without any pre-configured scope. + +
+ +`core:window:deny-set-position` + + + +Denies the set_position command without any pre-configured scope. + +
+ +`core:window:allow-set-progress-bar` + + + +Enables the set_progress_bar command without any pre-configured scope. + +
+ +`core:window:deny-set-progress-bar` + + + +Denies the set_progress_bar command without any pre-configured scope. + +
+ +`core:window:allow-set-resizable` + + + +Enables the set_resizable command without any pre-configured scope. + +
+ +`core:window:deny-set-resizable` + + + +Denies the set_resizable command without any pre-configured scope. + +
+ +`core:window:allow-set-shadow` + + + +Enables the set_shadow command without any pre-configured scope. + +
+ +`core:window:deny-set-shadow` + + + +Denies the set_shadow command without any pre-configured scope. + +
+ +`core:window:allow-set-simple-fullscreen` + + + +Enables the set_simple_fullscreen command without any pre-configured scope. + +
+ +`core:window:deny-set-simple-fullscreen` + + + +Denies the set_simple_fullscreen command without any pre-configured scope. + +
+ +`core:window:allow-set-size` + + + +Enables the set_size command without any pre-configured scope. + +
+ +`core:window:deny-set-size` + + + +Denies the set_size command without any pre-configured scope. + +
+ +`core:window:allow-set-size-constraints` + + + +Enables the set_size_constraints command without any pre-configured scope. + +
+ +`core:window:deny-set-size-constraints` + + + +Denies the set_size_constraints command without any pre-configured scope. + +
+ +`core:window:allow-set-skip-taskbar` + + + +Enables the set_skip_taskbar command without any pre-configured scope. + +
+ +`core:window:deny-set-skip-taskbar` + + + +Denies the set_skip_taskbar command without any pre-configured scope. + +
+ +`core:window:allow-set-theme` + + + +Enables the set_theme command without any pre-configured scope. + +
+ +`core:window:deny-set-theme` + + + +Denies the set_theme command without any pre-configured scope. + +
+ +`core:window:allow-set-title` + + + +Enables the set_title command without any pre-configured scope. + +
+ +`core:window:deny-set-title` + + + +Denies the set_title command without any pre-configured scope. + +
+ +`core:window:allow-set-title-bar-style` + + + +Enables the set_title_bar_style command without any pre-configured scope. + +
+ +`core:window:deny-set-title-bar-style` + + + +Denies the set_title_bar_style command without any pre-configured scope. + +
+ +`core:window:allow-set-visible-on-all-workspaces` + + + +Enables the set_visible_on_all_workspaces command without any pre-configured scope. + +
+ +`core:window:deny-set-visible-on-all-workspaces` + + + +Denies the set_visible_on_all_workspaces command without any pre-configured scope. + +
+ +`core:window:allow-show` + + + +Enables the show command without any pre-configured scope. + +
+ +`core:window:deny-show` + + + +Denies the show command without any pre-configured scope. + +
+ +`core:window:allow-start-dragging` + + + +Enables the start_dragging command without any pre-configured scope. + +
+ +`core:window:deny-start-dragging` + + + +Denies the start_dragging command without any pre-configured scope. + +
+ +`core:window:allow-start-resize-dragging` + + + +Enables the start_resize_dragging command without any pre-configured scope. + +
+ +`core:window:deny-start-resize-dragging` + + + +Denies the start_resize_dragging command without any pre-configured scope. + +
+ +`core:window:allow-theme` + + + +Enables the theme command without any pre-configured scope. + +
+ +`core:window:deny-theme` + + + +Denies the theme command without any pre-configured scope. + +
+ +`core:window:allow-title` + + + +Enables the title command without any pre-configured scope. + +
+ +`core:window:deny-title` + + + +Denies the title command without any pre-configured scope. + +
+ +`core:window:allow-toggle-maximize` + + + +Enables the toggle_maximize command without any pre-configured scope. + +
+ +`core:window:deny-toggle-maximize` + + + +Denies the toggle_maximize command without any pre-configured scope. + +
+ +`core:window:allow-unmaximize` + + + +Enables the unmaximize command without any pre-configured scope. + +
+ +`core:window:deny-unmaximize` + + + +Denies the unmaximize command without any pre-configured scope. + +
+ +`core:window:allow-unminimize` + + + +Enables the unminimize command without any pre-configured scope. + +
+ +`core:window:deny-unminimize` + + + +Denies the unminimize command without any pre-configured scope. + +
diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/bundle.global.js b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/bundle.global.js new file mode 100644 index 000000000..28f49c47f --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/bundle.global.js @@ -0,0 +1 @@ +var __TAURI_IIFE__=function(e){"use strict";function n(e,n,t,i){if("a"===t&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof n?e!==n||!i:!n.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===t?i:"a"===t?i.call(e):i?i.value:n.get(e)}function t(e,n,t,i,r){if("m"===i)throw new TypeError("Private method is not writable");if("a"===i&&!r)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof n?e!==n||!r:!n.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");return"a"===i?r.call(e,t):r?r.value=t:n.set(e,t),t}var i,r,s,a,l;"function"==typeof SuppressedError&&SuppressedError;const o="__TAURI_TO_IPC_KEY__";function u(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}class c{constructor(e){i.set(this,void 0),r.set(this,0),s.set(this,[]),a.set(this,void 0),t(this,i,e||(()=>{}),"f"),this.id=u(e=>{const l=e.index;if("end"in e)return void(l==n(this,r,"f")?this.cleanupCallback():t(this,a,l,"f"));const o=e.message;if(l==n(this,r,"f")){for(n(this,i,"f").call(this,o),t(this,r,n(this,r,"f")+1,"f");n(this,r,"f")in n(this,s,"f");){const e=n(this,s,"f")[n(this,r,"f")];n(this,i,"f").call(this,e),delete n(this,s,"f")[n(this,r,"f")],t(this,r,n(this,r,"f")+1,"f")}n(this,r,"f")===n(this,a,"f")&&this.cleanupCallback()}else n(this,s,"f")[l]=o})}cleanupCallback(){window.__TAURI_INTERNALS__.unregisterCallback(this.id)}set onmessage(e){t(this,i,e,"f")}get onmessage(){return n(this,i,"f")}[(i=new WeakMap,r=new WeakMap,s=new WeakMap,a=new WeakMap,o)](){return`__CHANNEL__:${this.id}`}toJSON(){return this[o]()}}class d{constructor(e,n,t){this.plugin=e,this.event=n,this.channelId=t}async unregister(){return h(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}}async function p(e,n,t){const i=new c(t);try{return await h(`plugin:${e}|register_listener`,{event:n,handler:i}),new d(e,n,i.id)}catch{return await h(`plugin:${e}|registerListener`,{event:n,handler:i}),new d(e,n,i.id)}}async function h(e,n={},t){return window.__TAURI_INTERNALS__.invoke(e,n,t)}class w{get rid(){return n(this,l,"f")}constructor(e){l.set(this,void 0),t(this,l,e,"f")}async close(){return h("plugin:resources|close",{rid:this.rid})}async[(l=new WeakMap,Symbol.asyncDispose)](){await this.close()}}var _=Object.freeze({__proto__:null,Channel:c,PluginListener:d,Resource:w,SERIALIZE_TO_IPC_FN:o,addPluginListener:p,checkPermissions:async function(e){return h(`plugin:${e}|check_permissions`)},convertFileSrc:function(e,n="asset"){return window.__TAURI_INTERNALS__.convertFileSrc(e,n)},invoke:h,isTauri:function(){return!!(globalThis||window).isTauri},requestPermissions:async function(e){return h(`plugin:${e}|request_permissions`)},transformCallback:u});class y extends w{constructor(e){super(e)}static async new(e,n,t){return h("plugin:image|new",{rgba:e,width:n,height:t}).then(e=>new y(e))}static async fromBytes(e){return h("plugin:image|from_bytes",{bytes:e}).then(e=>new y(e))}static async fromPath(e){return h("plugin:image|from_path",{path:e}).then(e=>new y(e))}async rgba(){return h("plugin:image|rgba",{rid:this.rid}).then(e=>new Uint8Array(e))}async size(){return h("plugin:image|size",{rid:this.rid})}}function g(e){return null==e?null:"string"==typeof e?e:e instanceof y?e.rid:e}var b,m=Object.freeze({__proto__:null,Image:y,transformImage:g});!function(e){e.Nsis="nsis",e.Msi="msi",e.Deb="deb",e.Rpm="rpm",e.AppImage="appimage",e.App="app"}(b||(b={}));var v=Object.freeze({__proto__:null,get BundleType(){return b},defaultWindowIcon:async function(){return h("plugin:app|default_window_icon").then(e=>e?new y(e):null)},exit:async function(e=0){return h("plugin:app|exit",{code:e})},fetchDataStoreIdentifiers:async function(){return h("plugin:app|fetch_data_store_identifiers")},getBundleType:async function(){return h("plugin:app|bundle_type")},getIdentifier:async function(){return h("plugin:app|identifier")},getName:async function(){return h("plugin:app|name")},getTauriVersion:async function(){return h("plugin:app|tauri_version")},getVersion:async function(){return h("plugin:app|version")},hide:async function(){return h("plugin:app|app_hide")},onBackButtonPress:async function(e){return p("app","back-button",e)},removeDataStore:async function(e){return h("plugin:app|remove_data_store",{uuid:e})},setDockVisibility:async function(e){return h("plugin:app|set_dock_visibility",{visible:e})},setTheme:async function(e){return h("plugin:app|set_app_theme",{theme:e})},show:async function(){return h("plugin:app|app_show")},supportsMultipleWindows:async function(){return h("plugin:app|supports_multiple_windows")}});class f{constructor(...e){this.type="Logical",1===e.length?"Logical"in e[0]?(this.width=e[0].Logical.width,this.height=e[0].Logical.height):(this.width=e[0].width,this.height=e[0].height):(this.width=e[0],this.height=e[1])}toPhysical(e){return new k(this.width*e,this.height*e)}[o](){return{width:this.width,height:this.height}}toJSON(){return this[o]()}}class k{constructor(...e){this.type="Physical",1===e.length?"Physical"in e[0]?(this.width=e[0].Physical.width,this.height=e[0].Physical.height):(this.width=e[0].width,this.height=e[0].height):(this.width=e[0],this.height=e[1])}toLogical(e){return new f(this.width/e,this.height/e)}[o](){return{width:this.width,height:this.height}}toJSON(){return this[o]()}}class A{constructor(e){this.size=e}toLogical(e){return this.size instanceof f?this.size:this.size.toLogical(e)}toPhysical(e){return this.size instanceof k?this.size:this.size.toPhysical(e)}[o](){return{[`${this.size.type}`]:{width:this.size.width,height:this.size.height}}}toJSON(){return this[o]()}}class T{constructor(...e){this.type="Logical",1===e.length?"Logical"in e[0]?(this.x=e[0].Logical.x,this.y=e[0].Logical.y):(this.x=e[0].x,this.y=e[0].y):(this.x=e[0],this.y=e[1])}toPhysical(e){return new I(this.x*e,this.y*e)}[o](){return{x:this.x,y:this.y}}toJSON(){return this[o]()}}class I{constructor(...e){this.type="Physical",1===e.length?"Physical"in e[0]?(this.x=e[0].Physical.x,this.y=e[0].Physical.y):(this.x=e[0].x,this.y=e[0].y):(this.x=e[0],this.y=e[1])}toLogical(e){return new T(this.x/e,this.y/e)}[o](){return{x:this.x,y:this.y}}toJSON(){return this[o]()}}class E{constructor(e){this.position=e}toLogical(e){return this.position instanceof T?this.position:this.position.toLogical(e)}toPhysical(e){return this.position instanceof I?this.position:this.position.toPhysical(e)}[o](){return{[`${this.position.type}`]:{x:this.position.x,y:this.position.y}}}toJSON(){return this[o]()}}var R,D=Object.freeze({__proto__:null,LogicalPosition:T,LogicalSize:f,PhysicalPosition:I,PhysicalSize:k,Position:E,Size:A});async function S(e,n){window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener(e,n),await h("plugin:event|unlisten",{event:e,eventId:n})}async function N(e,n,t){var i;const r="string"==typeof(null==t?void 0:t.target)?{kind:"AnyLabel",label:t.target}:null!==(i=null==t?void 0:t.target)&&void 0!==i?i:{kind:"Any"};return h("plugin:event|listen",{event:e,target:r,handler:u(n)}).then(n=>async()=>S(e,n))}async function L(e,n,t){return N(e,t=>{S(e,t.id),n(t)},t)}async function C(e,n){await h("plugin:event|emit",{event:e,payload:n})}async function x(e,n,t){const i="string"==typeof e?{kind:"AnyLabel",label:e}:e;await h("plugin:event|emit_to",{target:i,event:n,payload:t})}!function(e){e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_CREATED="tauri://window-created",e.WINDOW_SUSPENDED="tauri://suspended",e.WINDOW_RESUMED="tauri://resumed",e.WEBVIEW_CREATED="tauri://webview-created",e.DRAG_ENTER="tauri://drag-enter",e.DRAG_OVER="tauri://drag-over",e.DRAG_DROP="tauri://drag-drop",e.DRAG_LEAVE="tauri://drag-leave"}(R||(R={}));var W,P,z,O=Object.freeze({__proto__:null,get TauriEvent(){return R},emit:C,emitTo:x,listen:N,once:L});function U(e){var n;if("items"in e)e.items=null===(n=e.items)||void 0===n?void 0:n.map(e=>"rid"in e?e:U(e));else if("action"in e&&e.action){const n=new c;return n.onmessage=e.action,delete e.action,{...e,handler:n}}return e}async function F(e,n){const t=new c;if(n&&"object"==typeof n&&("action"in n&&n.action&&(t.onmessage=n.action,delete n.action),"item"in n&&n.item&&"object"==typeof n.item&&"About"in n.item&&n.item.About&&"object"==typeof n.item.About&&"icon"in n.item.About&&n.item.About.icon&&(n.item.About.icon=g(n.item.About.icon)),"icon"in n&&n.icon&&(n.icon=g(n.icon)),"items"in n&&n.items)){function i(e){var n;return"rid"in e?[e.rid,e.kind]:("item"in e&&"object"==typeof e.item&&(null===(n=e.item.About)||void 0===n?void 0:n.icon)&&(e.item.About.icon=g(e.item.About.icon)),"icon"in e&&e.icon&&(e.icon=g(e.icon)),"items"in e&&e.items&&(e.items=e.items.map(i)),U(e))}n.items=n.items.map(i)}return h("plugin:menu|new",{kind:e,options:n,handler:t})}class M extends w{get id(){return n(this,W,"f")}get kind(){return n(this,P,"f")}constructor(e,n,i){super(e),W.set(this,void 0),P.set(this,void 0),t(this,W,n,"f"),t(this,P,i,"f")}}W=new WeakMap,P=new WeakMap;class B extends M{constructor(e,n){super(e,n,"MenuItem")}static async new(e){return F("MenuItem",e).then(([e,n])=>new B(e,n))}async text(){return h("plugin:menu|text",{rid:this.rid,kind:this.kind})}async setText(e){return h("plugin:menu|set_text",{rid:this.rid,kind:this.kind,text:e})}async isEnabled(){return h("plugin:menu|is_enabled",{rid:this.rid,kind:this.kind})}async setEnabled(e){return h("plugin:menu|set_enabled",{rid:this.rid,kind:this.kind,enabled:e})}async setAccelerator(e){return h("plugin:menu|set_accelerator",{rid:this.rid,kind:this.kind,accelerator:e})}}class G extends M{constructor(e,n){super(e,n,"Check")}static async new(e){return F("Check",e).then(([e,n])=>new G(e,n))}async text(){return h("plugin:menu|text",{rid:this.rid,kind:this.kind})}async setText(e){return h("plugin:menu|set_text",{rid:this.rid,kind:this.kind,text:e})}async isEnabled(){return h("plugin:menu|is_enabled",{rid:this.rid,kind:this.kind})}async setEnabled(e){return h("plugin:menu|set_enabled",{rid:this.rid,kind:this.kind,enabled:e})}async setAccelerator(e){return h("plugin:menu|set_accelerator",{rid:this.rid,kind:this.kind,accelerator:e})}async isChecked(){return h("plugin:menu|is_checked",{rid:this.rid})}async setChecked(e){return h("plugin:menu|set_checked",{rid:this.rid,checked:e})}}!function(e){e.Add="Add",e.Advanced="Advanced",e.Bluetooth="Bluetooth",e.Bookmarks="Bookmarks",e.Caution="Caution",e.ColorPanel="ColorPanel",e.ColumnView="ColumnView",e.Computer="Computer",e.EnterFullScreen="EnterFullScreen",e.Everyone="Everyone",e.ExitFullScreen="ExitFullScreen",e.FlowView="FlowView",e.Folder="Folder",e.FolderBurnable="FolderBurnable",e.FolderSmart="FolderSmart",e.FollowLinkFreestanding="FollowLinkFreestanding",e.FontPanel="FontPanel",e.GoLeft="GoLeft",e.GoRight="GoRight",e.Home="Home",e.IChatTheater="IChatTheater",e.IconView="IconView",e.Info="Info",e.InvalidDataFreestanding="InvalidDataFreestanding",e.LeftFacingTriangle="LeftFacingTriangle",e.ListView="ListView",e.LockLocked="LockLocked",e.LockUnlocked="LockUnlocked",e.MenuMixedState="MenuMixedState",e.MenuOnState="MenuOnState",e.MobileMe="MobileMe",e.MultipleDocuments="MultipleDocuments",e.Network="Network",e.Path="Path",e.PreferencesGeneral="PreferencesGeneral",e.QuickLook="QuickLook",e.RefreshFreestanding="RefreshFreestanding",e.Refresh="Refresh",e.Remove="Remove",e.RevealFreestanding="RevealFreestanding",e.RightFacingTriangle="RightFacingTriangle",e.Share="Share",e.Slideshow="Slideshow",e.SmartBadge="SmartBadge",e.StatusAvailable="StatusAvailable",e.StatusNone="StatusNone",e.StatusPartiallyAvailable="StatusPartiallyAvailable",e.StatusUnavailable="StatusUnavailable",e.StopProgressFreestanding="StopProgressFreestanding",e.StopProgress="StopProgress",e.TrashEmpty="TrashEmpty",e.TrashFull="TrashFull",e.User="User",e.UserAccounts="UserAccounts",e.UserGroup="UserGroup",e.UserGuest="UserGuest"}(z||(z={}));class V extends M{constructor(e,n){super(e,n,"Icon")}static async new(e){return F("Icon",e).then(([e,n])=>new V(e,n))}async text(){return h("plugin:menu|text",{rid:this.rid,kind:this.kind})}async setText(e){return h("plugin:menu|set_text",{rid:this.rid,kind:this.kind,text:e})}async isEnabled(){return h("plugin:menu|is_enabled",{rid:this.rid,kind:this.kind})}async setEnabled(e){return h("plugin:menu|set_enabled",{rid:this.rid,kind:this.kind,enabled:e})}async setAccelerator(e){return h("plugin:menu|set_accelerator",{rid:this.rid,kind:this.kind,accelerator:e})}async setIcon(e){return h("plugin:menu|set_icon",{rid:this.rid,kind:this.kind,icon:g(e)})}}class j extends M{constructor(e,n){super(e,n,"Predefined")}static async new(e){return F("Predefined",e).then(([e,n])=>new j(e,n))}async text(){return h("plugin:menu|text",{rid:this.rid,kind:this.kind})}async setText(e){return h("plugin:menu|set_text",{rid:this.rid,kind:this.kind,text:e})}}function H([e,n,t]){switch(t){case"Submenu":return new $(e,n);case"Predefined":return new j(e,n);case"Check":return new G(e,n);case"Icon":return new V(e,n);default:return new B(e,n)}}class $ extends M{constructor(e,n){super(e,n,"Submenu")}static async new(e){return F("Submenu",e).then(([e,n])=>new $(e,n))}async text(){return h("plugin:menu|text",{rid:this.rid,kind:this.kind})}async setText(e){return h("plugin:menu|set_text",{rid:this.rid,kind:this.kind,text:e})}async isEnabled(){return h("plugin:menu|is_enabled",{rid:this.rid,kind:this.kind})}async setEnabled(e){return h("plugin:menu|set_enabled",{rid:this.rid,kind:this.kind,enabled:e})}async append(e){return h("plugin:menu|append",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e)})}async prepend(e){return h("plugin:menu|prepend",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e)})}async insert(e,n){return h("plugin:menu|insert",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e),position:n})}async remove(e){return h("plugin:menu|remove",{rid:this.rid,kind:this.kind,item:[e.rid,e.kind]})}async removeAt(e){return h("plugin:menu|remove_at",{rid:this.rid,kind:this.kind,position:e}).then(H)}async items(){return h("plugin:menu|items",{rid:this.rid,kind:this.kind}).then(e=>e.map(H))}async get(e){return h("plugin:menu|get",{rid:this.rid,kind:this.kind,id:e}).then(e=>e?H(e):null)}async popup(e,n){var t;return h("plugin:menu|popup",{rid:this.rid,kind:this.kind,window:null!==(t=null==n?void 0:n.label)&&void 0!==t?t:null,at:e instanceof E?e:e?new E(e):null})}async setAsWindowsMenuForNSApp(){return h("plugin:menu|set_as_windows_menu_for_nsapp",{rid:this.rid})}async setAsHelpMenuForNSApp(){return h("plugin:menu|set_as_help_menu_for_nsapp",{rid:this.rid})}async setIcon(e){return h("plugin:menu|set_icon",{rid:this.rid,kind:this.kind,icon:g(e)})}}class q extends M{constructor(e,n){super(e,n,"Menu")}static async new(e){return F("Menu",e).then(([e,n])=>new q(e,n))}static async default(){return h("plugin:menu|create_default").then(([e,n])=>new q(e,n))}async append(e){return h("plugin:menu|append",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e)})}async prepend(e){return h("plugin:menu|prepend",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e)})}async insert(e,n){return h("plugin:menu|insert",{rid:this.rid,kind:this.kind,items:(Array.isArray(e)?e:[e]).map(e=>"rid"in e?[e.rid,e.kind]:e),position:n})}async remove(e){return h("plugin:menu|remove",{rid:this.rid,kind:this.kind,item:[e.rid,e.kind]})}async removeAt(e){return h("plugin:menu|remove_at",{rid:this.rid,kind:this.kind,position:e}).then(H)}async items(){return h("plugin:menu|items",{rid:this.rid,kind:this.kind}).then(e=>e.map(H))}async get(e){return h("plugin:menu|get",{rid:this.rid,kind:this.kind,id:e}).then(e=>e?H(e):null)}async popup(e,n){var t;return h("plugin:menu|popup",{rid:this.rid,kind:this.kind,window:null!==(t=null==n?void 0:n.label)&&void 0!==t?t:null,at:e instanceof E?e:e?new E(e):null})}async setAsAppMenu(){return h("plugin:menu|set_as_app_menu",{rid:this.rid}).then(e=>e?new q(e[0],e[1]):null)}async setAsWindowMenu(e){var n;return h("plugin:menu|set_as_window_menu",{rid:this.rid,window:null!==(n=null==e?void 0:e.label)&&void 0!==n?n:null}).then(e=>e?new q(e[0],e[1]):null)}}var J=Object.freeze({__proto__:null,CheckMenuItem:G,IconMenuItem:V,Menu:q,MenuItem:B,get NativeIcon(){return z},PredefinedMenuItem:j,Submenu:$,itemFromKind:H});function Q(){var e,n;window.__TAURI_INTERNALS__=null!==(e=window.__TAURI_INTERNALS__)&&void 0!==e?e:{},window.__TAURI_EVENT_PLUGIN_INTERNALS__=null!==(n=window.__TAURI_EVENT_PLUGIN_INTERNALS__)&&void 0!==n?n:{}}var Z,K=Object.freeze({__proto__:null,clearMocks:function(){"object"==typeof window.__TAURI_INTERNALS__&&(delete window.__TAURI_INTERNALS__.invoke,delete window.__TAURI_INTERNALS__.transformCallback,delete window.__TAURI_INTERNALS__.unregisterCallback,delete window.__TAURI_INTERNALS__.runCallback,delete window.__TAURI_INTERNALS__.callbacks,delete window.__TAURI_INTERNALS__.convertFileSrc,delete window.__TAURI_INTERNALS__.metadata,"object"==typeof window.__TAURI_EVENT_PLUGIN_INTERNALS__&&delete window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener)},mockConvertFileSrc:function(e){Q(),window.__TAURI_INTERNALS__.convertFileSrc=function(n,t="asset"){const i=encodeURIComponent(n);return"windows"===e?`http://${t}.localhost/${i}`:`${t}://localhost/${i}`}},mockIPC:function(e,n){function t(e,n){switch(e){case"plugin:event|listen":return function(e){i.has(e.event)||i.set(e.event,[]);return i.get(e.event).push(e.handler),e.handler}(n);case"plugin:event|emit":return function(e){const n=i.get(e.event)||[];for(const t of n)a(t,e);return null}(n);case"plugin:event|unlisten":return function(e){const n=i.get(e.event);if(n){const t=n.indexOf(e.id);-1!==t&&n.splice(t,1)}}(n)}}Q();const i=new Map,r=new Map;function s(e){r.delete(e)}function a(e,n){const t=r.get(e);t?t(n):console.warn(`[TAURI] Couldn't find callback id ${e}. This might happen when the app is reloaded while Rust is running an asynchronous operation.`)}window.__TAURI_INTERNALS__.invoke=async function(i,r,s){return(null==n?void 0:n.shouldMockEvents)&&function(e){return e.startsWith("plugin:event|")}(i)?t(i,r):e(i,r)},window.__TAURI_INTERNALS__.transformCallback=function(e,n=!1){const t=window.crypto.getRandomValues(new Uint32Array(1))[0];return r.set(t,i=>(n&&s(t),e&&e(i))),t},window.__TAURI_INTERNALS__.unregisterCallback=s,window.__TAURI_INTERNALS__.runCallback=a,window.__TAURI_INTERNALS__.callbacks=r,window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener=function(e,n){s(n)}},mockWindows:function(e,...n){Q(),window.__TAURI_INTERNALS__.metadata={currentWindow:{label:e},currentWebview:{windowLabel:e,label:e}}}});!function(e){e[e.Audio=1]="Audio",e[e.Cache=2]="Cache",e[e.Config=3]="Config",e[e.Data=4]="Data",e[e.LocalData=5]="LocalData",e[e.Document=6]="Document",e[e.Download=7]="Download",e[e.Picture=8]="Picture",e[e.Public=9]="Public",e[e.Video=10]="Video",e[e.Resource=11]="Resource",e[e.Temp=12]="Temp",e[e.AppConfig=13]="AppConfig",e[e.AppData=14]="AppData",e[e.AppLocalData=15]="AppLocalData",e[e.AppCache=16]="AppCache",e[e.AppLog=17]="AppLog",e[e.Desktop=18]="Desktop",e[e.Executable=19]="Executable",e[e.Font=20]="Font",e[e.Home=21]="Home",e[e.Runtime=22]="Runtime",e[e.Template=23]="Template"}(Z||(Z={}));var Y=Object.freeze({__proto__:null,get BaseDirectory(){return Z},appCacheDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.AppCache})},appConfigDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.AppConfig})},appDataDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.AppData})},appLocalDataDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.AppLocalData})},appLogDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.AppLog})},audioDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Audio})},basename:async function(e,n){return h("plugin:path|basename",{path:e,ext:n})},cacheDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Cache})},configDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Config})},dataDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Data})},delimiter:function(){return window.__TAURI_INTERNALS__.plugins.path.delimiter},desktopDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Desktop})},dirname:async function(e){return h("plugin:path|dirname",{path:e})},documentDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Document})},downloadDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Download})},executableDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Executable})},extname:async function(e){return h("plugin:path|extname",{path:e})},fontDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Font})},homeDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Home})},isAbsolute:async function(e){return h("plugin:path|is_absolute",{path:e})},join:async function(...e){return h("plugin:path|join",{paths:e})},localDataDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.LocalData})},normalize:async function(e){return h("plugin:path|normalize",{path:e})},pictureDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Picture})},publicDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Public})},resolve:async function(...e){return h("plugin:path|resolve",{paths:e})},resolveResource:async function(e){return h("plugin:path|resolve_directory",{directory:Z.Resource,path:e})},resourceDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Resource})},runtimeDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Runtime})},sep:function(){return window.__TAURI_INTERNALS__.plugins.path.sep},tempDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Temp})},templateDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Template})},videoDir:async function(){return h("plugin:path|resolve_directory",{directory:Z.Video})}});class X extends w{constructor(e,n){super(e),this.id=n}static async getById(e){return h("plugin:tray|get_by_id",{id:e}).then(n=>n?new X(n,e):null)}static async removeById(e){return h("plugin:tray|remove_by_id",{id:e})}static async new(e){(null==e?void 0:e.menu)&&(e.menu=[e.menu.rid,e.menu.kind]),(null==e?void 0:e.icon)&&(e.icon=g(e.icon));const n=new c;if(null==e?void 0:e.action){const t=e.action;n.onmessage=e=>t(function(e){const n=e;return n.position=new I(e.position),n.rect.position=new I(e.rect.position),n.rect.size=new k(e.rect.size),n}(e)),delete e.action}return h("plugin:tray|new",{options:null!=e?e:{},handler:n}).then(([e,n])=>new X(e,n))}async setIcon(e){let n=null;return e&&(n=g(e)),h("plugin:tray|set_icon",{rid:this.rid,icon:n})}async setMenu(e){return e&&(e=[e.rid,e.kind]),h("plugin:tray|set_menu",{rid:this.rid,menu:e})}async setTooltip(e){return h("plugin:tray|set_tooltip",{rid:this.rid,tooltip:e})}async setTitle(e){return h("plugin:tray|set_title",{rid:this.rid,title:e})}async setVisible(e){return h("plugin:tray|set_visible",{rid:this.rid,visible:e})}async setTempDirPath(e){return h("plugin:tray|set_temp_dir_path",{rid:this.rid,path:e})}async setIconAsTemplate(e){return h("plugin:tray|set_icon_as_template",{rid:this.rid,asTemplate:e})}async setIconWithAsTemplate(e,n){let t=null;return e&&(t=g(e)),h("plugin:tray|set_icon_with_as_template",{rid:this.rid,icon:t,asTemplate:n})}async setMenuOnLeftClick(e){return h("plugin:tray|set_show_menu_on_left_click",{rid:this.rid,onLeft:e})}async setShowMenuOnLeftClick(e){return h("plugin:tray|set_show_menu_on_left_click",{rid:this.rid,onLeft:e})}}var ee,ne,te=Object.freeze({__proto__:null,TrayIcon:X});!function(e){e[e.Critical=1]="Critical",e[e.Informational=2]="Informational"}(ee||(ee={}));class ie{constructor(e){this._preventDefault=!1,this.event=e.event,this.id=e.id}preventDefault(){this._preventDefault=!0}isPreventDefault(){return this._preventDefault}}function re(){return new le(window.__TAURI_INTERNALS__.metadata.currentWindow.label,{skip:!0})}async function se(){return h("plugin:window|get_all_windows").then(e=>e.map(e=>new le(e,{skip:!0})))}!function(e){e.None="none",e.Normal="normal",e.Indeterminate="indeterminate",e.Paused="paused",e.Error="error"}(ne||(ne={}));const ae=["tauri://created","tauri://error"];class le{constructor(e,n={}){var t;this.label=e,this.listeners=Object.create(null),(null==n?void 0:n.skip)||h("plugin:window|create",{options:{...n,parent:"string"==typeof n.parent?n.parent:null===(t=n.parent)||void 0===t?void 0:t.label,label:e}}).then(async()=>this.emit("tauri://created")).catch(async e=>this.emit("tauri://error",e))}static async getByLabel(e){var n;return null!==(n=(await se()).find(n=>n.label===e))&&void 0!==n?n:null}static getCurrent(){return re()}static async getAll(){return se()}static async getFocusedWindow(){for(const e of await se())if(await e.isFocused())return e;return null}async listen(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:N(e,n,{target:{kind:"Window",label:this.label}})}async once(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:L(e,n,{target:{kind:"Window",label:this.label}})}async emit(e,n){if(!ae.includes(e))return C(e,n);for(const t of this.listeners[e]||[])t({event:e,id:-1,payload:n})}async emitTo(e,n,t){if(!ae.includes(n))return x(e,n,t);for(const e of this.listeners[n]||[])e({event:n,id:-1,payload:t})}_handleTauriEvent(e,n){return!!ae.includes(e)&&(e in this.listeners?this.listeners[e].push(n):this.listeners[e]=[n],!0)}async scaleFactor(){return h("plugin:window|scale_factor",{label:this.label})}async innerPosition(){return h("plugin:window|inner_position",{label:this.label}).then(e=>new I(e))}async outerPosition(){return h("plugin:window|outer_position",{label:this.label}).then(e=>new I(e))}async innerSize(){return h("plugin:window|inner_size",{label:this.label}).then(e=>new k(e))}async outerSize(){return h("plugin:window|outer_size",{label:this.label}).then(e=>new k(e))}async isFullscreen(){return h("plugin:window|is_fullscreen",{label:this.label})}async isMinimized(){return h("plugin:window|is_minimized",{label:this.label})}async isMaximized(){return h("plugin:window|is_maximized",{label:this.label})}async isFocused(){return h("plugin:window|is_focused",{label:this.label})}async isDecorated(){return h("plugin:window|is_decorated",{label:this.label})}async isResizable(){return h("plugin:window|is_resizable",{label:this.label})}async isMaximizable(){return h("plugin:window|is_maximizable",{label:this.label})}async isMinimizable(){return h("plugin:window|is_minimizable",{label:this.label})}async isClosable(){return h("plugin:window|is_closable",{label:this.label})}async isVisible(){return h("plugin:window|is_visible",{label:this.label})}async title(){return h("plugin:window|title",{label:this.label})}async theme(){return h("plugin:window|theme",{label:this.label})}async isAlwaysOnTop(){return h("plugin:window|is_always_on_top",{label:this.label})}async activityName(){return h("plugin:window|activity_name",{label:this.label})}async sceneIdentifier(){return h("plugin:window|scene_identifier",{label:this.label})}async center(){return h("plugin:window|center",{label:this.label})}async requestUserAttention(e){let n=null;return e&&(n=e===ee.Critical?{type:"Critical"}:{type:"Informational"}),h("plugin:window|request_user_attention",{label:this.label,value:n})}async setResizable(e){return h("plugin:window|set_resizable",{label:this.label,value:e})}async setEnabled(e){return h("plugin:window|set_enabled",{label:this.label,value:e})}async isEnabled(){return h("plugin:window|is_enabled",{label:this.label})}async setMaximizable(e){return h("plugin:window|set_maximizable",{label:this.label,value:e})}async setMinimizable(e){return h("plugin:window|set_minimizable",{label:this.label,value:e})}async setClosable(e){return h("plugin:window|set_closable",{label:this.label,value:e})}async setTitle(e){return h("plugin:window|set_title",{label:this.label,value:e})}async maximize(){return h("plugin:window|maximize",{label:this.label})}async unmaximize(){return h("plugin:window|unmaximize",{label:this.label})}async toggleMaximize(){return h("plugin:window|toggle_maximize",{label:this.label})}async minimize(){return h("plugin:window|minimize",{label:this.label})}async unminimize(){return h("plugin:window|unminimize",{label:this.label})}async show(){return h("plugin:window|show",{label:this.label})}async hide(){return h("plugin:window|hide",{label:this.label})}async close(){return h("plugin:window|close",{label:this.label})}async destroy(){return h("plugin:window|destroy",{label:this.label})}async setDecorations(e){return h("plugin:window|set_decorations",{label:this.label,value:e})}async setShadow(e){return h("plugin:window|set_shadow",{label:this.label,value:e})}async setEffects(e){return h("plugin:window|set_effects",{label:this.label,value:e})}async clearEffects(){return h("plugin:window|set_effects",{label:this.label,value:null})}async setAlwaysOnTop(e){return h("plugin:window|set_always_on_top",{label:this.label,value:e})}async setAlwaysOnBottom(e){return h("plugin:window|set_always_on_bottom",{label:this.label,value:e})}async setContentProtected(e){return h("plugin:window|set_content_protected",{label:this.label,value:e})}async setSize(e){return h("plugin:window|set_size",{label:this.label,value:e instanceof A?e:new A(e)})}async setMinSize(e){return h("plugin:window|set_min_size",{label:this.label,value:e instanceof A?e:e?new A(e):null})}async setMaxSize(e){return h("plugin:window|set_max_size",{label:this.label,value:e instanceof A?e:e?new A(e):null})}async setSizeConstraints(e){function n(e){return e?{Logical:e}:null}return h("plugin:window|set_size_constraints",{label:this.label,value:{minWidth:n(null==e?void 0:e.minWidth),minHeight:n(null==e?void 0:e.minHeight),maxWidth:n(null==e?void 0:e.maxWidth),maxHeight:n(null==e?void 0:e.maxHeight)}})}async setPosition(e){return h("plugin:window|set_position",{label:this.label,value:e instanceof E?e:new E(e)})}async setFullscreen(e){return h("plugin:window|set_fullscreen",{label:this.label,value:e})}async setFullscreenOnMonitor(e){return h("plugin:window|set_fullscreen_on_monitor",{label:this.label,value:e})}async setSimpleFullscreen(e){return h("plugin:window|set_simple_fullscreen",{label:this.label,value:e})}async setFocus(){return h("plugin:window|set_focus",{label:this.label})}async setFocusable(e){return h("plugin:window|set_focusable",{label:this.label,value:e})}async setIcon(e){return h("plugin:window|set_icon",{label:this.label,value:g(e)})}async setSkipTaskbar(e){return h("plugin:window|set_skip_taskbar",{label:this.label,value:e})}async setCursorGrab(e){return h("plugin:window|set_cursor_grab",{label:this.label,value:e})}async setCursorVisible(e){return h("plugin:window|set_cursor_visible",{label:this.label,value:e})}async setCursorIcon(e){return h("plugin:window|set_cursor_icon",{label:this.label,value:e})}async setBackgroundColor(e){return h("plugin:window|set_background_color",{label:this.label,value:e})}async setCursorPosition(e){return h("plugin:window|set_cursor_position",{label:this.label,value:e instanceof E?e:new E(e)})}async setIgnoreCursorEvents(e){return h("plugin:window|set_ignore_cursor_events",{label:this.label,value:e})}async startDragging(){return h("plugin:window|start_dragging",{label:this.label})}async startResizeDragging(e){return h("plugin:window|start_resize_dragging",{label:this.label,value:e})}async setBadgeCount(e){return h("plugin:window|set_badge_count",{label:this.label,value:e})}async setBadgeLabel(e){return h("plugin:window|set_badge_label",{label:this.label,value:e})}async setOverlayIcon(e){return h("plugin:window|set_overlay_icon",{label:this.label,value:e?g(e):void 0})}async setProgressBar(e){return h("plugin:window|set_progress_bar",{label:this.label,value:e})}async setVisibleOnAllWorkspaces(e){return h("plugin:window|set_visible_on_all_workspaces",{label:this.label,value:e})}async setTitleBarStyle(e){return h("plugin:window|set_title_bar_style",{label:this.label,value:e})}async setTheme(e){return h("plugin:window|set_theme",{label:this.label,value:e})}async onResized(e){return this.listen(R.WINDOW_RESIZED,n=>{n.payload=new k(n.payload),e(n)})}async onMoved(e){return this.listen(R.WINDOW_MOVED,n=>{n.payload=new I(n.payload),e(n)})}async onCloseRequested(e){return this.listen(R.WINDOW_CLOSE_REQUESTED,async n=>{const t=new ie(n);await e(t),t.isPreventDefault()||await this.destroy()})}async onDragDropEvent(e){const n=await this.listen(R.DRAG_ENTER,n=>{e({...n,payload:{type:"enter",paths:n.payload.paths,position:new I(n.payload.position)}})}),t=await this.listen(R.DRAG_OVER,n=>{e({...n,payload:{type:"over",position:new I(n.payload.position)}})}),i=await this.listen(R.DRAG_DROP,n=>{e({...n,payload:{type:"drop",paths:n.payload.paths,position:new I(n.payload.position)}})}),r=await this.listen(R.DRAG_LEAVE,n=>{e({...n,payload:{type:"leave"}})});return()=>{n(),i(),t(),r()}}async onFocusChanged(e){const n=await this.listen(R.WINDOW_FOCUS,n=>{e({...n,payload:!0})}),t=await this.listen(R.WINDOW_BLUR,n=>{e({...n,payload:!1})});return()=>{n(),t()}}async onScaleChanged(e){return this.listen(R.WINDOW_SCALE_FACTOR_CHANGED,e)}async onThemeChanged(e){return this.listen(R.WINDOW_THEME_CHANGED,e)}}var oe,ue,ce,de;function pe(e){return null===e?null:{name:e.name,scaleFactor:e.scaleFactor,position:new I(e.position),size:new k(e.size),workArea:{position:new I(e.workArea.position),size:new k(e.workArea.size)}}}!function(e){e.Disabled="disabled",e.Throttle="throttle",e.Suspend="suspend"}(oe||(oe={})),function(e){e.Default="default",e.FluentOverlay="fluentOverlay"}(ue||(ue={})),function(e){e.AppearanceBased="appearanceBased",e.Light="light",e.Dark="dark",e.MediumLight="mediumLight",e.UltraDark="ultraDark",e.Titlebar="titlebar",e.Selection="selection",e.Menu="menu",e.Popover="popover",e.Sidebar="sidebar",e.HeaderView="headerView",e.Sheet="sheet",e.WindowBackground="windowBackground",e.HudWindow="hudWindow",e.FullScreenUI="fullScreenUI",e.Tooltip="tooltip",e.ContentBackground="contentBackground",e.UnderWindowBackground="underWindowBackground",e.UnderPageBackground="underPageBackground",e.LiquidGlassRegular="liquidGlassRegular",e.LiquidGlassClear="liquidGlassClear",e.Mica="mica",e.Blur="blur",e.Acrylic="acrylic",e.Tabbed="tabbed",e.TabbedDark="tabbedDark",e.TabbedLight="tabbedLight"}(ce||(ce={})),function(e){e.FollowsWindowActiveState="followsWindowActiveState",e.Active="active",e.Inactive="inactive"}(de||(de={}));var he=Object.freeze({__proto__:null,CloseRequestedEvent:ie,get Effect(){return ce},get EffectState(){return de},LogicalPosition:T,LogicalSize:f,PhysicalPosition:I,PhysicalSize:k,get ProgressBarStatus(){return ne},get UserAttentionType(){return ee},Window:le,availableMonitors:async function(){return h("plugin:window|available_monitors").then(e=>e.map(pe))},currentMonitor:async function(){return h("plugin:window|current_monitor").then(pe)},cursorPosition:async function(){return h("plugin:window|cursor_position").then(e=>new I(e))},getAllWindows:se,getCurrentWindow:re,monitorFromPoint:async function(e,n){return h("plugin:window|monitor_from_point",{x:e,y:n}).then(pe)},primaryMonitor:async function(){return h("plugin:window|primary_monitor").then(pe)}});function we(){return new ge(re(),window.__TAURI_INTERNALS__.metadata.currentWebview.label,{skip:!0})}async function _e(){return h("plugin:webview|get_all_webviews").then(e=>e.map(e=>new ge(new le(e.windowLabel,{skip:!0}),e.label,{skip:!0})))}const ye=["tauri://created","tauri://error"];class ge{constructor(e,n,t){this.window=e,this.label=n,this.listeners=Object.create(null),(null==t?void 0:t.skip)||h("plugin:webview|create_webview",{windowLabel:e.label,options:{...t,label:n}}).then(async()=>this.emit("tauri://created")).catch(async e=>this.emit("tauri://error",e))}static async getByLabel(e){var n;return null!==(n=(await _e()).find(n=>n.label===e))&&void 0!==n?n:null}static getCurrent(){return we()}static async getAll(){return _e()}async listen(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:N(e,n,{target:{kind:"Webview",label:this.label}})}async once(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:L(e,n,{target:{kind:"Webview",label:this.label}})}async emit(e,n){if(!ye.includes(e))return C(e,n);for(const t of this.listeners[e]||[])t({event:e,id:-1,payload:n})}async emitTo(e,n,t){if(!ye.includes(n))return x(e,n,t);for(const e of this.listeners[n]||[])e({event:n,id:-1,payload:t})}_handleTauriEvent(e,n){return!!ye.includes(e)&&(e in this.listeners?this.listeners[e].push(n):this.listeners[e]=[n],!0)}async position(){return h("plugin:webview|webview_position",{label:this.label}).then(e=>new I(e))}async size(){return h("plugin:webview|webview_size",{label:this.label}).then(e=>new k(e))}async close(){return h("plugin:webview|webview_close",{label:this.label})}async setSize(e){return h("plugin:webview|set_webview_size",{label:this.label,value:e instanceof A?e:new A(e)})}async setPosition(e){return h("plugin:webview|set_webview_position",{label:this.label,value:e instanceof E?e:new E(e)})}async setFocus(){return h("plugin:webview|set_webview_focus",{label:this.label})}async setAutoResize(e){return h("plugin:webview|set_webview_auto_resize",{label:this.label,value:e})}async hide(){return h("plugin:webview|webview_hide",{label:this.label})}async show(){return h("plugin:webview|webview_show",{label:this.label})}async setZoom(e){return h("plugin:webview|set_webview_zoom",{label:this.label,value:e})}async reparent(e){return h("plugin:webview|reparent",{label:this.label,window:"string"==typeof e?e:e.label})}async clearAllBrowsingData(){return h("plugin:webview|clear_all_browsing_data")}async setBackgroundColor(e){return h("plugin:webview|set_webview_background_color",{label:this.label,value:e})}async onDragDropEvent(e){const n=await this.listen(R.DRAG_ENTER,n=>{e({...n,payload:{type:"enter",paths:n.payload.paths,position:new I(n.payload.position)}})}),t=await this.listen(R.DRAG_OVER,n=>{e({...n,payload:{type:"over",position:new I(n.payload.position)}})}),i=await this.listen(R.DRAG_DROP,n=>{e({...n,payload:{type:"drop",paths:n.payload.paths,position:new I(n.payload.position)}})}),r=await this.listen(R.DRAG_LEAVE,n=>{e({...n,payload:{type:"leave"}})});return()=>{n(),i(),t(),r()}}}var be,me,ve=Object.freeze({__proto__:null,Webview:ge,getAllWebviews:_e,getCurrentWebview:we});function fe(){const e=we();return new Ae(e.label,{skip:!0})}async function ke(){return h("plugin:window|get_all_windows").then(e=>e.map(e=>new Ae(e,{skip:!0})))}class Ae{constructor(e,n={}){var t;this.label=e,this.listeners=Object.create(null),(null==n?void 0:n.skip)||h("plugin:webview|create_webview_window",{options:{...n,parent:"string"==typeof n.parent?n.parent:null===(t=n.parent)||void 0===t?void 0:t.label,label:e}}).then(async()=>this.emit("tauri://created")).catch(async e=>this.emit("tauri://error",e))}static async getByLabel(e){var n;const t=null!==(n=(await ke()).find(n=>n.label===e))&&void 0!==n?n:null;return t?new Ae(t.label,{skip:!0}):null}static getCurrent(){return fe()}static async getAll(){return ke()}async listen(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:N(e,n,{target:{kind:"WebviewWindow",label:this.label}})}async once(e,n){return this._handleTauriEvent(e,n)?()=>{const t=this.listeners[e];t.splice(t.indexOf(n),1)}:L(e,n,{target:{kind:"WebviewWindow",label:this.label}})}async setBackgroundColor(e){return h("plugin:window|set_background_color",{label:this.label,value:e}).then(()=>h("plugin:webview|set_webview_background_color",{label:this.label,value:e}))}}be=Ae,me=[le,ge],(Array.isArray(me)?me:[me]).forEach(e=>{Object.getOwnPropertyNames(e.prototype).forEach(n=>{var t;"object"==typeof be.prototype&&be.prototype&&n in be.prototype||Object.defineProperty(be.prototype,n,null!==(t=Object.getOwnPropertyDescriptor(e.prototype,n))&&void 0!==t?t:Object.create(null))})});var Te=Object.freeze({__proto__:null,WebviewWindow:Ae,getAllWebviewWindows:ke,getCurrentWebviewWindow:fe});return e.app=v,e.core=_,e.dpi=D,e.event=O,e.image=m,e.menu=J,e.mocks=K,e.path=Y,e.tray=te,e.webview=ve,e.webviewWindow=Te,e.window=he,e}({});window.__TAURI__=__TAURI_IIFE__; diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/core.js b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/core.js new file mode 100644 index 000000000..86bafacc9 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/core.js @@ -0,0 +1,114 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +;(function () { + function uid() { + return window.crypto.getRandomValues(new Uint32Array(1))[0] + } + + const osName = __TEMPLATE_os_name__ + const protocolScheme = __TEMPLATE_protocol_scheme__ + + Object.defineProperty(window.__TAURI_INTERNALS__, 'convertFileSrc', { + value: function (filePath, protocol = 'asset') { + const path = encodeURIComponent(filePath) + return osName === 'windows' || osName === 'android' + ? `${protocolScheme}://${protocol}.localhost/${path}` + : `${protocol}://localhost/${path}` + } + }) + + const callbacks = new Map() + + function registerCallback(callback, once) { + const identifier = uid() + callbacks.set(identifier, (data) => { + if (once) { + unregisterCallback(identifier) + } + return callback && callback(data) + }) + return identifier + } + + function unregisterCallback(id) { + callbacks.delete(id) + } + + function runCallback(id, data) { + const callback = callbacks.get(id) + if (callback) { + callback(data) + } else { + console.warn( + `[TAURI] Couldn't find callback id ${id}. This might happen when the app is reloaded while Rust is running an asynchronous operation.` + ) + } + } + + // Maybe let's rename it to `registerCallback`? + Object.defineProperty(window.__TAURI_INTERNALS__, 'transformCallback', { + value: registerCallback + }) + + Object.defineProperty(window.__TAURI_INTERNALS__, 'unregisterCallback', { + value: unregisterCallback + }) + + Object.defineProperty(window.__TAURI_INTERNALS__, 'runCallback', { + value: runCallback + }) + + // This is just for the debugging purposes + Object.defineProperty(window.__TAURI_INTERNALS__, 'callbacks', { + value: callbacks + }) + + const ipcQueue = [] + let isWaitingForIpc = false + + function waitForIpc() { + if ('ipc' in window.__TAURI_INTERNALS__) { + for (const action of ipcQueue) { + action() + } + } else { + setTimeout(waitForIpc, 50) + } + } + + Object.defineProperty(window.__TAURI_INTERNALS__, 'invoke', { + value: function (cmd, payload = {}, options) { + return new Promise(function (resolve, reject) { + const callback = registerCallback((r) => { + resolve(r) + unregisterCallback(error) + }, true) + const error = registerCallback((e) => { + reject(e) + unregisterCallback(callback) + }, true) + + const action = () => { + window.__TAURI_INTERNALS__.ipc({ + cmd, + callback, + error, + payload, + options + }) + } + if ('ipc' in window.__TAURI_INTERNALS__) { + action() + } else { + ipcQueue.push(action) + if (!isWaitingForIpc) { + waitForIpc() + isWaitingForIpc = true + } + } + }) + } + }) +})() diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/freeze_prototype.js b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/freeze_prototype.js new file mode 100644 index 000000000..113ca8ce1 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/freeze_prototype.js @@ -0,0 +1,5 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +Object.freeze(Object.prototype) diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/init.js b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/init.js new file mode 100644 index 000000000..ef6f72f60 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/init.js @@ -0,0 +1,15 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +;(function () { + __RAW_freeze_prototype__ + + __RAW_pattern_script__ + + __RAW_ipc_script__ + + __RAW_core_script__ + + __RAW_event_initialization_script__ +})() diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/ipc-protocol.js b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/ipc-protocol.js new file mode 100644 index 000000000..9ca244053 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/ipc-protocol.js @@ -0,0 +1,91 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +;(function () { + /** + * A runtime generated key to ensure an IPC call comes from an initialized frame. + * + * This is declared outside the `window.__TAURI_INVOKE__` definition to prevent + * the key from being leaked by `window.__TAURI_INVOKE__.toString()`. + */ + const __TAURI_INVOKE_KEY__ = __TEMPLATE_invoke_key__ + + const processIpcMessage = __RAW_process_ipc_message_fn__ + const osName = __TEMPLATE_os_name__ + const fetchChannelDataCommand = __TEMPLATE_fetch_channel_data_command__ + let customProtocolIpcFailed = false + + // on Android we never use it because Android does not have support to reading the request body + const canUseCustomProtocol = osName !== 'android' + + function sendIpcMessage(message) { + const { cmd, callback, error, payload, options } = message + + if ( + !customProtocolIpcFailed + && (canUseCustomProtocol || cmd === fetchChannelDataCommand) + ) { + const { contentType, data } = processIpcMessage(payload) + + const headers = new Headers((options && options.headers) || {}) + headers.set('Content-Type', contentType) + headers.set('Tauri-Callback', callback) + headers.set('Tauri-Error', error) + headers.set('Tauri-Invoke-Key', __TAURI_INVOKE_KEY__) + + fetch(window.__TAURI_INTERNALS__.convertFileSrc(cmd, 'ipc'), { + method: 'POST', + body: data, + headers + }) + .then((response) => { + const callbackId = + response.headers.get('Tauri-Response') === 'ok' ? callback : error + // we need to split here because on Android the content-type gets duplicated + switch ((response.headers.get('content-type') || '').split(',')[0]) { + case 'application/json': + return response.json().then((r) => [callbackId, r]) + case 'text/plain': + return response.text().then((r) => [callbackId, r]) + default: + return response.arrayBuffer().then((r) => [callbackId, r]) + } + }) + .then( + ([callbackId, data]) => { + window.__TAURI_INTERNALS__.runCallback(callbackId, data) + }, + (e) => { + console.warn( + 'IPC custom protocol failed, Tauri will now use the postMessage interface instead', + e + ) + // failed to use the custom protocol IPC (either the webview blocked a custom protocol or it was a CSP error) + // so we need to fallback to the postMessage interface + customProtocolIpcFailed = true + sendIpcMessage(message) + } + ) + } else { + // otherwise use the postMessage interface + const { data } = processIpcMessage({ + cmd, + callback, + error, + options: { + ...options, + customProtocolIpcBlocked: customProtocolIpcFailed + }, + payload, + __TAURI_INVOKE_KEY__ + }) + // `window.ipc.postMessage` came from `tauri-runtime-wry` > `wry` [`with_ipc_handler`](https://github.com/tauri-apps/wry/blob/a0403b9e2f1ff9d73be7dce1184f058afcaa1d82/src/lib.rs#L1130) + window.ipc.postMessage(data) + } + } + + Object.defineProperty(window.__TAURI_INTERNALS__, 'postMessage', { + value: sendIpcMessage + }) +})() diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/ipc.js b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/ipc.js new file mode 100644 index 000000000..d6aa08011 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/ipc.js @@ -0,0 +1,210 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * @typedef {{callback: string, error: string, data: *}} IsolationPayload - a valid isolation payload + */ + +;(function () { + /** + * @type {string} + */ + const pattern = window.__TAURI_INTERNALS__.__TAURI_PATTERN__.pattern + + /** + * @type {string} + */ + const isolationOrigin = __TEMPLATE_isolation_origin__ + + /** + * @type {{queue: object[], ready: boolean, frame: HTMLElement | null}} + */ + const isolation = Object.create(null) + isolation.queue = [] + isolation.ready = false + isolation.frame = null + + /** + * Detects if a message event is a valid isolation message. + * + * @param {MessageEvent} event - a message event that is expected to be an isolation message + * @return {boolean} - if the event was a valid isolation message + */ + function isIsolationMessage(event) { + if ( + typeof event.data === 'object' + && typeof event.data.payload === 'object' + ) { + const keys = Object.keys(event.data.payload || {}) + return ( + keys.length > 0 + && keys.every( + (key) => key === 'contentType' || key === 'nonce' || key === 'payload' + ) + ) + } + return false + } + + /** + * Detects if data is able to transform into an isolation payload. + * + * @param {object} data - object that is expected to contain at least a callback and error identifier + * @return {boolean} - if the data is able to transform into an isolation payload + */ + function isIsolationPayload(data) { + return ( + typeof data === 'object' + && 'callback' in data + && 'error' in data + && !isIsolationMessage(data) + ) + } + + /** + * Sends a properly formatted message to the isolation frame. + * + * @param {IsolationPayload} data - data that has been validated to be an isolation payload + */ + function sendIsolationMessage(data) { + // set the frame dom element if it's not been set before + if (!isolation.frame) { + const frame = document.querySelector('iframe#__tauri_isolation__') + if (frame.src.startsWith(isolationOrigin)) { + isolation.frame = frame + } else { + console.error( + 'Tauri IPC found an isolation iframe, but it had the wrong origin' + ) + } + } + + // ensure we have the target to send the message to + if (!isolation.frame || !isolation.frame.contentWindow) { + console.error( + 'Tauri "Isolation" Pattern could not find the Isolation iframe window' + ) + return + } + + // `postMessage` uses `structuredClone` to serialize the data before sending it + // unlike `JSON.stringify`, we can't extend a type with a method similar to `toJSON` + // so that `structuredClone` would use, so until https://github.com/whatwg/html/issues/7428 + // we manually call `toIPC` + function serializeIpcPayload(data) { + // if this value changes, make sure to update it in: + // 1. process-ipc-message-fn.js + // 2. core.ts + const SERIALIZE_TO_IPC_FN = '__TAURI_TO_IPC_KEY__' + + if ( + typeof data === 'object' + && data !== null + && 'constructor' in data + && data.constructor === Array + ) { + return data.map((v) => serializeIpcPayload(v)) + } + + if ( + typeof data === 'object' + && data !== null + && SERIALIZE_TO_IPC_FN in data + ) { + return data[SERIALIZE_TO_IPC_FN]() + } + + if ( + typeof data === 'object' + && data !== null + && 'constructor' in data + && data.constructor === Object + ) { + const acc = {} + Object.entries(data).forEach(([k, v]) => { + acc[k] = serializeIpcPayload(v) + }) + return acc + } + + return data + } + + data.payload = serializeIpcPayload(data.payload) + + isolation.frame.contentWindow.postMessage( + data, + '*' /* todo: set this to the secure origin */ + ) + } + + Object.defineProperty(window.__TAURI_INTERNALS__, 'ipc', { + // todo: JSDoc this function + value: Object.freeze((message) => { + switch (pattern) { + case 'brownfield': + window.__TAURI_INTERNALS__.postMessage(message) + break + + case 'isolation': + if (!isIsolationPayload(message)) { + console.error( + 'Tauri "Isolation" Pattern found an invalid isolation message payload', + message + ) + break + } + + if (isolation.ready) { + sendIsolationMessage(message) + } else { + isolation.queue.push(message) + } + + break + + case 'error': + console.error( + 'Tauri IPC found a Tauri Pattern, but it was an error. Check for other log messages to find the cause.' + ) + break + + default: + console.error( + 'Tauri IPC did not find a Tauri Pattern that it understood.' + ) + break + } + }) + }) + + /** + * IMPORTANT: See isolation_secure.js for the isolation frame implementation. + * main frame -> isolation frame = isolation payload + * isolation frame -> main frame = isolation message + */ + if (pattern === 'isolation') { + window.addEventListener( + 'message', + (event) => { + // watch for the isolation frame being ready and flush any queued messages + if (event.data === '__TAURI_ISOLATION_READY__') { + isolation.ready = true + + for (const message of isolation.queue) { + sendIsolationMessage(message) + } + + isolation.queue = [] + return + } + + if (isIsolationMessage(event)) { + window.__TAURI_INTERNALS__.postMessage(event.data) + } + }, + false + ) + } +})() diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/isolation.js b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/isolation.js new file mode 100644 index 000000000..19a34c9ba --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/isolation.js @@ -0,0 +1,17 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +if (location.href !== __TEMPLATE_isolation_src__) { + window.addEventListener('DOMContentLoaded', () => { + let style = document.createElement('style') + style.textContent = __TEMPLATE_style__ + document.head.append(style) + + let iframe = document.createElement('iframe') + iframe.id = '__tauri_isolation__' + iframe.sandbox.add('allow-scripts') + iframe.src = __TEMPLATE_isolation_src__ + document.body.append(iframe) + }) +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/pattern.js b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/pattern.js new file mode 100644 index 000000000..dce39b4ea --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/pattern.js @@ -0,0 +1,21 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +;(function () { + function __tauriDeepFreeze(object) { + const props = Object.getOwnPropertyNames(object) + + for (const prop of props) { + if (typeof object[prop] === 'object') { + __tauriDeepFreeze(object[prop]) + } + } + + return Object.freeze(object) + } + + Object.defineProperty(window.__TAURI_INTERNALS__, '__TAURI_PATTERN__', { + value: __tauriDeepFreeze(__TEMPLATE_pattern__) + }) +})() diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/process-ipc-message-fn.js b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/process-ipc-message-fn.js new file mode 100644 index 000000000..4ee2cb2f6 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/scripts/process-ipc-message-fn.js @@ -0,0 +1,46 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// this is a function and not an iife so use it carefully + +(function (message) { + if ( + message instanceof ArrayBuffer + || ArrayBuffer.isView(message) + || Array.isArray(message) + ) { + return { + contentType: 'application/octet-stream', + data: message + } + } else { + const data = JSON.stringify(message, (_k, val) => { + // if this value changes, make sure to update it in: + // 1. ipc.js + // 2. core.ts + const SERIALIZE_TO_IPC_FN = '__TAURI_TO_IPC_KEY__' + + if (val instanceof Map) { + return Object.fromEntries(val.entries()) + } else if (val instanceof Uint8Array) { + return Array.from(val) + } else if (val instanceof ArrayBuffer) { + return Array.from(new Uint8Array(val)) + } else if ( + typeof val === 'object' + && val !== null + && SERIALIZE_TO_IPC_FN in val + ) { + return val[SERIALIZE_TO_IPC_FN]() + } else { + return val + } + }) + + return { + contentType: 'application/json', + data + } + } +}) diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/app.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/app.rs new file mode 100644 index 000000000..d37d1273c --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/app.rs @@ -0,0 +1,2833 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{ + Context, DeviceEventFilter, Emitter, EventLoopMessage, EventName, Listener, Manager, Monitor, + Runtime, Scopes, StateManager, Theme, Webview, WebviewWindowBuilder, Window, + image::Image, + ipc::{ + CallbackFn, CommandArg, CommandItem, Invoke, InvokeError, InvokeHandler, InvokeResponseBody, + channel::ChannelDataIpcQueue, + }, + manager::{AppManager, Asset, webview::UriSchemeProtocol}, + plugin::{Plugin, PluginStore}, + resources::ResourceTable, + runtime::{ + ExitRequestedEventAction, RunEvent as RuntimeRunEvent, + window::{WebviewEvent as RuntimeWebviewEvent, WindowEvent as RuntimeWindowEvent}, + }, + sealed::{ManagerBase, RuntimeOrDispatch}, + utils::{Env, config::Config}, + webview::PageLoadPayload, +}; + +#[cfg(desktop)] +use crate::menu::{Menu, MenuEvent}; +#[cfg(all(desktop, feature = "tray-icon"))] +use crate::tray::{TrayIcon, TrayIconBuilder, TrayIconEvent, TrayIconId}; +use raw_window_handle::HasDisplayHandle; +use serialize_to_javascript::{DefaultTemplate, Template, default_template}; +use tauri_macros::default_runtime; +#[cfg(desktop)] +use tauri_runtime::EventLoopProxy; +use tauri_runtime::{ + RuntimeInitArgs, + dpi::{PhysicalPosition, PhysicalSize}, + window::DragDropEvent, +}; +use tauri_utils::{PackageInfo, assets::AssetsIter}; + +use std::{ + borrow::Cow, + collections::HashMap, + fmt, + sync::{Arc, Mutex, MutexGuard, atomic, mpsc::Sender}, + thread::ThreadId, + time::Duration, +}; + +use crate::{Event, EventTarget, event::EventId, runtime::RuntimeHandle}; + +#[cfg(target_os = "macos")] +use crate::ActivationPolicy; + +pub(crate) mod plugin; + +#[cfg(desktop)] +pub(crate) type GlobalMenuEventListener = Box; +#[cfg(all(desktop, feature = "tray-icon"))] +pub(crate) type GlobalTrayIconEventListener = + Box; +pub(crate) type GlobalWindowEventListener = Box, &WindowEvent) + Send + Sync>; +pub(crate) type GlobalWebviewEventListener = + Box, &WebviewEvent) + Send + Sync>; +/// A closure that is run when the Tauri application is setting up. +pub type SetupHook = + Box) -> std::result::Result<(), Box> + Send>; +/// A closure that is run every time a page starts or finishes loading. +pub type OnPageLoad = dyn Fn(&Webview, &PageLoadPayload<'_>) + Send + Sync + 'static; +/// A closure that is run when the web content process terminates. +#[cfg(any(target_os = "macos", target_os = "ios"))] +pub type OnWebContentProcessTerminate = dyn Fn(&Webview) + Send + Sync + 'static; +pub type ChannelInterceptor = + Box, CallbackFn, usize, &InvokeResponseBody) -> bool + Send + Sync + 'static>; + +/// The exit code on [`RunEvent::ExitRequested`] when [`AppHandle#method.restart`] is called. +pub const RESTART_EXIT_CODE: i32 = i32::MAX; + +/// Api exposed on the `ExitRequested` event. +#[derive(Debug, Clone)] +pub struct ExitRequestApi { + tx: Sender, + code: Option, +} + +impl ExitRequestApi { + /// Prevents the app from exiting. + /// + /// **Note:** This is ignored when using [`AppHandle#method.restart`]. + pub fn prevent_exit(&self) { + if self.code != Some(RESTART_EXIT_CODE) { + self.tx.send(ExitRequestedEventAction::Prevent).unwrap(); + } + } +} + +/// Api exposed on the `CloseRequested` event. +#[derive(Debug, Clone)] +pub struct CloseRequestApi(Sender); + +impl CloseRequestApi { + /// Prevents the window from being closed. + pub fn prevent_close(&self) { + self.0.send(true).unwrap(); + } +} + +/// An event from a window. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum WindowEvent { + /// The size of the window has changed. Contains the client area's new dimensions. + Resized(PhysicalSize), + /// The position of the window has changed. Contains the window's new position. + Moved(PhysicalPosition), + /// The window has been requested to close. + #[non_exhaustive] + CloseRequested { + /// An API modify the behavior of the close requested event. + api: CloseRequestApi, + }, + /// The window has been destroyed. + Destroyed, + /// The window gained or lost focus. + /// + /// The parameter is true if the window has gained focus, and false if it has lost focus. + Focused(bool), + /// The window's scale factor has changed. + /// + /// The following user actions can cause DPI changes: + /// + /// - Changing the display's resolution. + /// - Changing the display's scale factor (e.g. in Control Panel on Windows). + /// - Moving the window to a display with a different scale factor. + #[non_exhaustive] + ScaleFactorChanged { + /// The new scale factor. + scale_factor: f64, + /// The window inner size. + new_inner_size: PhysicalSize, + }, + /// An event associated with the drag and drop action. + DragDrop(DragDropEvent), + /// The system window theme has changed. Only delivered if the window [`theme`](`crate::window::WindowBuilder#method.theme`) is `None`. + /// + /// Applications might wish to react to this to change the theme of the content of the window when the system changes the window theme. + /// + /// ## Platform-specific + /// + /// - **Linux**: Not supported. + ThemeChanged(Theme), + /// Emitted when the application has been suspended. + /// + /// ## Platform-specific + /// + /// - **Android**: This is triggered by `onPause` method of the Activity. + /// - **iOS**: This is triggered by `applicationWillResignActive` method of the UIApplicationDelegate. + /// - **Linux / macOS / Windows**: Unsupported. + #[cfg(mobile)] + Suspended, + /// Emitted when the application has been resumed. + /// + /// ## Platform-specific + /// + /// - **Android**: This is triggered by `onResume` method of the Activity. The first onResume() is ignored to match the iOS implementation, since that is called on activity creation. + /// - **iOS**: This is triggered by `applicationWillEnterForeground` method of the UIApplicationDelegate. + /// - **Linux / macOS / Windows**: Unsupported. + #[cfg(mobile)] + Resumed, +} + +impl From for WindowEvent { + fn from(event: RuntimeWindowEvent) -> Self { + match event { + RuntimeWindowEvent::Resized(size) => Self::Resized(size), + RuntimeWindowEvent::Moved(position) => Self::Moved(position), + RuntimeWindowEvent::CloseRequested { signal_tx } => Self::CloseRequested { + api: CloseRequestApi(signal_tx), + }, + RuntimeWindowEvent::Destroyed => Self::Destroyed, + RuntimeWindowEvent::Focused(flag) => Self::Focused(flag), + RuntimeWindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size, + } => Self::ScaleFactorChanged { + scale_factor, + new_inner_size, + }, + RuntimeWindowEvent::DragDrop(event) => Self::DragDrop(event), + RuntimeWindowEvent::ThemeChanged(theme) => Self::ThemeChanged(theme), + #[cfg(mobile)] + RuntimeWindowEvent::Suspended => Self::Suspended, + #[cfg(mobile)] + RuntimeWindowEvent::Resumed => Self::Resumed, + } + } +} + +/// An event from a window. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum WebviewEvent { + /// An event associated with the drag and drop action. + DragDrop(DragDropEvent), +} + +impl From for WebviewEvent { + fn from(event: RuntimeWebviewEvent) -> Self { + match event { + RuntimeWebviewEvent::DragDrop(e) => Self::DragDrop(e), + } + } +} + +/// An application event, triggered from the event loop. +/// +/// See [`App::run`](crate::App#method.run) for usage examples. +#[derive(Debug)] +#[non_exhaustive] +pub enum RunEvent { + /// Event loop is exiting. + Exit, + /// The app is about to exit + #[non_exhaustive] + ExitRequested { + /// Exit code. + /// [`Option::None`] when the exit is requested by user interaction, + /// [`Option::Some`] when requested programmatically via [`AppHandle#method.exit`] and [`AppHandle#method.restart`]. + code: Option, + /// Event API + api: ExitRequestApi, + }, + /// An event associated with a window. + #[non_exhaustive] + WindowEvent { + /// The window label. + label: String, + /// The detailed event. + event: WindowEvent, + }, + /// An event associated with a webview. + #[non_exhaustive] + WebviewEvent { + /// The window label. + label: String, + /// The detailed event. + event: WebviewEvent, + }, + /// Application ready. + Ready, + /// Sent if the event loop is being resumed. + Resumed, + /// Emitted when all of the event loop's input events have been processed and redraw processing is about to begin. + /// + /// This event is useful as a place to put your code that should be run after all state-changing events have been handled and you want to do stuff (updating state, performing calculations, etc) that happens as the "main body" of your event loop. + MainEventsCleared, + /// Emitted when the user wants to open the specified resource with the app. + #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))] + #[cfg_attr( + docsrs, + doc(cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))) + )] + Opened { + /// The URL of the resources that is being open. + urls: Vec, + }, + /// An event from a menu item, could be on the window menu bar, application menu bar (on macOS) or tray icon menu. + #[cfg(desktop)] + #[cfg_attr(docsrs, doc(cfg(desktop)))] + MenuEvent(crate::menu::MenuEvent), + /// An event from a tray icon. + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "tray-icon"))))] + TrayIconEvent(crate::tray::TrayIconEvent), + /// Emitted when the NSApplicationDelegate's applicationShouldHandleReopen gets called + #[non_exhaustive] + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + Reopen { + /// Indicates whether the NSApplication object found any visible windows in your application. + has_visible_windows: bool, + }, + /// Emitted when a scene is requested by the system. + /// + /// This event is emitted when a scene is requested by the system. + /// Scenes created by [`Window::new`] are not emitted with this event. + /// It is also not emitted for the main scene. + #[cfg(target_os = "ios")] + #[cfg_attr(docsrs, doc(cfg(target_os = "ios")))] + SceneRequested { + /// Scene that was requested by the system. + scene: objc2::rc::Retained, + /// Options that were used to request the scene. + /// + /// This lets you determine why the scene was requested. + options: objc2::rc::Retained, + }, +} + +impl From for RunEvent { + fn from(event: EventLoopMessage) -> Self { + match event { + #[cfg(desktop)] + EventLoopMessage::MenuEvent(e) => Self::MenuEvent(e), + #[cfg(all(desktop, feature = "tray-icon"))] + EventLoopMessage::TrayIconEvent(e) => Self::TrayIconEvent(e), + } + } +} + +/// The asset resolver is a helper to access the [`crate::Assets`] interface. +#[derive(Debug, Clone)] +pub struct AssetResolver { + manager: Arc>, +} + +impl AssetResolver { + /// Gets the app asset associated with the given path. + /// + /// By default it tries to infer your application's URL scheme in production by checking if all webviews + /// were configured with [`crate::webview::WebviewBuilder::use_https_scheme`] or `tauri.conf.json > app > windows > useHttpsScheme`. + /// If you are resolving an asset for a webview with a more dynamic configuration, see [`AssetResolver::get_for_scheme`]. + /// + /// In production, this resolves to the embedded asset bundled in the app executable + /// which contains your frontend assets in [`frontendDist`](https://v2.tauri.app/reference/config/#frontenddist) during build time. + /// + /// In dev mode, if [`devUrl`](https://v2.tauri.app/reference/config/#devurl) is set, we don't bundle the assets to reduce re-builds, + /// and this will fall back to read from `frontendDist` directly. + /// Note that the dist directory must exist so you might need to build your frontend assets first. + pub fn get(&self, path: String) -> Option { + let use_https_scheme = self + .manager + .webviews() + .values() + .all(|webview| webview.use_https_scheme()); + self.get_for_scheme(path, use_https_scheme) + } + + /// Same as [`AssetResolver::get`] but resolves the custom protocol scheme based on a parameter. + /// + /// - `use_https_scheme`: If `true` when using [`Pattern::Isolation`](crate::Pattern::Isolation), + /// the csp header will contain `https://tauri.localhost` instead of `http://tauri.localhost` + pub fn get_for_scheme(&self, path: String, use_https_scheme: bool) -> Option { + #[cfg(dev)] + { + // We don't bundle the assets when in dev mode and `devUrl` is set, so fall back to read from `frontendDist` directly + // TODO: Maybe handle `FrontendDist::Files` as well + if let (Some(_), Some(crate::utils::config::FrontendDist::Directory(dist_path))) = ( + &self.manager.config().build.dev_url, + &self.manager.config().build.frontend_dist, + ) { + let asset_path = std::path::PathBuf::from(&path) + .components() + .filter(|c| !matches!(c, std::path::Component::RootDir)) + .collect::(); + + let asset_path = self + .manager + .config_parent() + .map(|p| p.join(dist_path).join(&asset_path)) + .unwrap_or_else(|| dist_path.join(&asset_path)); + return std::fs::read(asset_path).ok().map(|bytes| { + let mime_type = crate::utils::mime_type::MimeType::parse(&bytes, &path); + Asset { + bytes, + mime_type, + csp_header: None, + } + }); + } + } + + self.manager.get_asset(path, use_https_scheme).ok() + } + + /// Iterate on all assets. + pub fn iter(&self) -> Box> { + self.manager.assets.iter() + } +} + +/// A handle to the currently running application. +/// +/// This type implements [`Manager`] which allows for manipulation of global application items. +#[default_runtime(crate::Wry, wry)] +#[derive(Debug)] +pub struct AppHandle { + pub(crate) runtime_handle: R::Handle, + pub(crate) manager: Arc>, + event_loop: Arc>, +} + +/// Not the real event loop, only contains the main thread id of the event loop +#[derive(Debug)] +struct EventLoop { + main_thread_id: ThreadId, +} + +/// APIs specific to the wry runtime. +#[cfg(feature = "wry")] +impl AppHandle { + /// Create a new tao window using a callback. The event loop must be running at this point. + pub fn create_tao_window< + F: FnOnce() -> (String, tauri_runtime_wry::TaoWindowBuilder) + Send + 'static, + >( + &self, + f: F, + ) -> crate::Result> { + self.runtime_handle.create_tao_window(f).map_err(Into::into) + } + + /// Sends a window message to the event loop. + pub fn send_tao_window_event( + &self, + window_id: tauri_runtime_wry::TaoWindowId, + message: tauri_runtime_wry::WindowMessage, + ) -> crate::Result<()> { + self + .runtime_handle + .send_event(tauri_runtime_wry::Message::Window( + self.runtime_handle.window_id(window_id), + message, + )) + .map_err(Into::into) + } +} + +#[cfg(target_vendor = "apple")] +impl AppHandle { + /// Fetches all Data Store Identifiers by this app + /// + /// Needs to be called from Main Thread + pub async fn fetch_data_store_identifiers(&self) -> crate::Result> { + let (tx, rx) = tokio::sync::oneshot::channel::, tauri_runtime::Error>>(); + let lock: Arc>> = Arc::new(Mutex::new(Some(tx))); + let runtime_handle = self.runtime_handle.clone(); + + self.run_on_main_thread(move || { + let cloned_lock = lock.clone(); + if let Err(err) = runtime_handle.fetch_data_store_identifiers(move |ids| { + if let Some(tx) = cloned_lock.lock().unwrap().take() { + let _ = tx.send(Ok(ids)); + } + }) { + if let Some(tx) = lock.lock().unwrap().take() { + let _ = tx.send(Err(err)); + } + } + })?; + + rx.await?.map_err(Into::into) + } + /// Deletes a Data Store of this app + /// + /// Needs to be called from Main Thread + pub async fn remove_data_store(&self, uuid: [u8; 16]) -> crate::Result<()> { + let (tx, rx) = tokio::sync::oneshot::channel::>(); + let lock: Arc>> = Arc::new(Mutex::new(Some(tx))); + let runtime_handle = self.runtime_handle.clone(); + + self.run_on_main_thread(move || { + let cloned_lock = lock.clone(); + if let Err(err) = runtime_handle.remove_data_store(uuid, move |result| { + if let Some(tx) = cloned_lock.lock().unwrap().take() { + let _ = tx.send(result); + } + }) { + if let Some(tx) = lock.lock().unwrap().take() { + let _ = tx.send(Err(err)); + } + } + })?; + rx.await?.map_err(Into::into) + } +} + +impl Clone for AppHandle { + fn clone(&self) -> Self { + Self { + runtime_handle: self.runtime_handle.clone(), + manager: self.manager.clone(), + event_loop: self.event_loop.clone(), + } + } +} + +impl<'de, R: Runtime> CommandArg<'de, R> for AppHandle { + /// Grabs the [`Window`] from the [`CommandItem`] and returns the associated [`AppHandle`]. This will never fail. + fn from_command(command: CommandItem<'de, R>) -> std::result::Result { + Ok(command.message.webview().app_handle) + } +} + +impl AppHandle { + /// Runs the given closure on the main thread. + pub fn run_on_main_thread(&self, f: F) -> crate::Result<()> { + self + .runtime_handle + .run_on_main_thread(f) + .map_err(Into::into) + } + + /// Adds a Tauri application plugin. + /// This function can be used to register a plugin that is loaded dynamically e.g. after login. + /// For plugins that are created when the app is started, prefer [`Builder::plugin`]. + /// + /// See [`Builder::plugin`] for more information. + /// + /// # Examples + /// + /// ``` + /// use tauri::{plugin::{Builder as PluginBuilder, TauriPlugin}, Runtime}; + /// + /// fn init_plugin() -> TauriPlugin { + /// PluginBuilder::new("dummy").build() + /// } + /// + /// tauri::Builder::default() + /// .setup(move |app| { + /// let handle = app.handle().clone(); + /// std::thread::spawn(move || { + /// handle.plugin(init_plugin()); + /// }); + /// + /// Ok(()) + /// }); + /// ``` + pub fn plugin + 'static>(&self, plugin: P) -> crate::Result<()> { + self.plugin_boxed(Box::new(plugin)) + } + + /// Adds a Tauri application plugin. + /// + /// This method is similar to [`Self::plugin`], + /// but accepts a boxed trait object instead of a generic type. + #[cfg_attr(feature = "tracing", tracing::instrument(name = "app::plugin::register", skip(plugin), fields(name = plugin.name())))] + pub fn plugin_boxed(&self, mut plugin: Box>) -> crate::Result<()> { + let mut store = self.manager().plugins.lock().unwrap(); + store.initialize(&mut plugin, self, &self.config().plugins)?; + store.register(plugin); + + Ok(()) + } + + /// Removes the plugin with the given name. + /// + /// # Examples + /// + /// ``` + /// use tauri::{plugin::{Builder as PluginBuilder, TauriPlugin, Plugin}, Runtime}; + /// + /// fn init_plugin() -> TauriPlugin { + /// PluginBuilder::new("dummy").build() + /// } + /// + /// let plugin = init_plugin(); + /// // `.name()` requires the `Plugin` trait import + /// let plugin_name = plugin.name(); + /// tauri::Builder::default() + /// .plugin(plugin) + /// .setup(move |app| { + /// let handle = app.handle().clone(); + /// std::thread::spawn(move || { + /// handle.remove_plugin(plugin_name); + /// }); + /// + /// Ok(()) + /// }); + /// ``` + pub fn remove_plugin(&self, plugin: &str) -> bool { + self.manager().plugins.lock().unwrap().unregister(plugin) + } + + /// Exits the app by triggering [`RunEvent::ExitRequested`] and [`RunEvent::Exit`]. + /// + /// ## Platform-specific + /// + /// - **Android**: Unless the exit is prevented, the activity is finished instead of the process being exited directly, + /// so the app closes with the system transition and the activity lifecycle callbacks run. + /// [`RunEvent::Exit`] is triggered once the activity is destroyed, and `exit_code` is not used as the process exit code. + pub fn exit(&self, exit_code: i32) { + if let Err(e) = self.runtime_handle.request_exit(exit_code) { + log::error!("failed to exit: {}", e); + self.cleanup_before_exit(); + std::process::exit(exit_code); + } + } + + /// Finishes the Android activity, and any other activity of its task, so the app closes gracefully. + #[cfg(target_os = "android")] + pub(crate) fn finish_activity(&self) -> crate::Result<()> { + // the app plugin is a core plugin, so its Android handle is always managed + self + .state::>() + .0 + .run_mobile_plugin::<()>("exit", ())?; + Ok(()) + } + + /// Restarts the app by triggering [`RunEvent::ExitRequested`] with code [`RESTART_EXIT_CODE`](crate::RESTART_EXIT_CODE) and [`RunEvent::Exit`]. + /// + /// When this function is called on the main thread, we cannot guarantee the delivery of those events, + /// so we skip them and directly restart the process. + /// + /// If you want to trigger them reliably, use [`Self::request_restart`] instead + pub fn restart(&self) -> ! { + if self.event_loop.lock().unwrap().main_thread_id == std::thread::current().id() { + log::debug!("restart triggered on the main thread"); + self.cleanup_before_exit(); + crate::process::restart(&self.env()); + } else { + log::debug!("restart triggered from a separate thread"); + // we're running on a separate thread, so we must trigger the exit request and wait for it to finish + self + .manager + .restart_on_exit + .store(true, atomic::Ordering::Relaxed); + // We'll be restarting when we receive the next `RuntimeRunEvent::Exit` event in `App::run` if this call succeed + match self.runtime_handle.request_exit(RESTART_EXIT_CODE) { + Ok(()) => loop { + std::thread::sleep(Duration::MAX); + }, + Err(e) => { + log::error!("failed to request exit: {e}"); + self.cleanup_before_exit(); + crate::process::restart(&self.env()); + } + } + } + } + + /// Restarts the app by triggering [`RunEvent::ExitRequested`] with code [`RESTART_EXIT_CODE`] and [`RunEvent::Exit`]. + pub fn request_restart(&self) { + self + .manager + .restart_on_exit + .store(true, atomic::Ordering::Relaxed); + // We'll be restarting when we receive the next `RuntimeRunEvent::Exit` event in `App::run` if this call succeed + if self.runtime_handle.request_exit(RESTART_EXIT_CODE).is_err() { + self.cleanup_before_exit(); + crate::process::restart(&self.env()); + } + } + + /// Sets the activation policy for the application. It is set to `NSApplicationActivationPolicyRegular` by default. + /// + /// # Examples + /// ```,no_run + /// tauri::Builder::default() + /// .setup(move |app| { + /// #[cfg(target_os = "macos")] + /// app.handle().set_activation_policy(tauri::ActivationPolicy::Accessory); + /// Ok(()) + /// }); + /// ``` + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + pub fn set_activation_policy(&self, activation_policy: ActivationPolicy) -> crate::Result<()> { + self + .runtime_handle + .set_activation_policy(activation_policy) + .map_err(Into::into) + } + + /// Sets the dock visibility for the application. + /// + /// # Examples + /// ```,no_run + /// tauri::Builder::default() + /// .setup(move |app| { + /// #[cfg(target_os = "macos")] + /// app.handle().set_dock_visibility(false); + /// Ok(()) + /// }); + /// ``` + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + pub fn set_dock_visibility(&self, visible: bool) -> crate::Result<()> { + self + .runtime_handle + .set_dock_visibility(visible) + .map_err(Into::into) + } + + /// Change the device event filter mode. + /// + /// See [App::set_device_event_filter] for details. + /// + /// ## Platform-specific + /// + /// See [App::set_device_event_filter] for details. + pub fn set_device_event_filter(&self, filter: DeviceEventFilter) { + self.runtime_handle.set_device_event_filter(filter); + } + + /// Whether the application supports multiple windows. + #[cfg(target_os = "ios")] + pub fn supports_multiple_windows(&self) -> bool { + let (tx, rx) = std::sync::mpsc::channel(); + let _ = self.run_on_main_thread(move || { + let mtm = objc2::MainThreadMarker::new().unwrap(); + let ui_application = objc2_ui_kit::UIApplication::sharedApplication(mtm); + tx.send(ui_application.supportsMultipleScenes()).unwrap(); + }); + rx.recv().unwrap() + } +} + +impl Manager for AppHandle { + fn resources_table(&self) -> MutexGuard<'_, ResourceTable> { + self.manager.resources_table() + } +} + +impl ManagerBase for AppHandle { + fn manager(&self) -> &AppManager { + &self.manager + } + + fn manager_owned(&self) -> Arc> { + self.manager.clone() + } + + fn runtime(&self) -> RuntimeOrDispatch<'_, R> { + RuntimeOrDispatch::RuntimeHandle(self.runtime_handle.clone()) + } + + fn managed_app_handle(&self) -> &AppHandle { + self + } + + #[cfg(target_os = "android")] + fn activity_name(&self) -> Option> { + None + } + + #[cfg(target_os = "ios")] + fn scene_identifier(&self) -> Option> { + None + } +} + +/// The instance of the currently running application. +/// +/// This type implements [`Manager`] which allows for manipulation of global application items. +#[default_runtime(crate::Wry, wry)] +pub struct App { + runtime: Option, + setup: Option>, + manager: Arc>, + handle: AppHandle, + ran_setup: bool, +} + +impl fmt::Debug for App { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("App") + .field("runtime", &self.runtime) + .field("manager", &self.manager) + .field("handle", &self.handle) + .finish() + } +} + +impl Manager for App { + fn resources_table(&self) -> MutexGuard<'_, ResourceTable> { + self.manager.resources_table() + } +} + +impl ManagerBase for App { + fn manager(&self) -> &AppManager { + &self.manager + } + + fn manager_owned(&self) -> Arc> { + self.manager.clone() + } + + fn runtime(&self) -> RuntimeOrDispatch<'_, R> { + if let Some(runtime) = self.runtime.as_ref() { + RuntimeOrDispatch::Runtime(runtime) + } else { + self.handle.runtime() + } + } + + fn managed_app_handle(&self) -> &AppHandle { + self.handle() + } + + #[cfg(target_os = "android")] + fn activity_name(&self) -> Option> { + None + } + + #[cfg(target_os = "ios")] + fn scene_identifier(&self) -> Option> { + None + } +} + +/// APIs specific to the wry runtime. +#[cfg(feature = "wry")] +impl App { + /// Adds a [`tauri_runtime_wry::Plugin`] using its [`tauri_runtime_wry::PluginBuilder`]. + /// + /// # Stability + /// + /// This API is unstable. + pub fn wry_plugin + Send + 'static>( + &mut self, + plugin: P, + ) where +

>::Plugin: Send, + { + self.handle.runtime_handle.plugin(plugin); + } +} + +macro_rules! shared_app_impl { + ($app: ty) => { + impl $app { + /// Registers a global menu event listener. + #[cfg(desktop)] + pub fn on_menu_event, MenuEvent) + Send + Sync + 'static>( + &self, + handler: F, + ) { + self.manager.menu.on_menu_event(handler) + } + + /// Registers a global tray icon menu event listener. + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "tray-icon"))))] + pub fn on_tray_icon_event, TrayIconEvent) + Send + Sync + 'static>( + &self, + handler: F, + ) { + self.manager.tray.on_tray_icon_event(handler) + } + + /// Gets a tray icon using the provided id. + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "tray-icon"))))] + pub fn tray_by_id<'a, I>(&self, id: &'a I) -> Option> + where + I: ?Sized, + TrayIconId: PartialEq<&'a I>, + { + self.manager.tray.tray_by_id(self.app_handle(), id) + } + + /// Removes a tray icon using the provided id from tauri's internal state and returns it. + /// + /// Note that dropping the returned icon, may cause the tray icon to disappear + /// if it wasn't cloned somewhere else or referenced by JS. + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "tray-icon"))))] + pub fn remove_tray_by_id<'a, I>(&self, id: &'a I) -> Option> + where + I: ?Sized, + TrayIconId: PartialEq<&'a I>, + { + self.manager.tray.remove_tray_by_id(self.app_handle(), id) + } + + /// Gets the app's configuration, defined on the `tauri.conf.json` file. + pub fn config(&self) -> &Config { + self.manager.config() + } + + /// Gets the app's package information. + pub fn package_info(&self) -> &PackageInfo { + self.manager.package_info() + } + + /// The application's asset resolver. + pub fn asset_resolver(&self) -> AssetResolver { + AssetResolver { + manager: self.manager.clone(), + } + } + + /// Returns the primary monitor of the system. + /// + /// Returns None if it can't identify any monitor as a primary one. + pub fn primary_monitor(&self) -> crate::Result> { + Ok(match self.runtime() { + RuntimeOrDispatch::Runtime(h) => h.primary_monitor().map(Into::into), + RuntimeOrDispatch::RuntimeHandle(h) => h.primary_monitor()?.map(Into::into), + _ => unreachable!(), + }) + } + + /// Returns the monitor that contains the given point. + pub fn monitor_from_point(&self, x: f64, y: f64) -> crate::Result> { + Ok(match self.runtime() { + RuntimeOrDispatch::Runtime(h) => h.monitor_from_point(x, y).map(Into::into), + RuntimeOrDispatch::RuntimeHandle(h) => h.monitor_from_point(x, y)?.map(Into::into), + _ => unreachable!(), + }) + } + + /// Returns the list of all the monitors available on the system. + pub fn available_monitors(&self) -> crate::Result> { + Ok(match self.runtime() { + RuntimeOrDispatch::Runtime(h) => { + h.available_monitors().into_iter().map(Into::into).collect() + } + RuntimeOrDispatch::RuntimeHandle(h) => h + .available_monitors()? + .into_iter() + .map(Into::into) + .collect(), + _ => unreachable!(), + }) + } + + /// Get the cursor position relative to the top-left hand corner of the desktop. + /// + /// Note that the top-left hand corner of the desktop is not necessarily the same as the screen. + /// If the user uses a desktop with multiple monitors, + /// the top-left hand corner of the desktop is the top-left hand corner of the main monitor on Windows and macOS + /// or the top-left of the leftmost monitor on X11. + /// + /// The coordinates can be negative if the top-left hand corner of the window is outside of the visible screen region. + pub fn cursor_position(&self) -> crate::Result> { + Ok(match self.runtime() { + RuntimeOrDispatch::Runtime(h) => h.cursor_position()?, + RuntimeOrDispatch::RuntimeHandle(h) => h.cursor_position()?, + _ => unreachable!(), + }) + } + + /// Sets the app theme. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + pub fn set_theme(&self, theme: Option) { + #[cfg(windows)] + for window in self.manager.windows().values() { + if let (Some(menu), Ok(hwnd)) = (window.menu(), window.hwnd()) { + let raw_hwnd = hwnd.0 as isize; + let _ = self.run_on_main_thread(move || { + let _ = unsafe { + menu.inner().set_theme_for_hwnd( + raw_hwnd, + theme + .map(crate::menu::map_to_menu_theme) + .unwrap_or(muda::MenuTheme::Auto), + ) + }; + }); + }; + } + match self.runtime() { + RuntimeOrDispatch::Runtime(h) => h.set_theme(theme), + RuntimeOrDispatch::RuntimeHandle(h) => h.set_theme(theme), + _ => unreachable!(), + } + } + + /// Returns the default window icon. + pub fn default_window_icon(&self) -> Option<&Image<'_>> { + self.manager.window.default_icon.as_ref() + } + + /// Returns the app-wide menu. + #[cfg(desktop)] + pub fn menu(&self) -> Option> { + self.manager.menu.menu_lock().clone() + } + + /// Sets the app-wide menu and returns the previous one. + /// + /// If a window was not created with an explicit menu or had one set explicitly, + /// this menu will be assigned to it. + #[cfg(desktop)] + pub fn set_menu(&self, menu: Menu) -> crate::Result>> { + let prev_menu = self.remove_menu()?; + + self.manager.menu.insert_menu_into_stash(&menu); + + self.manager.menu.menu_lock().replace(menu.clone()); + + // set it on all windows that don't have one or previously had the app-wide menu + #[cfg(not(target_os = "macos"))] + { + for window in self.manager.windows().values() { + let has_app_wide_menu = window.has_app_wide_menu() || window.menu().is_none(); + if has_app_wide_menu { + window.set_menu(menu.clone())?; + window.menu_lock().replace(crate::window::WindowMenu { + is_app_wide: true, + menu: menu.clone(), + }); + } + } + } + + // set it app-wide for macos + #[cfg(target_os = "macos")] + { + let menu_ = menu.clone(); + self.run_on_main_thread(move || { + let _ = init_app_menu(&menu_); + })?; + } + + Ok(prev_menu) + } + + /// Remove the app-wide menu and returns it. + /// + /// If a window was not created with an explicit menu or had one set explicitly, + /// this will remove the menu from it. + #[cfg(desktop)] + pub fn remove_menu(&self) -> crate::Result>> { + let menu = self.manager.menu.menu_lock().as_ref().cloned(); + #[allow(unused_variables)] + if let Some(menu) = menu { + // remove from windows that have the app-wide menu + #[cfg(not(target_os = "macos"))] + { + for window in self.manager.windows().values() { + let has_app_wide_menu = window.has_app_wide_menu(); + if has_app_wide_menu { + window.remove_menu()?; + *window.menu_lock() = None; + } + } + } + + // remove app-wide for macos + #[cfg(target_os = "macos")] + { + self.run_on_main_thread(move || { + menu.inner().remove_for_nsapp(); + })?; + } + } + + let prev_menu = self.manager.menu.menu_lock().take(); + + self + .manager + .remove_menu_from_stash_by_id(prev_menu.as_ref().map(|m| m.id())); + + Ok(prev_menu) + } + + /// Hides the app-wide menu from windows that have it. + /// + /// If a window was not created with an explicit menu or had one set explicitly, + /// this will hide the menu from it. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Unsupported. + #[cfg(desktop)] + pub fn hide_menu(&self) -> crate::Result<()> { + #[cfg(not(target_os = "macos"))] + { + let is_app_menu_set = self.manager.menu.menu_lock().is_some(); + if is_app_menu_set { + for window in self.manager.windows().values() { + if window.has_app_wide_menu() { + window.hide_menu()?; + } + } + } + } + + Ok(()) + } + + /// Shows the app-wide menu for windows that have it. + /// + /// If a window was not created with an explicit menu or had one set explicitly, + /// this will show the menu for it. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Unsupported. + #[cfg(desktop)] + pub fn show_menu(&self) -> crate::Result<()> { + #[cfg(not(target_os = "macos"))] + { + let is_app_menu_set = self.manager.menu.menu_lock().is_some(); + if is_app_menu_set { + for window in self.manager.windows().values() { + if window.has_app_wide_menu() { + window.show_menu()?; + } + } + } + } + + Ok(()) + } + + /// Shows the application, but does not automatically focus it. + #[cfg(target_os = "macos")] + pub fn show(&self) -> crate::Result<()> { + match self.runtime() { + RuntimeOrDispatch::Runtime(r) => r.show(), + RuntimeOrDispatch::RuntimeHandle(h) => h.show()?, + _ => unreachable!(), + } + Ok(()) + } + + /// Hides the application. + #[cfg(target_os = "macos")] + pub fn hide(&self) -> crate::Result<()> { + match self.runtime() { + RuntimeOrDispatch::Runtime(r) => r.hide(), + RuntimeOrDispatch::RuntimeHandle(h) => h.hide()?, + _ => unreachable!(), + } + Ok(()) + } + + /// Runs necessary cleanup tasks before exiting the process. + /// **You should always exit the tauri app immediately after this function returns and not use any tauri-related APIs.** + pub fn cleanup_before_exit(&self) { + #[cfg(all(desktop, feature = "tray-icon"))] + self.manager.tray.icons.lock().unwrap().clear(); + self.manager.resources_table().clear(); + for (_, window) in self.manager.windows() { + window.resources_table().clear(); + #[cfg(windows)] + let _ = window.hide(); + } + for (_, webview) in self.manager.webviews() { + webview.resources_table().clear(); + } + } + + /// Gets the invoke key that must be referenced when using [`crate::webview::InvokeRequest`]. + /// + /// # Security + /// + /// DO NOT expose this key to third party scripts as might grant access to the backend from external URLs and iframes. + pub fn invoke_key(&self) -> &str { + self.manager.invoke_key() + } + + /// Whether the application supports multiple windows. + #[cfg(desktop)] + pub fn supports_multiple_windows(&self) -> bool { + true + } + + /// Whether the application supports multiple windows. + #[cfg(target_os = "android")] + pub fn supports_multiple_windows(&self) -> bool { + let runtime_handle = match self.runtime() { + RuntimeOrDispatch::Runtime(runtime) => runtime.handle(), + RuntimeOrDispatch::RuntimeHandle(handle) => handle, + _ => unreachable!(), + }; + + let (tx, rx) = std::sync::mpsc::channel(); + + runtime_handle.run_on_android_context(move |env, _activity, _webview| { + let supports = (|| { + let version_class = env.find_class("android/os/Build$VERSION")?; + let sdk = env + .get_static_field(version_class, "SDK_INT", "I")? + .i() + .unwrap_or_default(); + crate::Result::Ok(sdk >= 32) + })() + .unwrap_or(false); + + let _ = tx.send(supports); + }); + + rx.recv().unwrap_or(false) + } + } + + impl Listener for $app { + /// Listen to an event on this app. + /// + /// # Examples + /// + /// ``` + /// use tauri::Listener; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// app.listen("component-loaded", move |event| { + /// println!("window just loaded a component"); + /// }); + /// + /// Ok(()) + /// }); + /// ``` + fn listen(&self, event: impl Into, handler: F) -> EventId + where + F: Fn(Event) + Send + 'static, + { + let event = EventName::new(event.into()).unwrap(); + self.manager.listen(event, EventTarget::App, handler) + } + + /// Listen to an event on this app only once. + /// + /// See [`Self::listen`] for more information. + fn once(&self, event: impl Into, handler: F) -> EventId + where + F: FnOnce(Event) + Send + 'static, + { + let event = EventName::new(event.into()).unwrap(); + self.manager.once(event, EventTarget::App, handler) + } + + /// Unlisten to an event on this app. + /// + /// # Examples + /// + /// ``` + /// use tauri::Listener; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let handler = app.listen("component-loaded", move |event| { + /// println!("app just loaded a component"); + /// }); + /// + /// // stop listening to the event when you do not need it anymore + /// app.unlisten(handler); + /// + /// Ok(()) + /// }); + /// ``` + fn unlisten(&self, id: EventId) { + self.manager.unlisten(id) + } + } + + impl Emitter for $app {} + }; +} + +shared_app_impl!(App); +shared_app_impl!(AppHandle); + +impl App { + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "app::core_plugins::register") + )] + fn register_core_plugins(&self) -> crate::Result<()> { + self.handle.plugin(crate::path::plugin::init())?; + self.handle.plugin(crate::event::plugin::init(self))?; + self.handle.plugin(crate::window::plugin::init())?; + self.handle.plugin(crate::webview::plugin::init())?; + self.handle.plugin(crate::app::plugin::init())?; + self.handle.plugin(crate::resources::plugin::init())?; + self.handle.plugin(crate::image::plugin::init())?; + #[cfg(desktop)] + self.handle.plugin(crate::menu::plugin::init())?; + #[cfg(all(desktop, feature = "tray-icon"))] + self.handle.plugin(crate::tray::plugin::init())?; + Ok(()) + } + + /// Runs the given closure on the main thread. + pub fn run_on_main_thread(&self, f: F) -> crate::Result<()> { + self.app_handle().run_on_main_thread(f) + } + + /// Gets a handle to the application instance. + pub fn handle(&self) -> &AppHandle { + &self.handle + } + + /// Whether the application supports multiple windows. + #[cfg(target_os = "ios")] + pub fn supports_multiple_windows(&self) -> bool { + let mtm = objc2::MainThreadMarker::new().unwrap(); + let ui_application = objc2_ui_kit::UIApplication::sharedApplication(mtm); + ui_application.supportsMultipleScenes() + } + + /// Sets the activation policy for the application. It is set to `NSApplicationActivationPolicyRegular` by default. + /// + /// # Examples + /// ```,no_run + /// tauri::Builder::default() + /// .setup(move |app| { + /// #[cfg(target_os = "macos")] + /// app.set_activation_policy(tauri::ActivationPolicy::Accessory); + /// Ok(()) + /// }); + /// ``` + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + pub fn set_activation_policy(&mut self, activation_policy: ActivationPolicy) { + if let Some(runtime) = self.runtime.as_mut() { + runtime.set_activation_policy(activation_policy); + } else { + let _ = self.app_handle().set_activation_policy(activation_policy); + } + } + + /// Sets the dock visibility for the application. + /// + /// # Examples + /// ```,no_run + /// tauri::Builder::default() + /// .setup(move |app| { + /// #[cfg(target_os = "macos")] + /// app.set_dock_visibility(false); + /// Ok(()) + /// }); + /// ``` + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + pub fn set_dock_visibility(&mut self, visible: bool) { + if let Some(runtime) = self.runtime.as_mut() { + runtime.set_dock_visibility(visible); + } else { + let _ = self.app_handle().set_dock_visibility(visible); + } + } + + /// Change the device event filter mode. + /// + /// Since the DeviceEvent capture can lead to high CPU usage for unfocused windows, [`tao`] + /// will ignore them by default for unfocused windows on Windows. This method allows changing + /// the filter to explicitly capture them again. + /// + /// ## Platform-specific + /// + /// - ** Linux / macOS / iOS / Android**: Unsupported. + /// + /// # Examples + /// ```,no_run + /// let mut app = tauri::Builder::default() + /// // on an actual app, remove the string argument + /// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) + /// .expect("error while building tauri application"); + /// app.set_device_event_filter(tauri::DeviceEventFilter::Always); + /// app.run(|_app_handle, _event| {}); + /// ``` + /// + /// [`tao`]: https://crates.io/crates/tao + pub fn set_device_event_filter(&mut self, filter: DeviceEventFilter) { + self + .runtime + .as_mut() + .unwrap() + .set_device_event_filter(filter); + } + + /// Runs the application. + /// + /// This function never returns. When the application finishes, the process is exited directly using [`std::process::exit`]. + /// See [`run_return`](Self::run_return) if you need to run code after the application event loop exits. + /// + /// # Panics + /// + /// This function will panic if the setup-function supplied in [`Builder::setup`] fails. + /// + /// # Examples + /// ```,no_run + /// let app = tauri::Builder::default() + /// // on an actual app, remove the string argument + /// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) + /// .expect("error while building tauri application"); + /// app.run(|_app_handle, event| match event { + /// tauri::RunEvent::ExitRequested { api, .. } => { + /// api.prevent_exit(); + /// } + /// _ => {} + /// }); + /// ``` + pub fn run, RunEvent) + 'static>(mut self, callback: F) { + self.handle.event_loop.lock().unwrap().main_thread_id = std::thread::current().id(); + + self + .runtime + .take() + .unwrap() + .run(self.make_run_event_loop_callback(callback)); + } + + /// Runs the application, returning its intended exit code. + /// + /// Note when using [`AppHandle::restart`] and [`AppHandle::request_restart`], + /// this function will handle the restart request, exit and restart the app without returning + /// + /// ## Platform-specific + /// + /// - **iOS**: Unsupported. The application will fallback to [`run`](Self::run). + /// + /// # Panics + /// + /// This function will panic if the setup-function supplied in [`Builder::setup`] fails. + /// + /// # Examples + /// ```,no_run + /// let app = tauri::Builder::default() + /// // on an actual app, remove the string argument + /// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) + /// .expect("error while building tauri application"); + /// let exit_code = app + /// .run_return(|_app_handle, event| match event { + /// tauri::RunEvent::ExitRequested { api, .. } => { + /// api.prevent_exit(); + /// } + /// _ => {} + /// }); + /// + /// std::process::exit(exit_code); + /// ``` + pub fn run_return, RunEvent) + 'static>(mut self, callback: F) -> i32 { + self.handle.event_loop.lock().unwrap().main_thread_id = std::thread::current().id(); + + self + .runtime + .take() + .unwrap() + .run_return(self.make_run_event_loop_callback(callback)) + } + + fn make_run_event_loop_callback, RunEvent) + 'static>( + mut self, + mut callback: F, + ) -> impl FnMut(RuntimeRunEvent) { + // whether the activity is being finished to fulfill an exit request the app accepted + #[cfg(target_os = "android")] + let mut finishing_activity = false; + + move |event| match event { + RuntimeRunEvent::Ready => { + if let Err(e) = setup(&mut self) { + panic!("Failed to setup app: {e}"); + } + let event = on_event_loop_event(self.handle(), RuntimeRunEvent::Ready, self.manager()); + callback(self.handle(), event); + } + RuntimeRunEvent::Exit => { + let event = on_event_loop_event(self.handle(), RuntimeRunEvent::Exit, self.manager()); + callback(self.handle(), event); + self.cleanup_before_exit(); + if self.manager.restart_on_exit.load(atomic::Ordering::Relaxed) { + crate::process::restart(&self.env()); + } + } + // On Android, exiting the process while the activity is still on screen skips the close transition + // and flashes a blank screen, so a programmatic exit finishes the activity instead and the event loop + // exits through the regular window destroyed path. + #[cfg(target_os = "android")] + RuntimeRunEvent::ExitRequested { + code: Some(code), + tx, + } if code != RESTART_EXIT_CODE && !self.manager.windows().is_empty() => { + let (app_tx, app_rx) = std::sync::mpsc::channel(); + let event = on_event_loop_event( + self.handle(), + RuntimeRunEvent::ExitRequested { + code: Some(code), + tx: app_tx, + }, + self.manager(), + ); + callback(self.handle(), event); + + if matches!(app_rx.try_recv(), Ok(ExitRequestedEventAction::Prevent)) { + let _ = tx.send(ExitRequestedEventAction::Prevent); + } else { + match self.handle().finish_activity() { + Ok(()) => { + finishing_activity = true; + // keep the event loop running until the activity is destroyed + let _ = tx.send(ExitRequestedEventAction::Prevent); + } + // fall back to exiting the process directly + Err(e) => log::error!("failed to finish the activity: {e}"), + } + } + } + #[cfg(target_os = "android")] + RuntimeRunEvent::ExitRequested { code: None, .. } if finishing_activity => { + // the app already accepted this exit when it was requested + } + _ => { + let event = on_event_loop_event(self.handle(), event, self.manager()); + callback(self.handle(), event); + } + } + } + + /// Runs an iteration of the runtime event loop and immediately return. + /// + /// Note that when using this API, app cleanup is not automatically done. + /// The cleanup calls [`App::cleanup_before_exit`] so you may want to call that function before exiting the application. + /// + /// # Examples + /// ```no_run + /// use tauri::Manager; + /// + /// let mut app = tauri::Builder::default() + /// // on an actual app, remove the string argument + /// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) + /// .expect("error while building tauri application"); + /// + /// loop { + /// app.run_iteration(|_app, _event| {}); + /// if app.webview_windows().is_empty() { + /// app.cleanup_before_exit(); + /// break; + /// } + /// } + /// ``` + #[cfg(desktop)] + #[deprecated( + note = "When called in a loop (as suggested by the name), this function will busy-loop. To re-gain control of control flow after the app has exited, use `App::run_return` instead." + )] + pub fn run_iteration, RunEvent) + 'static>(&mut self, mut callback: F) { + if !self.ran_setup { + if let Err(e) = setup(self) { + panic!("Failed to setup app: {e}"); + } + } + + let app_handle = self.handle().clone(); + + app_handle.event_loop.lock().unwrap().main_thread_id = std::thread::current().id(); + + self.runtime.as_mut().unwrap().run_iteration(move |event| { + let event = on_event_loop_event(&app_handle, event, app_handle.manager()); + callback(&app_handle, event); + }) + } +} + +/// Builds a Tauri application. +/// +/// # Examples +/// ```,no_run +/// tauri::Builder::default() +/// // on an actual app, remove the string argument +/// .run(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) +/// .expect("error while running tauri application"); +/// ``` +#[allow(clippy::type_complexity)] +pub struct Builder { + /// A flag indicating that the runtime must be started on an environment that supports the event loop not on the main thread. + #[cfg(any(windows, target_os = "linux"))] + runtime_any_thread: bool, + + /// The JS message handler. + invoke_handler: Box>, + + /// The script that initializes the `window.__TAURI_INTERNALS__.postMessage` function. + pub(crate) invoke_initialization_script: String, + + channel_interceptor: Option>, + + /// The setup hook. + setup: SetupHook, + + /// Page load hook. + on_page_load: Option>>, + + /// Permission request hook. + on_permission_request: Option>>, + + /// Web content process termination hook. + #[cfg(any(target_os = "macos", target_os = "ios"))] + on_web_content_process_terminate: Option>>, + + /// All passed plugins + plugins: PluginStore, + + /// The webview protocols available to all windows. + uri_scheme_protocols: HashMap>>, + + /// App state. + state: StateManager, + + /// A closure that returns the menu set to all windows. + #[cfg(desktop)] + menu: Option) -> crate::Result> + Send>>, + + /// Menu event listeners for any menu event. + #[cfg(desktop)] + menu_event_listeners: Vec>>, + + /// Tray event listeners for any tray icon event. + #[cfg(all(desktop, feature = "tray-icon"))] + tray_icon_event_listeners: Vec>>, + + /// Enable macOS default menu creation. + #[allow(unused)] + enable_macos_default_menu: bool, + + /// Whether to activate the application when launched while another application is active. + #[cfg(target_os = "macos")] + activate_ignoring_other_apps: bool, + + /// Window event handlers that listens to all windows. + window_event_listeners: Vec>, + + /// Webview event handlers that listens to all webviews. + webview_event_listeners: Vec>, + + /// The device event filter. + device_event_filter: DeviceEventFilter, + + pub(crate) invoke_key: String, +} + +#[derive(Template)] +#[default_template("../scripts/ipc-protocol.js")] +pub(crate) struct InvokeInitializationScript<'a> { + /// The function that processes the IPC message. + #[raw] + pub(crate) process_ipc_message_fn: &'a str, + pub(crate) os_name: &'a str, + pub(crate) fetch_channel_data_command: &'a str, + pub(crate) invoke_key: &'a str, +} + +/// Make `Wry` the default `Runtime` for `Builder` +#[cfg(feature = "wry")] +#[cfg_attr(docsrs, doc(cfg(feature = "wry")))] +impl Default for Builder { + fn default() -> Self { + Self::new() + } +} + +#[cfg(not(feature = "wry"))] +#[cfg_attr(docsrs, doc(cfg(not(feature = "wry"))))] +impl Default for Builder { + fn default() -> Self { + Self::new() + } +} + +impl Builder { + /// Creates a new App builder. + pub fn new() -> Self { + let invoke_key = crate::generate_invoke_key().unwrap(); + + Self { + #[cfg(any(windows, target_os = "linux"))] + runtime_any_thread: false, + setup: Box::new(|_| Ok(())), + invoke_handler: Box::new(|_| false), + invoke_initialization_script: InvokeInitializationScript { + process_ipc_message_fn: crate::manager::webview::PROCESS_IPC_MESSAGE_FN, + os_name: std::env::consts::OS, + fetch_channel_data_command: crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND, + invoke_key: &invoke_key.clone(), + } + .render_default(&Default::default()) + .unwrap() + .into_string(), + channel_interceptor: None, + on_page_load: None, + on_permission_request: None, + #[cfg(any(target_os = "macos", target_os = "ios"))] + on_web_content_process_terminate: None, + plugins: PluginStore::default(), + uri_scheme_protocols: Default::default(), + state: StateManager::new(), + #[cfg(desktop)] + menu: None, + #[cfg(desktop)] + menu_event_listeners: Vec::new(), + #[cfg(all(desktop, feature = "tray-icon"))] + tray_icon_event_listeners: Vec::new(), + enable_macos_default_menu: true, + #[cfg(target_os = "macos")] + activate_ignoring_other_apps: true, + window_event_listeners: Vec::new(), + webview_event_listeners: Vec::new(), + device_event_filter: Default::default(), + invoke_key, + } + } +} + +impl Builder { + /// Builds a new Tauri application running on any thread, bypassing the main thread requirement. + /// + /// ## Platform-specific + /// + /// - **macOS:** on macOS the application *must* be executed on the main thread, so this function is not exposed. + #[cfg(any(windows, target_os = "linux"))] + #[cfg_attr(docsrs, doc(cfg(any(windows, target_os = "linux"))))] + #[must_use] + pub fn any_thread(mut self) -> Self { + self.runtime_any_thread = true; + self + } + + /// Defines the JS message handler callback. + /// + /// # Examples + /// ``` + /// #[tauri::command] + /// fn command_1() -> String { + /// return "hello world".to_string(); + /// } + /// tauri::Builder::default() + /// .invoke_handler(tauri::generate_handler![ + /// command_1, + /// // etc... + /// ]); + /// ``` + #[must_use] + pub fn invoke_handler(mut self, invoke_handler: F) -> Self + where + F: Fn(Invoke) -> bool + Send + Sync + 'static, + { + self.invoke_handler = Box::new(invoke_handler); + self + } + + /// Defines a custom JS message system. + /// + /// The `initialization_script` is a script that initializes `window.__TAURI_INTERNALS__.postMessage`. + /// That function must take the `(message: object, options: object)` arguments and send it to the backend. + /// + /// Additionally, the script must include a `__INVOKE_KEY__` token that is replaced with a value that must be sent with the IPC payload + /// to check the integrity of the message by the [`crate::WebviewWindow::on_message`] API, e.g. + /// + /// ```js + /// const invokeKey = __INVOKE_KEY__; + /// fetch('my-impl://command', { + /// headers: { + /// 'Tauri-Invoke-Key': invokeKey, + /// } + /// }) + /// ``` + /// + /// Note that the implementation details is up to your implementation. + #[must_use] + pub fn invoke_system(mut self, initialization_script: impl AsRef) -> Self { + self.invoke_initialization_script = initialization_script + .as_ref() + .replace("__INVOKE_KEY__", &format!("\"{}\"", self.invoke_key)); + self + } + + /// Registers a channel interceptor that can overwrite the default channel implementation. + /// + /// If the event has been consumed, it must return `true`. + /// + /// The channel automatically orders the messages, so the third closure argument represents the message number. + /// The payload expected by the channel receiver is in the form of `{ id: usize, message: T }`. + pub fn channel_interceptor< + F: Fn(&Webview, CallbackFn, usize, &InvokeResponseBody) -> bool + Send + Sync + 'static, + >( + mut self, + interceptor: F, + ) -> Self { + self.channel_interceptor.replace(Box::new(interceptor)); + self + } + + /// Append a custom initialization script. + /// + /// Allow to append custom initialization script instead of replacing entire invoke system. + /// + /// # Examples + /// + /// ``` + /// let custom_script = r#" + /// // A custom call system bridge build on top of tauri invoke system. + /// async function invoke(cmd, args = {}) { + /// if (!args) args = {}; + /// + /// let prefix = ""; + /// + /// if (args?.__module) { + /// prefix = `plugin:hybridcall.${args.__module}|`; + /// } + /// + /// const command = `${prefix}tauri_${cmd}`; + /// + /// const invoke = window.__TAURI_INTERNALS__.invoke; + /// + /// return invoke(command, args).then(result => { + /// if (window.build.debug) { + /// console.log(`call: ${command}`); + /// console.log(`args: ${JSON.stringify(args)}`); + /// console.log(`return: ${JSON.stringify(result)}`); + /// } + /// + /// return result; + /// }); + /// } + /// "#; + /// + /// tauri::Builder::default() + /// .append_invoke_initialization_script(custom_script); + /// ``` + pub fn append_invoke_initialization_script( + mut self, + initialization_script: impl AsRef, + ) -> Self { + self + .invoke_initialization_script + .push_str(initialization_script.as_ref()); + self + } + + /// Defines the setup hook. + /// + /// # Examples + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +use tauri::Manager; +tauri::Builder::default() + .setup(|app| { + let main_window = app.get_webview_window("main").unwrap(); + main_window.set_title("Tauri!")?; + Ok(()) + }); +``` + "#### + )] + #[must_use] + pub fn setup(mut self, setup: F) -> Self + where + F: FnOnce(&mut App) -> std::result::Result<(), Box> + Send + 'static, + { + self.setup = Box::new(setup); + self + } + + /// Defines the page load hook. + #[must_use] + pub fn on_page_load(mut self, on_page_load: F) -> Self + where + F: Fn(&Webview, &PageLoadPayload<'_>) + Send + Sync + 'static, + { + self.on_page_load.replace(Arc::new(on_page_load)); + self + } + + /// Defines a closure to be executed when a permission is requested. + /// + /// The handler receives the [`crate::webview::PermissionKind`] and should return + /// the desired [`crate::webview::PermissionResponse`]. + /// + /// This is not called if a [`crate::webview::WebviewBuilder::on_permission_request`] + /// is set on the webview and returned a response other than [`crate::webview::PermissionResponse::Default`]. + /// + /// > [!NOTE] + /// > This handler only triggers for new permission requests. If the user has already + /// > allowed or denied a permission persistently within the webview, the browser + /// > will use the saved preference instead of calling this handler. + /// + /// ## Platform-specific: + /// + /// - **Windows**: Fully supported via WebView2's PermissionRequested event. + /// - **macOS / iOS**: Fully supported via WKUIDelegate's requestMediaCapturePermission. + /// - **Linux**: Fully supported via WebKitGTK's permission-request signal. + /// - **Android**: Supported via JNI bridge for geolocation, microphone, camera, + /// protected media, and MIDI requests. Android runtime permissions may still + /// trigger native OS prompts before access is granted. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri::webview::{PermissionKind, PermissionResponse}; + /// tauri::Builder::default() + /// .on_permission_request(|_, kind| match kind { + /// PermissionKind::Geolocation => PermissionResponse::Allow, + /// PermissionKind::Notifications => PermissionResponse::Allow, + /// _ => PermissionResponse::Default, + /// }); + /// ``` + #[must_use] + pub fn on_permission_request(mut self, on_permission_request: F) -> Self + where + F: Fn(Webview, crate::webview::PermissionKind) -> crate::webview::PermissionResponse + + Send + + Sync + + 'static, + { + self + .on_permission_request + .replace(Arc::new(on_permission_request)); + self + } + + /// Defines the web content process termination hook. + /// + /// ## Platform-specific + /// + /// - **Linux / Windows / Android:** Unsupported. + #[cfg(any(target_os = "macos", target_os = "ios"))] + #[must_use] + pub fn on_web_content_process_terminate(mut self, on_web_content_process_terminate: F) -> Self + where + F: Fn(&Webview) + Send + Sync + 'static, + { + self + .on_web_content_process_terminate + .replace(Arc::new(on_web_content_process_terminate)); + self + } + + /// Adds a Tauri application plugin. + /// + /// A plugin is created using the [`crate::plugin::Builder`] struct.Check its documentation for more information. + /// + /// # Examples + /// + /// ``` + /// mod plugin { + /// use tauri::{plugin::{Builder as PluginBuilder, TauriPlugin}, RunEvent, Runtime}; + /// + /// // this command can be called in the frontend using `invoke('plugin:window|do_something')`. + /// #[tauri::command] + /// async fn do_something(app: tauri::AppHandle, window: tauri::Window) -> Result<(), String> { + /// println!("command called"); + /// Ok(()) + /// } + /// pub fn init() -> TauriPlugin { + /// PluginBuilder::new("window") + /// .setup(|app, api| { + /// // initialize the plugin here + /// Ok(()) + /// }) + /// .on_event(|app, event| { + /// match event { + /// RunEvent::Ready => { + /// println!("app is ready"); + /// } + /// RunEvent::WindowEvent { label, event, .. } => { + /// println!("window {} received an event: {:?}", label, event); + /// } + /// _ => (), + /// } + /// }) + /// .invoke_handler(tauri::generate_handler![do_something]) + /// .build() + /// } + /// } + /// + /// tauri::Builder::default() + /// .plugin(plugin::init()); + /// ``` + #[must_use] + pub fn plugin + 'static>(self, plugin: P) -> Self { + self.plugin_boxed(Box::new(plugin)) + } + + /// Adds a Tauri application plugin. + /// + /// This method is similar to [`Self::plugin`], + /// but accepts a boxed trait object instead of a generic type. + #[must_use] + pub fn plugin_boxed(mut self, plugin: Box>) -> Self { + self.plugins.register(plugin); + self + } + + /// Add `state` to the state managed by the application. + /// + /// This method can be called any number of times as long as each call + /// refers to a different `T`. + /// + /// Managed state can be retrieved by any command handler via the + /// [`crate::State`] guard. In particular, if a value of type `T` + /// is managed by Tauri, adding `State` to the list of arguments in a + /// command handler instructs Tauri to retrieve the managed value. + /// Additionally, [`state`](crate::Manager#method.state) can be used to retrieve the value manually. + /// + /// # Panics + /// + /// Panics if state of type `T` is already being managed. + /// + /// # Mutability + /// + /// Since the managed state is global and must be [`Send`] + [`Sync`], mutations can only happen through interior mutability: + /// + /// ```,no_run + /// use std::{collections::HashMap, sync::Mutex}; + /// use tauri::State; + /// // here we use Mutex to achieve interior mutability + /// struct Storage { + /// store: Mutex>, + /// } + /// struct Connection; + /// struct DbConnection { + /// db: Mutex>, + /// } + /// + /// #[tauri::command] + /// fn connect(connection: State) { + /// // initialize the connection, mutating the state with interior mutability + /// *connection.db.lock().unwrap() = Some(Connection {}); + /// } + /// + /// #[tauri::command] + /// fn storage_insert(key: u64, value: String, storage: State) { + /// // mutate the storage behind the Mutex + /// storage.store.lock().unwrap().insert(key, value); + /// } + /// + /// tauri::Builder::default() + /// .manage(Storage { store: Default::default() }) + /// .manage(DbConnection { db: Default::default() }) + /// .invoke_handler(tauri::generate_handler![connect, storage_insert]) + /// // on an actual app, remove the string argument + /// .run(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) + /// .expect("error while running tauri application"); + /// ``` + /// + /// # Examples + /// + /// ```,no_run + /// use tauri::State; + /// + /// struct MyInt(isize); + /// struct MyString(String); + /// + /// #[tauri::command] + /// fn int_command(state: State) -> String { + /// format!("The stateful int is: {}", state.0) + /// } + /// + /// #[tauri::command] + /// fn string_command<'r>(state: State<'r, MyString>) { + /// println!("state: {}", state.inner().0); + /// } + /// + /// tauri::Builder::default() + /// .manage(MyInt(10)) + /// .manage(MyString("Hello, managed state!".to_string())) + /// .invoke_handler(tauri::generate_handler![int_command, string_command]) + /// // on an actual app, remove the string argument + /// .run(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) + /// .expect("error while running tauri application"); + /// ``` + #[must_use] + pub fn manage(self, state: T) -> Self + where + T: Send + Sync + 'static, + { + let type_name = std::any::type_name::(); + assert!( + self.state.set(state), + "state for type '{type_name}' is already being managed", + ); + self + } + + /// Sets the menu to use on all windows. + /// + /// # Examples + /// ``` + /// use tauri::menu::{Menu, MenuItem, PredefinedMenuItem, Submenu}; + /// + /// tauri::Builder::default() + /// .menu(|handle| Menu::with_items(handle, &[ + /// &Submenu::with_items( + /// handle, + /// "File", + /// true, + /// &[ + /// &PredefinedMenuItem::close_window(handle, None)?, + /// #[cfg(target_os = "macos")] + /// &MenuItem::new(handle, "Hello", true, None::<&str>)?, + /// ], + /// )? + /// ])); + /// ``` + #[must_use] + #[cfg(desktop)] + pub fn menu) -> crate::Result> + Send + 'static>( + mut self, + f: F, + ) -> Self { + self.menu.replace(Box::new(f)); + self + } + + /// Registers an event handler for any menu event. + /// + /// # Examples + /// ``` + /// use tauri::menu::*; + /// + /// tauri::Builder::default() + /// .on_menu_event(|app, event| { + /// if event.id() == "quit" { + /// app.exit(0); + /// } + /// }); + /// ``` + #[must_use] + #[cfg(desktop)] + pub fn on_menu_event, MenuEvent) + Send + Sync + 'static>( + mut self, + f: F, + ) -> Self { + self.menu_event_listeners.push(Box::new(f)); + self + } + + /// Registers an event handler for any tray icon event. + /// + /// # Examples + /// ``` + /// use tauri::Manager; + /// + /// tauri::Builder::default() + /// .on_tray_icon_event(|app, event| { + /// let tray = app.tray_by_id(event.id()).expect("can't find tray icon"); + /// let _ = tray.set_visible(false); + /// }); + /// ``` + #[must_use] + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "tray-icon"))))] + pub fn on_tray_icon_event, TrayIconEvent) + Send + Sync + 'static>( + mut self, + f: F, + ) -> Self { + self.tray_icon_event_listeners.push(Box::new(f)); + self + } + + /// Enable or disable the default menu on macOS. Enabled by default. + /// + /// # Examples + /// ``` + /// tauri::Builder::default() + /// .enable_macos_default_menu(false); + /// ``` + #[must_use] + pub fn enable_macos_default_menu(mut self, enable: bool) -> Self { + self.enable_macos_default_menu = enable; + self + } + + /// Configures whether the application should activate when launched while another application + /// is active. + /// + /// By default, the application ignores other applications and activates when launched. + /// + /// If `false`, the application activates only if no other application is currently active. + /// If `true`, the application activates regardless. + /// + /// This only affects the initial activation at launch. Explicitly focusing a window later + /// (e.g. via [`Window::set_focus`](crate::window::Window::set_focus)) still activates the + /// application regardless of this setting. + /// + /// # Examples + /// ```,no_run + /// tauri::Builder::default() + /// .activate_ignoring_other_apps(false); + /// ``` + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + #[must_use] + pub fn activate_ignoring_other_apps(mut self, ignore: bool) -> Self { + self.activate_ignoring_other_apps = ignore; + self + } + + /// Registers a window event handler for all windows. + /// + /// # Examples + /// ``` + /// tauri::Builder::default() + /// .on_window_event(|window, event| match event { + /// tauri::WindowEvent::Focused(focused) => { + /// // hide window whenever it loses focus + /// if !focused { + /// window.hide().unwrap(); + /// } + /// } + /// _ => {} + /// }); + /// ``` + #[must_use] + pub fn on_window_event, &WindowEvent) + Send + Sync + 'static>( + mut self, + handler: F, + ) -> Self { + self.window_event_listeners.push(Box::new(handler)); + self + } + + /// Registers a webview event handler for all webviews. + /// + /// # Examples + /// ``` + /// tauri::Builder::default() + /// .on_webview_event(|window, event| match event { + /// tauri::WebviewEvent::DragDrop(event) => { + /// println!("{:?}", event); + /// } + /// _ => {} + /// }); + /// ``` + #[must_use] + pub fn on_webview_event, &WebviewEvent) + Send + Sync + 'static>( + mut self, + handler: F, + ) -> Self { + self.webview_event_listeners.push(Box::new(handler)); + self + } + + /// Registers a URI scheme protocol available to all webviews. + /// + /// Leverages [setURLSchemeHandler](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/2875766-seturlschemehandler) on macOS, + /// [AddWebResourceRequestedFilter](https://docs.microsoft.com/en-us/dotnet/api/microsoft.web.webview2.core.corewebview2.addwebresourcerequestedfilter?view=webview2-dotnet-1.0.774.44) on Windows + /// and [webkit-web-context-register-uri-scheme](https://webkitgtk.org/reference/webkit2gtk/stable/WebKitWebContext.html#webkit-web-context-register-uri-scheme) on Linux. + /// + /// # Arguments + /// + /// * `uri_scheme` The URI scheme to register, such as `example`. + /// * `protocol` the protocol associated with the given URI scheme. It's a function that takes a request and returns a response. + /// + /// # Examples + /// ``` + /// tauri::Builder::default() + /// .register_uri_scheme_protocol("app-files", |_ctx, request| { + /// // skip leading `/` + /// if let Ok(data) = std::fs::read(&request.uri().path()[1..]) { + /// http::Response::builder() + /// .body(data) + /// .unwrap() + /// } else { + /// http::Response::builder() + /// .status(http::StatusCode::BAD_REQUEST) + /// .header(http::header::CONTENT_TYPE, mime::TEXT_PLAIN.essence_str()) + /// .body("failed to read file".as_bytes().to_vec()) + /// .unwrap() + /// } + /// }); + /// ``` + /// + /// # Warning + /// + /// Pages loaded from a custom protocol will have a different Origin on different platforms. + /// Servers which enforce CORS will need to add the exact same Origin header (or `*`) in `Access-Control-Allow-Origin` + /// if you wish to send requests with native `fetch` and `XmlHttpRequest` APIs. Here are the + /// different Origin headers across platforms: + /// + /// - macOS, iOS and Linux: `://localhost/` (so it will be `my-scheme://localhost/path/to/page). + /// - Windows and Android: `http://.localhost/` by default (so it will be `http://my-scheme.localhost/path/to/page`). + /// To use `https` instead of `http`, use [`super::webview::WebviewBuilder::use_https_scheme`]. + #[must_use] + pub fn register_uri_scheme_protocol< + N: Into, + T: Into>, + H: Fn(UriSchemeContext<'_, R>, http::Request>) -> http::Response + + Send + + Sync + + 'static, + >( + mut self, + uri_scheme: N, + protocol_handler: H, + ) -> Self { + self.uri_scheme_protocols.insert( + uri_scheme.into(), + Arc::new(UriSchemeProtocol { + handler: Box::new(move |ctx, request, responder| { + responder.respond(protocol_handler(ctx, request)) + }), + }), + ); + self + } + + /// Similar to [`Self::register_uri_scheme_protocol`] but with an asynchronous responder that allows you + /// to process the request in a separate thread and respond asynchronously. + /// + /// # Arguments + /// + /// * `uri_scheme` The URI scheme to register, such as `example`. + /// * `protocol` the protocol associated with the given URI scheme. It's a function that takes an URL such as `example://localhost/asset.css`. + /// + /// # Examples + /// ``` + /// tauri::Builder::default() + /// .register_asynchronous_uri_scheme_protocol("app-files", |_ctx, request, responder| { + /// // skip leading `/` + /// let path = request.uri().path()[1..].to_string(); + /// std::thread::spawn(move || { + /// if let Ok(data) = std::fs::read(path) { + /// responder.respond( + /// http::Response::builder() + /// .body(data) + /// .unwrap() + /// ); + /// } else { + /// responder.respond( + /// http::Response::builder() + /// .status(http::StatusCode::BAD_REQUEST) + /// .header(http::header::CONTENT_TYPE, mime::TEXT_PLAIN.essence_str()) + /// .body("failed to read file".as_bytes().to_vec()) + /// .unwrap() + /// ); + /// } + /// }); + /// }); + /// ``` + /// + /// # Warning + /// + /// Pages loaded from a custom protocol will have a different Origin on different platforms. + /// Servers which enforce CORS will need to add the exact same Origin header (or `*`) in `Access-Control-Allow-Origin` + /// if you wish to send requests with native `fetch` and `XmlHttpRequest` APIs. Here are the + /// different Origin headers across platforms: + /// + /// - macOS, iOS and Linux: `://localhost/` (so it will be `my-scheme://localhost/path/to/page). + /// - Windows and Android: `http://.localhost/` by default (so it will be `http://my-scheme.localhost/path/to/page`). + /// To use `https` instead of `http`, use [`super::webview::WebviewBuilder::use_https_scheme`]. + #[must_use] + pub fn register_asynchronous_uri_scheme_protocol< + N: Into, + H: Fn(UriSchemeContext<'_, R>, http::Request>, UriSchemeResponder) + Send + Sync + 'static, + >( + mut self, + uri_scheme: N, + protocol_handler: H, + ) -> Self { + self.uri_scheme_protocols.insert( + uri_scheme.into(), + Arc::new(UriSchemeProtocol { + handler: Box::new(protocol_handler), + }), + ); + self + } + + /// Change the device event filter mode. + /// + /// Since the DeviceEvent capture can lead to high CPU usage for unfocused windows, [`tao`] + /// will ignore them by default for unfocused windows on Windows. This method allows changing + /// the filter to explicitly capture them again. + /// + /// ## Platform-specific + /// + /// - ** Linux / macOS / iOS / Android**: Unsupported. + /// + /// # Examples + /// ```,no_run + /// tauri::Builder::default() + /// .device_event_filter(tauri::DeviceEventFilter::Always); + /// ``` + /// + /// [`tao`]: https://crates.io/crates/tao + pub fn device_event_filter(mut self, filter: DeviceEventFilter) -> Self { + self.device_event_filter = filter; + self + } + + /// Builds the application. + /// + /// This creates the underlying webview runtime (the event loop), registers the core plugins and + /// initializes everything defined on the builder, but it does not start the application: + /// call [`App::run`] or [`App::run_return`] on the returned value to do so, or use [`Self::run`] + /// to do both at once. + /// + /// # Errors + /// + /// Returns an error if the application cannot be initialized, in particular when: + /// + /// - the webview runtime (event loop) cannot be created, for instance when it is created outside + /// the main thread, when the display server cannot be reached or when the WebView2 runtime is + /// missing on Windows; + /// - the `app > security > assetProtocol > scope` patterns in the configuration are invalid; + /// - the app menu defined with [`Self::menu`] cannot be created; + /// - the tray icon defined in the configuration cannot be created; + /// - a plugin fails to initialize, i.e. its [`Plugin::initialize`](crate::plugin::Plugin::initialize) + /// hook returns an error, which is also the case when a plugin is registered twice. + /// + /// # Examples + /// + /// ```rust,no_run + /// tauri::Builder::default() + /// // on an actual app, remove the string argument + /// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) + /// .expect("error while building tauri application") + /// .run(|_app_handle, _event| {}); + /// ``` + #[allow(clippy::type_complexity, unused_mut)] + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "app::build", skip_all) + )] + pub fn build(mut self, context: Context) -> crate::Result> { + #[cfg(target_os = "macos")] + if self.menu.is_none() && self.enable_macos_default_menu { + self.menu = Some(Box::new(|app_handle| { + crate::menu::Menu::default(app_handle) + })); + } + + let manager = Arc::new(AppManager::with_handlers( + context, + self.plugins, + self.invoke_handler, + self.on_page_load, + self.on_permission_request, + #[cfg(any(target_os = "macos", target_os = "ios"))] + self.on_web_content_process_terminate, + self.uri_scheme_protocols, + self.state, + #[cfg(desktop)] + self.menu_event_listeners, + #[cfg(all(desktop, feature = "tray-icon"))] + self.tray_icon_event_listeners, + self.window_event_listeners, + self.webview_event_listeners, + #[cfg(desktop)] + HashMap::new(), + self.invoke_initialization_script, + self.channel_interceptor, + self.invoke_key, + )); + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + let app_id = if manager.config.app.enable_gtk_app_id { + Some(manager.config.identifier.clone()) + } else { + None + }; + + let runtime_args = RuntimeInitArgs { + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + app_id, + + #[cfg(windows)] + msg_hook: { + let menus = manager.menu.menus.clone(); + Some(Box::new(move |msg| { + use windows::Win32::UI::WindowsAndMessaging::{HACCEL, MSG, TranslateAcceleratorW}; + unsafe { + let msg = msg as *const MSG; + for menu in menus.lock().unwrap().values() { + let translated = + TranslateAcceleratorW((*msg).hwnd, HACCEL(menu.inner().haccel() as _), msg); + if translated == 1 { + return true; + } + } + + false + } + })) + }, + }; + + // The env var must be set before the Runtime is created so that GetAvailableBrowserVersionString picks it up. + #[cfg(windows)] + { + if let crate::utils::config::WebviewInstallMode::FixedRuntime { path } = + &manager.config.bundle.windows.webview_install_mode + { + if let Some(exe_dir) = crate::utils::platform::current_exe() + .ok() + .and_then(|p| p.parent().map(|p| p.to_path_buf())) + { + // SAFETY: Always safe on Windows + unsafe { std::env::set_var("WEBVIEW2_BROWSER_EXECUTABLE_FOLDER", exe_dir.join(path)) }; + } else { + #[cfg(debug_assertions)] + eprintln!( + "failed to resolve resource directory; fallback to the installed Webview2 runtime." + ); + } + } + } + + #[cfg(any(windows, target_os = "linux"))] + let mut runtime = if self.runtime_any_thread { + R::new_any_thread(runtime_args)? + } else { + R::new(runtime_args)? + }; + #[cfg(not(any(windows, target_os = "linux")))] + let mut runtime = R::new(runtime_args)?; + + #[cfg(target_os = "macos")] + runtime.set_activate_ignoring_other_apps(self.activate_ignoring_other_apps); + + #[cfg(desktop)] + { + // setup menu event handler + let proxy = runtime.create_proxy(); + muda::MenuEvent::set_event_handler(Some(move |e: muda::MenuEvent| { + let _ = proxy.send_event(EventLoopMessage::MenuEvent(e.into())); + })); + + // setup tray event handler + #[cfg(feature = "tray-icon")] + { + let proxy = runtime.create_proxy(); + tray_icon::TrayIconEvent::set_event_handler(Some(move |e: tray_icon::TrayIconEvent| { + let _ = proxy.send_event(EventLoopMessage::TrayIconEvent(e.into())); + })); + } + } + + runtime.set_device_event_filter(self.device_event_filter); + + let runtime_handle = runtime.handle(); + + let app = App { + runtime: Some(runtime), + setup: Some(self.setup), + manager: manager.clone(), + handle: AppHandle { + runtime_handle, + manager, + event_loop: Arc::new(Mutex::new(EventLoop { + main_thread_id: std::thread::current().id(), + })), + }, + ran_setup: false, + }; + + app.register_core_plugins()?; + + #[cfg(desktop)] + if let Some(menu) = self.menu { + let menu = menu(&app.handle)?; + app + .manager + .menu + .menus_stash_lock() + .insert(menu.id().clone(), menu.clone()); + + #[cfg(target_os = "macos")] + init_app_menu(&menu)?; + + app.manager.menu.menu_lock().replace(menu); + } + + let env = Env::default(); + app.manage(env); + + app.manage(Scopes { + #[cfg(feature = "protocol-asset")] + asset_protocol: crate::scope::fs::Scope::new( + &app, + &app.config().app.security.asset_protocol.scope, + )?, + }); + + app.manage(ChannelDataIpcQueue::default()); + app.handle.plugin(crate::ipc::channel::plugin())?; + + let handle = app.handle(); + + // initialize default tray icon if defined + #[cfg(all(desktop, feature = "tray-icon"))] + { + let config = app.config(); + if let Some(tray_config) = &config.app.tray_icon { + let mut tray = + TrayIconBuilder::with_id(tray_config.id.clone().unwrap_or_else(|| "main".into())) + .icon_as_template(tray_config.icon_as_template) + .show_menu_on_left_click(tray_config.show_menu_on_left_click); + if let Some(icon) = &app.manager.tray.icon { + tray = tray.icon(icon.clone()); + } + if let Some(title) = &tray_config.title { + tray = tray.title(title); + } + if let Some(tooltip) = &tray_config.tooltip { + tray = tray.tooltip(tooltip); + } + tray.build(handle)?; + } + } + + app.manager.initialize_plugins(handle)?; + + Ok(app) + } + + /// Builds the configured application and runs it. + /// + /// This is a shorthand for [`Self::build`] followed by [`App::run`]. + /// For more flexibility, consider using those functions manually. + pub fn run(self, context: Context) -> crate::Result<()> { + self.build(context)?.run(|_, _| {}); + Ok(()) + } +} + +pub(crate) type UriSchemeResponderFn = Box>) + Send>; + +/// Async uri scheme protocol responder. +pub struct UriSchemeResponder(pub(crate) UriSchemeResponderFn); + +impl UriSchemeResponder { + /// Resolves the request with the given response. + pub fn respond>>(self, response: http::Response) { + let (parts, body) = response.into_parts(); + (self.0)(http::Response::from_parts(parts, body.into())) + } +} + +/// Uri scheme protocol context +pub struct UriSchemeContext<'a, R: Runtime> { + pub(crate) app_handle: &'a AppHandle, + pub(crate) webview_label: &'a str, +} + +impl<'a, R: Runtime> UriSchemeContext<'a, R> { + /// Get a reference to an [`AppHandle`]. + pub fn app_handle(&self) -> &'a AppHandle { + self.app_handle + } + + /// Get the webview label that made the uri scheme request. + pub fn webview_label(&self) -> &'a str { + self.webview_label + } +} + +#[cfg(target_os = "macos")] +fn init_app_menu(menu: &Menu) -> crate::Result<()> { + menu.inner().init_for_nsapp(); + + if let Some(window_menu) = menu.get(crate::menu::WINDOW_SUBMENU_ID) { + if let Some(m) = window_menu.as_submenu() { + m.set_as_windows_menu_for_nsapp()?; + } + } + if let Some(help_menu) = menu.get(crate::menu::HELP_SUBMENU_ID) { + if let Some(m) = help_menu.as_submenu() { + m.set_as_help_menu_for_nsapp()?; + } + } + + Ok(()) +} + +impl HasDisplayHandle for AppHandle { + fn display_handle( + &self, + ) -> std::result::Result, raw_window_handle::HandleError> { + self.runtime_handle.display_handle() + } +} + +impl HasDisplayHandle for App { + fn display_handle( + &self, + ) -> std::result::Result, raw_window_handle::HandleError> { + self.handle.display_handle() + } +} + +#[cfg_attr(feature = "tracing", tracing::instrument(name = "app::setup"))] +fn setup(app: &mut App) -> crate::Result<()> { + app.ran_setup = true; + + for window_config in app.config().app.windows.iter().filter(|w| w.create) { + WebviewWindowBuilder::from_config(app.handle(), window_config)?.build()?; + } + + app.manager.assets.setup(app); + + if let Some(setup) = app.setup.take() { + (setup)(app).map_err(|e| crate::Error::Setup(e.into()))?; + } + + Ok(()) +} + +fn on_event_loop_event( + app_handle: &AppHandle, + event: RuntimeRunEvent, + manager: &AppManager, +) -> RunEvent { + if let RuntimeRunEvent::WindowEvent { + label, + event: RuntimeWindowEvent::Destroyed, + } = &event + { + manager.on_window_close(label); + } + + let event = match event { + RuntimeRunEvent::Exit => RunEvent::Exit, + RuntimeRunEvent::ExitRequested { code, tx } => RunEvent::ExitRequested { + code, + api: ExitRequestApi { tx, code }, + }, + RuntimeRunEvent::WindowEvent { label, event } => RunEvent::WindowEvent { + label, + event: event.into(), + }, + RuntimeRunEvent::WebviewEvent { label, event } => RunEvent::WebviewEvent { + label, + event: event.into(), + }, + RuntimeRunEvent::Ready => { + // set the app icon in development + #[cfg(all(dev, target_os = "macos"))] + { + use objc2::{AllocAnyThread, MainThreadMarker}; + use objc2_app_kit::{NSApplication, NSImage}; + use objc2_foundation::NSData; + + if let Some(icon) = app_handle.manager.app_icon.clone() { + // TODO: Enable this check. + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + let app = NSApplication::sharedApplication(mtm); + let data = NSData::with_bytes(&icon); + let app_icon = NSImage::initWithData(NSImage::alloc(), &data).expect("creating icon"); + unsafe { app.setApplicationIconImage(Some(&app_icon)) }; + } + } + RunEvent::Ready + } + RuntimeRunEvent::Resumed => RunEvent::Resumed, + RuntimeRunEvent::MainEventsCleared => RunEvent::MainEventsCleared, + RuntimeRunEvent::UserEvent(t) => { + match t { + #[cfg(desktop)] + EventLoopMessage::MenuEvent(ref e) => { + for listener in &*app_handle + .manager + .menu + .global_event_listeners + .lock() + .unwrap() + { + listener(app_handle, e.clone()); + } + for (label, listener) in &*app_handle.manager.menu.event_listeners.lock().unwrap() { + if let Some(w) = app_handle.manager().get_window(label) { + listener(&w, e.clone()); + } + } + } + #[cfg(all(desktop, feature = "tray-icon"))] + EventLoopMessage::TrayIconEvent(ref e) => { + for listener in &*app_handle + .manager + .tray + .global_event_listeners + .lock() + .unwrap() + { + listener(app_handle, e.clone()); + } + + for (id, listener) in &*app_handle.manager.tray.event_listeners.lock().unwrap() { + if e.id() == id { + if let Some(tray) = app_handle.tray_by_id(id) { + listener(&tray, e.clone()); + } + } + } + } + } + + #[allow(unreachable_code)] + t.into() + } + #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))] + RuntimeRunEvent::Opened { urls } => RunEvent::Opened { urls }, + #[cfg(target_os = "macos")] + RuntimeRunEvent::Reopen { + has_visible_windows, + } => RunEvent::Reopen { + has_visible_windows, + }, + #[cfg(target_os = "ios")] + RuntimeRunEvent::SceneRequested { scene, options } => { + RunEvent::SceneRequested { scene, options } + } + _ => unimplemented!(), + }; + + manager + .plugins + .lock() + .expect("poisoned plugin store") + .on_event(app_handle, &event); + + event +} + +#[cfg(test)] +mod tests { + #[test] + fn is_send_sync() { + crate::test_utils::assert_send::(); + crate::test_utils::assert_sync::(); + + #[cfg(feature = "wry")] + { + crate::test_utils::assert_send::>(); + crate::test_utils::assert_sync::>(); + } + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/app/plugin.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/app/plugin.rs new file mode 100644 index 000000000..1decde60e --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/app/plugin.rs @@ -0,0 +1,158 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use tauri_utils::{Theme, config::BundleType}; + +use crate::{ + AppHandle, Manager, ResourceId, Runtime, Webview, command, + plugin::{Builder, TauriPlugin}, +}; + +#[command(root = "crate")] +pub fn version(app: AppHandle) -> String { + app.package_info().version.to_string() +} + +#[command(root = "crate")] +pub fn name(app: AppHandle) -> String { + app.package_info().name.clone() +} + +#[command(root = "crate")] +pub fn tauri_version() -> &'static str { + crate::VERSION +} + +#[command(root = "crate")] +pub fn identifier(app: AppHandle) -> String { + app.config().identifier.clone() +} + +#[command(root = "crate")] +#[allow(unused_variables)] +pub fn app_show(app: AppHandle) -> crate::Result<()> { + #[cfg(target_os = "macos")] + app.show()?; + Ok(()) +} + +#[command(root = "crate")] +#[allow(unused_variables)] +pub fn app_hide(app: AppHandle) -> crate::Result<()> { + #[cfg(target_os = "macos")] + app.hide()?; + Ok(()) +} + +#[command(root = "crate")] +pub fn exit(app: AppHandle, code: i32) { + app.exit(code) +} + +#[command(root = "crate")] +#[allow(unused_variables)] +pub async fn fetch_data_store_identifiers( + app: AppHandle, +) -> crate::Result> { + #[cfg(target_vendor = "apple")] + return app.fetch_data_store_identifiers().await; + #[cfg(not(target_vendor = "apple"))] + return Ok(Vec::new()); +} + +#[command(root = "crate")] +#[allow(unused_variables)] +pub async fn remove_data_store(app: AppHandle, uuid: [u8; 16]) -> crate::Result<()> { + #[cfg(target_vendor = "apple")] + app.remove_data_store(uuid).await?; + #[cfg(not(target_vendor = "apple"))] + let _ = uuid; + Ok(()) +} + +#[command(root = "crate")] +pub fn default_window_icon( + webview: Webview, + app: AppHandle, +) -> Option { + app.default_window_icon().cloned().map(|icon| { + let mut resources_table = webview.resources_table(); + resources_table.add(icon.to_owned()) + }) +} + +#[command(root = "crate")] +pub async fn set_app_theme(app: AppHandle, theme: Option) { + app.set_theme(theme); +} + +#[command(root = "crate")] +pub async fn set_dock_visibility( + app: AppHandle, + visible: bool, +) -> crate::Result<()> { + #[cfg(target_os = "macos")] + { + let mut focused_window = None; + for window in app.manager.windows().into_values() { + if window.is_focused().unwrap_or_default() { + focused_window.replace(window); + break; + } + } + + app.set_dock_visibility(visible)?; + + // retain focus + if let Some(focused_window) = focused_window { + let _ = focused_window.set_focus(); + } + } + #[cfg(not(target_os = "macos"))] + let (_app, _visible) = (app, visible); + Ok(()) +} + +#[command(root = "crate")] +pub fn bundle_type() -> Option { + tauri_utils::platform::bundle_type() +} + +#[command(root = "crate")] +pub fn supports_multiple_windows(app: AppHandle) -> bool { + app.supports_multiple_windows() +} + +pub fn init() -> TauriPlugin { + Builder::new("app") + .invoke_handler(crate::generate_handler![ + #![plugin(app)] + version, + name, + tauri_version, + identifier, + app_show, + app_hide, + exit, + fetch_data_store_identifiers, + remove_data_store, + default_window_icon, + set_app_theme, + set_dock_visibility, + bundle_type, + supports_multiple_windows, + ]) + .setup(|_app, _api| { + #[cfg(target_os = "android")] + { + let handle = _api.register_android_plugin("app.tauri", "AppPlugin")?; + _app.manage(AppPlugin(handle)); + } + Ok(()) + }) + .build() +} + +#[cfg(target_os = "android")] +pub(crate) struct AppPlugin(pub crate::plugin::PluginHandle); diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/async_runtime.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/async_runtime.rs new file mode 100644 index 000000000..970d5e33b --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/async_runtime.rs @@ -0,0 +1,363 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! The singleton async runtime used by Tauri and exposed to users. +//! +//! Tauri uses [`tokio`] Runtime to initialize code, such as +//! [`Plugin::initialize`](../plugin/trait.Plugin.html#method.initialize) and [`crate::Builder::setup`] hooks. +//! This module also re-export some common items most developers need from [`tokio`]. If there's +//! one you need isn't here, you could use types in [`tokio`] directly. +//! For custom command handlers, it's recommended to use a plain `async fn` command. + +pub use tokio::{ + runtime::{Handle as TokioHandle, Runtime as TokioRuntime}, + sync::{ + Mutex, RwLock, + mpsc::{Receiver, Sender, channel}, + }, + task::JoinHandle as TokioJoinHandle, +}; + +use std::{ + future::Future, + pin::Pin, + sync::OnceLock, + task::{Context, Poll}, +}; + +static RUNTIME: OnceLock = OnceLock::new(); + +struct GlobalRuntime { + runtime: Option, + handle: RuntimeHandle, +} + +impl GlobalRuntime { + fn handle(&self) -> RuntimeHandle { + if let Some(r) = &self.runtime { + r.handle() + } else { + self.handle.clone() + } + } + + #[track_caller] + fn spawn(&self, task: F) -> JoinHandle + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + if let Some(r) = &self.runtime { + r.spawn(task) + } else { + self.handle.spawn(task) + } + } + + #[track_caller] + /// Runs the provided function on an executor dedicated to blocking operations. + pub fn spawn_blocking(&self, func: F) -> JoinHandle + where + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, + { + if let Some(r) = &self.runtime { + r.spawn_blocking(func) + } else { + self.handle.spawn_blocking(func) + } + } + + #[track_caller] + fn block_on(&self, task: F) -> F::Output { + if let Some(r) = &self.runtime { + r.block_on(task) + } else { + self.handle.block_on(task) + } + } +} + +/// A runtime used to execute asynchronous tasks. +pub enum Runtime { + /// The tokio runtime. + Tokio(TokioRuntime), +} + +impl Runtime { + /// Gets a reference to the [`TokioRuntime`]. + pub fn inner(&self) -> &TokioRuntime { + let Self::Tokio(r) = self; + r + } + + /// Returns a handle of the async runtime. + pub fn handle(&self) -> RuntimeHandle { + match self { + Self::Tokio(r) => RuntimeHandle::Tokio(r.handle().clone()), + } + } + + #[track_caller] + /// Spawns a future onto the runtime. + pub fn spawn(&self, task: F) -> JoinHandle + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + match self { + Self::Tokio(r) => { + let _guard = r.enter(); + JoinHandle::Tokio(tokio::spawn(task)) + } + } + } + + #[track_caller] + /// Runs the provided function on an executor dedicated to blocking operations. + pub fn spawn_blocking(&self, func: F) -> JoinHandle + where + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, + { + match self { + Self::Tokio(r) => JoinHandle::Tokio(r.spawn_blocking(func)), + } + } + + #[track_caller] + /// Runs a future to completion on runtime. + pub fn block_on(&self, task: F) -> F::Output { + match self { + Self::Tokio(r) => r.block_on(task), + } + } +} + +/// An owned permission to join on a task (await its termination). +#[derive(Debug)] +pub enum JoinHandle { + /// The tokio JoinHandle. + Tokio(TokioJoinHandle), +} + +impl JoinHandle { + /// Gets a reference to the [`TokioJoinHandle`]. + pub fn inner(&self) -> &TokioJoinHandle { + let Self::Tokio(t) = self; + t + } + + /// Abort the task associated with the handle. + /// + /// Awaiting a cancelled task might complete as usual if the task was + /// already completed at the time it was cancelled, but most likely it + /// will fail with a cancelled `JoinError`. + pub fn abort(&self) { + match self { + Self::Tokio(t) => t.abort(), + } + } +} + +impl Future for JoinHandle { + type Output = crate::Result; + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match self.get_mut() { + Self::Tokio(t) => Pin::new(t).poll(cx).map_err(Into::into), + } + } +} + +/// A handle to the async runtime +#[derive(Clone)] +pub enum RuntimeHandle { + /// The tokio handle. + Tokio(TokioHandle), +} + +impl RuntimeHandle { + /// Gets a reference to the [`TokioHandle`]. + pub fn inner(&self) -> &TokioHandle { + let Self::Tokio(h) = self; + h + } + + #[track_caller] + /// Runs the provided function on an executor dedicated to blocking operations. + /// + /// Use this instead of [`Self::spawn`] for code that blocks the thread it runs on + /// (synchronous file or network I/O, heavy computation, FFI calls) so the async executor + /// is not blocked. The returned [`JoinHandle`] resolves to the closure's return value. + /// + /// # Examples + /// + /// ```rust + /// use tauri::Manager; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let handle = app.handle().clone(); + /// tauri::async_runtime::handle().spawn_blocking(move || { + /// let contents = std::fs::read_to_string("file.txt").unwrap_or_default(); + /// handle.package_info().name.len() + contents.len() + /// }); + /// Ok(()) + /// }); + /// ``` + pub fn spawn_blocking(&self, func: F) -> JoinHandle + where + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, + { + match self { + Self::Tokio(h) => JoinHandle::Tokio(h.spawn_blocking(func)), + } + } + + #[track_caller] + /// Spawns a future onto the runtime. + pub fn spawn(&self, task: F) -> JoinHandle + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + match self { + Self::Tokio(h) => { + let _guard = h.enter(); + JoinHandle::Tokio(tokio::spawn(task)) + } + } + } + + #[track_caller] + /// Runs a future to completion on runtime. + pub fn block_on(&self, task: F) -> F::Output { + match self { + Self::Tokio(h) => h.block_on(task), + } + } +} + +fn default_runtime() -> GlobalRuntime { + let runtime = Runtime::Tokio(TokioRuntime::new().unwrap()); + let handle = runtime.handle(); + GlobalRuntime { + runtime: Some(runtime), + handle, + } +} + +/// Sets the runtime to use to execute asynchronous tasks. +/// For convenience, this method takes a [`TokioHandle`]. +/// Note that you cannot drop the underlying [`TokioRuntime`]. +/// +/// # Examples +/// +/// ```rust +/// #[tokio::main] +/// async fn main() { +/// // perform some async task before initializing the app +/// do_something().await; +/// // share the current runtime with Tauri +/// tauri::async_runtime::set(tokio::runtime::Handle::current()); +/// +/// // bootstrap the tauri app... +/// // tauri::Builder::default().run().unwrap(); +/// } +/// +/// async fn do_something() {} +/// ``` +/// +/// # Panics +/// +/// Panics if the runtime is already set. +pub fn set(handle: TokioHandle) { + RUNTIME + .set(GlobalRuntime { + runtime: None, + handle: RuntimeHandle::Tokio(handle), + }) + .unwrap_or_else(|_| panic!("runtime already initialized")) +} + +/// Returns a handle of the async runtime. +pub fn handle() -> RuntimeHandle { + let runtime = RUNTIME.get_or_init(default_runtime); + runtime.handle() +} + +#[track_caller] +/// Runs a future to completion on runtime. +pub fn block_on(task: F) -> F::Output { + let runtime = RUNTIME.get_or_init(default_runtime); + runtime.block_on(task) +} + +#[track_caller] +/// Spawns a future onto the runtime. +pub fn spawn(task: F) -> JoinHandle +where + F: Future + Send + 'static, + F::Output: Send + 'static, +{ + let runtime = RUNTIME.get_or_init(default_runtime); + runtime.spawn(task) +} + +#[track_caller] +/// Runs the provided function on an executor dedicated to blocking operations. +pub fn spawn_blocking(func: F) -> JoinHandle +where + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, +{ + let runtime = RUNTIME.get_or_init(default_runtime); + runtime.spawn_blocking(func) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn runtime_spawn() { + let join = spawn(async { 5 }); + assert_eq!(join.await.unwrap(), 5); + } + + #[test] + fn runtime_block_on() { + assert_eq!(block_on(async { 0 }), 0); + } + + #[tokio::test] + async fn handle_spawn() { + let handle = handle(); + let join = handle.spawn(async { 5 }); + assert_eq!(join.await.unwrap(), 5); + } + + #[test] + fn handle_block_on() { + let handle = handle(); + assert_eq!(handle.block_on(async { 0 }), 0); + } + + #[tokio::test] + async fn handle_abort() { + let handle = handle(); + let join = handle.spawn(async { + // Here we sleep 1 second to ensure this task to be uncompleted when abort() invoked. + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + 5 + }); + join.abort(); + if let crate::Error::JoinError(raw_error) = join.await.unwrap_err() { + assert!(raw_error.is_cancelled()); + } else { + panic!("Abort did not result in the expected `JoinError`"); + } + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/error.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/error.rs new file mode 100644 index 000000000..252ba4d38 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/error.rs @@ -0,0 +1,201 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::fmt; + +/// A generic boxed error. +#[derive(Debug)] +pub struct SetupError(Box); + +impl From> for SetupError { + fn from(error: Box) -> Self { + Self(error) + } +} + +// safety: the setup error is only used on the main thread +// and we exit the process immediately. +unsafe impl Send for SetupError {} +unsafe impl Sync for SetupError {} + +impl fmt::Display for SetupError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl std::error::Error for SetupError {} + +/// Runtime errors that can happen inside a Tauri application. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum Error { + /// Runtime error. + #[error("runtime error: {0}")] + Runtime(#[from] tauri_runtime::Error), + /// Window label must be unique. + #[error("a window with label `{0}` already exists")] + WindowLabelAlreadyExists(String), + /// Webview label must be unique. + #[error("a webview with label `{0}` already exists")] + WebviewLabelAlreadyExists(String), + /// Cannot use the webview reparent function on webview windows. + #[error("cannot reparent when using a WebviewWindow")] + CannotReparentWebviewWindow, + /// Embedded asset not found. + #[error("asset not found: {0}")] + AssetNotFound(String), + /// Failed to serialize/deserialize. + #[error("JSON error: {0}")] + Json(#[from] serde_json::Error), + /// IO error. + #[error("{0}")] + Io(#[from] std::io::Error), + /// Failed to load window icon. + #[error("invalid icon: {0}")] + InvalidIcon(std::io::Error), + /// Invalid args when running a command. + #[error("invalid args `{1}` for command `{0}`: {2}")] + InvalidArgs(&'static str, &'static str, serde_json::Error), + /// Encountered an error in the setup hook, + #[error("error encountered during setup hook: {0}")] + Setup(SetupError), + /// Error initializing plugin. + #[error("failed to initialize plugin `{0}`: {1}")] + PluginInitialization(String, String), + /// A part of the URL is malformed or invalid. This may occur when parsing and combining + /// user-provided URLs and paths. + #[error("invalid url: {0}")] + InvalidUrl(url::ParseError), + /// Task join error. + #[error(transparent)] + JoinError(#[from] tokio::task::JoinError), + /// An error happened inside the isolation pattern. + #[cfg(feature = "isolation")] + #[error("isolation pattern error: {0}")] + IsolationPattern(#[from] tauri_utils::pattern::isolation::Error), + /// An invalid window URL was provided. Includes details about the error. + #[error("invalid window url: {0}")] + InvalidWebviewUrl(&'static str), + /// Invalid glob pattern. + #[error("invalid glob pattern: {0}")] + GlobPattern(#[from] glob::PatternError), + /// Image error. + #[cfg(any(feature = "image-png", feature = "image-ico"))] + #[error("failed to process image: {0}")] + Image(#[from] image::error::ImageError), + /// The Window's raw handle is invalid for the platform. + #[error("Unexpected `raw_window_handle` for the current platform")] + InvalidWindowHandle, + /// JNI error. + #[cfg(target_os = "android")] + #[error("jni error: {0}")] + Jni(#[from] jni::errors::Error), + /// Failed to receive message . + #[error("failed to receive message")] + FailedToReceiveMessage, + /// Menu error. + #[error("menu error: {0}")] + #[cfg(desktop)] + Menu(#[from] muda::Error), + /// Bad menu icon error. + #[error(transparent)] + #[cfg(desktop)] + BadMenuIcon(#[from] muda::BadIcon), + /// Tray icon error. + #[error("tray icon error: {0}")] + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "tray-icon"))))] + Tray(#[from] tray_icon::Error), + /// Bad tray icon error. + #[error(transparent)] + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "tray-icon"))))] + BadTrayIcon(#[from] tray_icon::BadIcon), + /// Path does not have a parent. + #[error("path does not have a parent")] + NoParent, + /// Path does not have an extension. + #[error("path does not have an extension")] + NoExtension, + /// Path does not have a basename. + #[error("path does not have a basename")] + NoBasename, + /// Path is not valid UTF-8 and cannot be represented in a URL. + #[error("path is not valid UTF-8: {}", .0.display())] + NonUtf8Path(std::path::PathBuf), + /// Cannot resolve current directory. + #[error("failed to read current dir: {0}")] + CurrentDir(std::io::Error), + /// Unknown path. + #[cfg(not(target_os = "android"))] + #[error("unknown path")] + UnknownPath, + /// The `app > appDirectoriesOverride` config contains a path that cannot be resolved. + #[error("invalid `app > appDirectoriesOverride` path `{path}`: {reason}", path = .0.display(), reason = .1)] + InvalidAppDirectoriesOverride(std::path::PathBuf, String), + /// Failed to invoke mobile plugin. + #[cfg(target_os = "android")] + #[error(transparent)] + PluginInvoke(#[from] crate::plugin::mobile::PluginInvokeError), + /// window not found. + #[error("window not found")] + WindowNotFound, + /// The resource id is invalid. + #[error("The resource id {0} is invalid.")] + BadResourceId(crate::resources::ResourceId), + /// The anyhow crate error. + #[error(transparent)] + Anyhow(#[from] anyhow::Error), + /// webview not found. + #[error("webview not found")] + WebviewNotFound, + /// API requires the unstable feature flag. + #[error("this feature requires the `unstable` flag on Cargo.toml")] + UnstableFeatureNotSupported, + /// Failed to deserialize scope object. + #[error("error deserializing scope: {0}")] + CannotDeserializeScope(Box), + /// Failed to get a raw handle. + #[error(transparent)] + RawHandleError(#[from] raw_window_handle::HandleError), + /// Something went wrong with the CSPRNG. + #[error("unable to generate random bytes from the operating system: {0}")] + Csprng(getrandom::Error), + /// Bad `__TAURI_INVOKE_KEY__` value received in ipc message. + #[error("bad __TAURI_INVOKE_KEY__ value received in ipc message")] + InvokeKey, + /// Illegal event name. + #[error("only alphanumeric, '-', '/', ':', '_' permitted for event names: {0:?}")] + IllegalEventName(String), + /// tokio oneshot channel failed to receive message + #[error(transparent)] + TokioOneshotRecv(#[from] tokio::sync::oneshot::error::RecvError), + /// Unexpected menu kind passed to menu/tray plugin command + #[error("Unexpected menu kind")] + UnexpectedMenuKind, + /// [`crate::image::Image::from_icon_resource`] failed + #[cfg(windows)] + #[cfg_attr(docsrs, doc(cfg(windows)))] + #[error("Can not load Image from icon resources: {0}")] + ImageFromResource(windows::core::Error), +} + +impl From for Error { + fn from(value: getrandom::Error) -> Self { + Self::Csprng(value) + } +} + +/// `Result` +pub type Result = std::result::Result; + +#[cfg(test)] +mod tests { + #[test] + fn error_is_send_sync() { + crate::test_utils::assert_send::(); + crate::test_utils::assert_sync::(); + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/event/event-system-spec.md b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/event/event-system-spec.md new file mode 100644 index 000000000..2fc910bef --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/event/event-system-spec.md @@ -0,0 +1,34 @@ +# Tauri Event System Specification + +## Emitters + +Emitters can emit to any and all listeners. + +- `App` and `AppHandle` +- `Window` +- `Webview` +- `WebviewWindow` +- Any type that implements `Manager` trait. + +## Emit functions + +- `emit`: emits an event to all listeners. +- `emit_to`: emits an event to a specified target. +- `emit_filter`: emits an event to targets based on a filtering callback. + +## Listeners + +Emitters can emit to any and all listeners. + +- `App` and `AppHandle` +- `Window` +- `Webview` +- `WebviewWindow` +- Any type that implements `Manager` trait but is limited to only using `listen_any/once_any`. + +## Listen functions + +- `listen`: Listens to all events targeting this listener type only. +- `once`: Listens to a single event targeting this listener type only. +- `listen_any` (available only through `Manager` trait): Listens to all events to any target (aka event sniffer). +- `once_any` (available only through `Manager` trait): Listens to a single event to any target (aka event sniffer). diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/event/event_name.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/event/event_name.rs new file mode 100644 index 000000000..3b553b622 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/event/event_name.rs @@ -0,0 +1,70 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{Deserialize, Deserializer}; + +/// Checks if an event name is valid. +fn is_event_name_valid(event: &str) -> bool { + event + .chars() + .all(|c| c.is_alphanumeric() || c == '-' || c == '/' || c == ':' || c == '_') +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub(crate) struct EventName(S); + +impl Copy for EventName<&str> {} + +impl> EventName { + pub(crate) fn new(s: S) -> crate::Result> { + if !is_event_name_valid(s.as_ref()) { + return Err(crate::Error::IllegalEventName(s.as_ref().to_string())); + } + Ok(EventName(s)) + } + + pub(crate) fn as_str_event(&self) -> EventName<&str> { + EventName(self.0.as_ref()) + } + + pub(crate) fn as_str(&self) -> &str { + self.0.as_ref() + } +} + +impl std::fmt::Display for EventName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl EventName<&'static str> { + // this convenience method is for using in const contexts to discharge the preconditions + // &'static prevents using this function accidentally with dynamically built string slices + pub(crate) const fn from_str(s: &'static str) -> EventName<&'static str> { + EventName(s) + } +} + +impl EventName<&str> { + pub fn into_owned(self) -> EventName { + EventName(self.0.to_string()) + } +} + +impl<'de> Deserialize<'de> for EventName { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let event_id = String::deserialize(deserializer)?; + if is_event_name_valid(&event_id) { + Ok(EventName(event_id)) + } else { + Err(serde::de::Error::custom( + "Event name must include only alphanumeric characters, `-`, `/`, `:` and `_`.", + )) + } + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/event/init.js b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/event/init.js new file mode 100644 index 000000000..8e80004de --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/event/init.js @@ -0,0 +1,10 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// eslint-disable-next-line +Object.defineProperty(window, '__TAURI_EVENT_PLUGIN_INTERNALS__', { + value: { + unregisterListener: __RAW_unregister_listener_function__ + } +}) diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/event/listener.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/event/listener.rs new file mode 100644 index 000000000..d45c84c4c --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/event/listener.rs @@ -0,0 +1,488 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{Runtime, Webview}; + +use super::{EmitArgs, Event, EventId, EventTarget}; + +use std::{ + boxed::Box, + cell::Cell, + collections::{HashMap, HashSet}, + sync::{ + Arc, Mutex, + atomic::{AtomicU32, Ordering}, + }, +}; + +/// What to do with the pending handler when resolving it? +enum Pending { + Unlisten(EventId), + Listen { + id: EventId, + event: crate::EventName, + handler: Handler, + }, + Emit(EmitArgs), + RemoveForTarget(EventTarget), +} + +/// Stored in [`Listeners`] to be called upon, when the event that stored it, is triggered. +struct Handler { + target: EventTarget, + callback: Box, +} + +impl Handler { + fn new(target: EventTarget, callback: F) -> Self { + Self { + target, + callback: Box::new(callback), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct JsHandler { + target: EventTarget, + id: EventId, +} + +impl JsHandler { + fn new(target: EventTarget, id: EventId) -> Self { + Self { target, id } + } +} + +type WebviewLabel = String; + +/// Holds event handlers and pending event handlers, along with the salts associating them. +struct InnerListeners { + pending: Mutex>, + handlers: Mutex>>, + js_event_listeners: Mutex>>>, + function_name: &'static str, + listeners_object_name: &'static str, + next_event_id: Arc, +} + +/// A self-contained event manager. +#[derive(Clone)] +pub struct Listeners { + inner: Arc, +} + +impl Default for Listeners { + fn default() -> Self { + Self { + inner: Arc::new(InnerListeners { + pending: Mutex::default(), + handlers: Mutex::default(), + js_event_listeners: Mutex::default(), + function_name: "__internal_unstable_listeners_function_id__", + listeners_object_name: "__internal_unstable_listeners_object_id__", + next_event_id: Default::default(), + }), + } + } +} + +impl Listeners { + pub(crate) fn next_event_id(&self) -> EventId { + self.inner.next_event_id.fetch_add(1, Ordering::Relaxed) + } + + /// Function name to represent the JavaScript event function. + pub(crate) fn function_name(&self) -> &str { + self.inner.function_name + } + + /// Listener object name to represent the JavaScript event listener object. + pub(crate) fn listeners_object_name(&self) -> &str { + self.inner.listeners_object_name + } + + /// Insert a pending event action to the queue. + fn insert_pending(&self, action: Pending) { + self + .inner + .pending + .lock() + .expect("poisoned pending event queue") + .push(action) + } + + /// Finish all pending event actions. + fn flush_pending(&self) -> crate::Result<()> { + let pending = { + let mut lock = self + .inner + .pending + .lock() + .expect("poisoned pending event queue"); + std::mem::take(&mut *lock) + }; + + for action in pending { + match action { + Pending::Unlisten(id) => self.unlisten(id), + Pending::Listen { id, event, handler } => self.listen_with_id(id, event, handler), + Pending::Emit(args) => self.emit(args)?, + Pending::RemoveForTarget(target) => self.remove_listeners_for_target(target), + } + } + + Ok(()) + } + + fn listen_with_id(&self, id: EventId, event: crate::EventName, handler: Handler) { + match self.inner.handlers.try_lock() { + Err(_) => self.insert_pending(Pending::Listen { id, event, handler }), + Ok(mut lock) => { + lock.entry(event).or_default().insert(id, handler); + } + } + } + + /// Adds an event listener. + pub(crate) fn listen( + &self, + event: crate::EventName, + target: EventTarget, + handler: F, + ) -> EventId { + let id = self.next_event_id(); + let handler = Handler::new(target, handler); + self.listen_with_id(id, event, handler); + id + } + + /// Listen to an event and immediately unlisten. + pub(crate) fn once( + &self, + event: crate::EventName, + target: EventTarget, + handler: F, + ) -> EventId { + let self_ = self.clone(); + let handler = Cell::new(Some(handler)); + + self.listen(event, target, move |event| { + let id = event.id; + self_.unlisten(id); + let handler = handler + .take() + .expect("attempted to call handler more than once"); + handler(event); + }) + } + + /// Removes an event listener. + pub(crate) fn unlisten(&self, id: EventId) { + match self.inner.handlers.try_lock() { + Err(_) => self.insert_pending(Pending::Unlisten(id)), + Ok(mut lock) => lock.values_mut().for_each(|handler| { + handler.remove(&id); + }), + } + } + + /// Removes every Rust-side event listener registered for the exact given target. + /// + /// Called when a window or webview is destroyed so its listeners don't leak. + /// Global targets such as [`EventTarget::App`], [`EventTarget::Any`] and [`EventTarget::AnyLabel`] are never matched here. + fn remove_listeners_for_target(&self, target: EventTarget) { + match self.inner.handlers.try_lock() { + Err(_) => self.insert_pending(Pending::RemoveForTarget(target)), + Ok(mut lock) => { + for handlers in lock.values_mut() { + handlers.retain(|_, handler| handler.target != target); + } + lock.retain(|_, handlers| !handlers.is_empty()); + } + } + } + + /// Emits the given event with its payload based on a filter. + pub(crate) fn emit_filter(&self, emit_args: EmitArgs, filter: Option) -> crate::Result<()> + where + F: Fn(&EventTarget) -> bool, + { + let mut maybe_pending = false; + + match self.inner.handlers.try_lock() { + Err(_) => self.insert_pending(Pending::Emit(emit_args)), + Ok(lock) => { + if let Some(handlers) = lock.get(&emit_args.event) { + let handlers = handlers.iter(); + let handlers = handlers.filter(|(_, h)| match_any_or_filter(&h.target, &filter)); + for (&id, Handler { callback, .. }) in handlers { + maybe_pending = true; + (callback)(Event::new(id, emit_args.payload.clone())) + } + } + } + } + + if maybe_pending { + self.flush_pending()?; + } + + Ok(()) + } + + /// Emits the given event with its payload. + pub(crate) fn emit(&self, emit_args: EmitArgs) -> crate::Result<()> { + self.emit_filter(emit_args, None::<&dyn Fn(&EventTarget) -> bool>) + } + + pub(crate) fn listen_js( + &self, + event: crate::EventName<&str>, + source_webview_label: &str, + target: EventTarget, + id: EventId, + ) { + let event = event.into_owned(); + let mut listeners = self.inner.js_event_listeners.lock().unwrap(); + listeners + .entry(source_webview_label.to_string()) + .or_default() + .entry(event) + .or_default() + .insert(JsHandler::new(target, id)); + } + + pub(crate) fn unlisten_js(&self, event: crate::EventName<&str>, id: EventId) { + let event = event.into_owned(); + let mut js_listeners = self.inner.js_event_listeners.lock().unwrap(); + let js_listeners = js_listeners.values_mut(); + for js_listeners in js_listeners { + if let Some(handlers) = js_listeners.get_mut(&event) { + handlers.retain(|h| h.id != id); + + if handlers.is_empty() { + js_listeners.remove(&event); + } + } + } + } + + /// Removes all JS event listeners registered from the given webview. + fn remove_webview_js_listeners(&self, webview_label: &str) { + self + .inner + .js_event_listeners + .lock() + .unwrap() + .remove(webview_label); + } + + /// Removes all event listeners associated with a destroyed webview. + pub(crate) fn remove_webview_listeners(&self, label: &str) { + self.remove_webview_js_listeners(label); + self.remove_listeners_for_target(EventTarget::webview(label)); + } + + /// Removes all event listeners associated with a destroyed window. + pub(crate) fn remove_window_listeners(&self, label: &str) { + self.remove_listeners_for_target(EventTarget::window(label)); + self.remove_listeners_for_target(EventTarget::webview_window(label)); + } + + pub(crate) fn has_js_listener bool>( + &self, + event: crate::EventName<&str>, + filter: F, + ) -> bool { + let event = event.into_owned(); + let js_listeners = self.inner.js_event_listeners.lock().unwrap(); + js_listeners.values().any(|events| { + events + .get(&event) + .map(|handlers| handlers.iter().any(|handler| filter(&handler.target))) + .unwrap_or(false) + }) + } + + pub(crate) fn emit_js_filter<'a, R, I, F>( + &self, + mut webviews: I, + emit_args: &EmitArgs, + filter: Option, + ) -> crate::Result<()> + where + R: Runtime, + I: Iterator>, + F: Fn(&EventTarget) -> bool, + { + let event = &emit_args.event; + let js_listeners = self.inner.js_event_listeners.lock().unwrap(); + webviews.try_for_each(|webview| { + if let Some(handlers) = js_listeners.get(webview.label()).and_then(|s| s.get(event)) { + let ids = handlers + .iter() + .filter(|handler| match_any_or_filter(&handler.target, &filter)) + .map(|handler| handler.id) + .collect::>(); + webview.emit_js(emit_args, &ids)?; + } + + Ok(()) + }) + } + + pub(crate) fn emit_js<'a, R, I>(&self, webviews: I, emit_args: &EmitArgs) -> crate::Result<()> + where + R: Runtime, + I: Iterator>, + { + self.emit_js_filter(webviews, emit_args, None::<&dyn Fn(&EventTarget) -> bool>) + } +} + +#[inline(always)] +fn match_any_or_filter bool>( + target: &EventTarget, + filter: &Option, +) -> bool { + *target == EventTarget::Any || filter.as_ref().map(|f| f(target)).unwrap_or(true) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::event::EventTarget; + use proptest::prelude::*; + + // dummy event handler function + fn event_fn(s: Event) { + println!("{s:?}"); + } + + proptest! { + #![proptest_config(ProptestConfig::with_cases(10000))] + + // check to see if listen() is properly passing keys into the LISTENERS map + #[test] + fn listeners_check_key(e in "[a-z]+") { + let listeners: Listeners = Default::default(); + // clone e as the key + let key = crate::EventName::new(e).unwrap(); + // pass e and an dummy func into listen + listeners.listen(key.clone(), EventTarget::Any, event_fn); + + // lock mutex + let l = listeners.inner.handlers.lock().unwrap(); + + // check if the generated key is in the map + assert!(l.contains_key(&key)); + } + + // check to see if listen inputs a handler function properly into the LISTENERS map. + #[test] + fn listeners_check_fn(e in "[a-z]+") { + let listeners: Listeners = Default::default(); + let key = crate::EventName::new(e).unwrap(); + // pass e and an dummy func into listen + listeners.listen(key.clone(), EventTarget::Any, event_fn); + + // lock mutex + let mut l = listeners.inner.handlers.lock().unwrap(); + + // check if l contains key + if l.contains_key(&key) { + // grab key if it exists + let handler = l.get_mut(&key); + // check to see if we get back a handler or not + match handler { + // pass on Some(handler) + Some(_) => {}, + // Fail on None + None => panic!("handler is None") + } + } + } + + // check to see if on_event properly grabs the stored function from listen. + #[test] + fn check_on_event(e in "[a-z]+", d in "[a-z]+") { + let listeners: Listeners = Default::default(); + let key = crate::EventName::new(e).unwrap(); + // call listen with key and the event_fn dummy func + listeners.listen(key.clone(), EventTarget::Any, event_fn); + // call on event with key and d. + listeners.emit(EmitArgs::new(key.as_str_event(), &d)?)?; + + // lock the mutex + let l = listeners.inner.handlers.lock().unwrap(); + + // assert that the key is contained in the listeners map + assert!(l.contains_key(&key)); + } + } + + #[test] + fn event_no_deadlocks() { + let listeners = Listeners::default(); + let listeners_clone = listeners.clone(); + let event = crate::EventName::new("test-event".to_owned()).unwrap(); + let event_clone = event.clone(); + listeners.once(event.clone(), EventTarget::Any, move |_event| { + listeners_clone + .emit(EmitArgs::new(event_clone.as_str_event(), &()).unwrap()) + .unwrap(); + }); + listeners + .emit(EmitArgs::new(event.as_str_event(), &()).unwrap()) + .unwrap(); + } + + #[test] + fn listeners_removed_on_webview_close() { + let listeners = Listeners::default(); + let event = crate::EventName::new("test-event".to_owned()).unwrap(); + let webview_label = "main"; + + let window = listeners.listen(event.clone(), EventTarget::window(webview_label), event_fn); + // Same label as the window, but a different target kind (multi-webview mode). + let webview = listeners.listen(event.clone(), EventTarget::webview(webview_label), event_fn); + let webview_window = listeners.listen( + event.clone(), + EventTarget::webview_window(webview_label), + event_fn, + ); + let other = listeners.listen(event.clone(), EventTarget::webview("other"), event_fn); + let app = listeners.listen(event.clone(), EventTarget::App, event_fn); + let any = listeners.listen(event.clone(), EventTarget::Any, event_fn); + let labeled = listeners.listen(event.clone(), EventTarget::labeled(webview_label), event_fn); + + let id = listeners.next_event_id(); + listeners.listen_js( + event.as_str_event(), + webview_label, + EventTarget::webview(webview_label), + id, + ); + assert!(listeners.has_js_listener(event.as_str_event(), |_| true)); + + listeners.remove_webview_listeners(webview_label); + + assert!(!listeners.has_js_listener(event.as_str_event(), |_| true)); + + let lock = listeners.inner.handlers.lock().unwrap(); + let remaining: HashSet = lock.get(&event).unwrap().keys().copied().collect(); + + // only the exact `Webview { label: "main" }` listener is gone + assert!(!remaining.contains(&webview)); + // same label but different kind is kept + assert!(remaining.contains(&window)); + assert!(remaining.contains(&webview_window)); + // unrelated and global listeners are kept + assert!(remaining.contains(&other)); + assert!(remaining.contains(&app)); + assert!(remaining.contains(&any)); + assert!(remaining.contains(&labeled)); + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/event/mod.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/event/mod.rs new file mode 100644 index 000000000..05dfd04f0 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/event/mod.rs @@ -0,0 +1,258 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +mod listener; +pub(crate) mod plugin; +use std::{convert::Infallible, str::FromStr}; + +pub(crate) use listener::Listeners; +use serde::{Deserialize, Serialize}; + +mod event_name; + +pub(crate) use event_name::EventName; + +use crate::ipc::CallbackFn; + +/// Unique id of an event. +pub type EventId = u32; + +/// Event Target +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +#[serde(tag = "kind")] +#[non_exhaustive] +pub enum EventTarget { + /// Any and all event targets. + Any, + + /// Any [`Window`](crate::Window), [`Webview`](crate::Webview) or [`WebviewWindow`](crate::WebviewWindow) that have this label. + AnyLabel { + /// Target label. + label: String, + }, + + /// [`App`](crate::App) and [`AppHandle`](crate::AppHandle) targets. + App, + + /// [`Window`](crate::Window) target. + Window { + /// window label. + label: String, + }, + + /// [`Webview`](crate::Webview) target. + Webview { + /// webview label. + label: String, + }, + + /// [`WebviewWindow`](crate::WebviewWindow) target. + WebviewWindow { + /// webview window label. + label: String, + }, +} + +impl EventTarget { + /// [`Self::Any`] target. + pub fn any() -> Self { + Self::Any + } + + /// [`Self::App`] target. + pub fn app() -> Self { + Self::App + } + + /// [`Self::AnyLabel`] target. + pub fn labeled(label: impl Into) -> Self { + Self::AnyLabel { + label: label.into(), + } + } + + /// [`Self::Window`] target. + pub fn window(label: impl Into) -> Self { + Self::Window { + label: label.into(), + } + } + + /// [`Self::Webview`] target. + pub fn webview(label: impl Into) -> Self { + Self::Webview { + label: label.into(), + } + } + + /// [`Self::WebviewWindow`] target. + pub fn webview_window(label: impl Into) -> Self { + Self::WebviewWindow { + label: label.into(), + } + } +} + +impl> From for EventTarget { + fn from(value: T) -> Self { + Self::AnyLabel { + label: value.as_ref().to_string(), + } + } +} + +impl FromStr for EventTarget { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + Ok(Self::AnyLabel { + label: s.to_string(), + }) + } +} + +/// Serialized emit arguments. +#[derive(Clone)] +pub struct EmitArgs { + /// event name. + event: EventName, + /// Serialized payload. + payload: String, +} + +impl EmitArgs { + pub fn new(event: EventName<&str>, payload: &S) -> crate::Result { + #[cfg(feature = "tracing")] + let _span = tracing::debug_span!("window::emit::serialize").entered(); + Ok(EmitArgs { + event: event.into_owned(), + payload: serde_json::to_string(payload)?, + }) + } + + pub fn new_str(event: EventName<&str>, payload: String) -> crate::Result { + #[cfg(feature = "tracing")] + let _span = tracing::debug_span!("window::emit::json").entered(); + Ok(EmitArgs { + event: event.into_owned(), + payload, + }) + } +} + +/// An event that was emitted. +#[derive(Debug, Clone)] +pub struct Event { + id: EventId, + data: String, +} + +impl Event { + fn new(id: EventId, data: String) -> Self { + Self { id, data } + } + + /// The [`EventId`] of the handler that was triggered. + pub fn id(&self) -> EventId { + self.id + } + + /// The event payload. + pub fn payload(&self) -> &str { + &self.data + } +} + +pub(crate) fn listen_js_script( + listeners_object_name: &str, + serialized_target: &str, + event: EventName<&str>, + event_id: EventId, + handler: CallbackFn, +) -> String { + let handler_id = handler.0; + format!( + "(function () {{ + if (window['{listeners_object_name}'] === void 0) {{ + Object.defineProperty(window, '{listeners_object_name}', {{ value: Object.create(null) }}); + }} + if (window['{listeners_object_name}']['{event}'] === void 0) {{ + Object.defineProperty(window['{listeners_object_name}'], '{event}', {{ value: Object.create(null) }}); + }} + const eventListeners = window['{listeners_object_name}']['{event}'] + const listener = {{ + target: {serialized_target}, + handlerId: {handler_id} + }}; + Object.defineProperty(eventListeners, '{event_id}', {{ value: listener, configurable: true }}); + }})() + ", + ) +} + +pub(crate) fn emit_js_script( + event_emit_function_name: &str, + emit_args: &EmitArgs, + serialized_ids: &str, +) -> crate::Result { + Ok(format!( + "(function () {{ const fn = window['{}']; fn && fn({{event: '{}', payload: {}}}, {ids}) }})()", + event_emit_function_name, + emit_args.event, + emit_args.payload, + ids = serialized_ids, + )) +} + +pub(crate) fn unlisten_js_script( + listeners_object_name: &str, + event_arg: &str, + event_id_arg: &str, +) -> String { + format!( + "(function () {{ + const listeners = (window['{listeners_object_name}'] || {{}})[{event_arg}] + if (listeners) {{ + const listener = listeners[{event_id_arg}] + if (listener) {{ + window.__TAURI_INTERNALS__.unregisterCallback(listener.handlerId) + }} + }} + }})() + ", + ) +} + +pub(crate) fn event_initialization_script(function_name: &str, listeners: &str) -> String { + format!( + "Object.defineProperty(window, '{function_name}', {{ + value: function (eventData, ids) {{ + const listeners = (window['{listeners}'] && window['{listeners}'][eventData.event]) || [] + for (const id of ids) {{ + const listener = listeners[id] + if (listener) {{ + eventData.id = id + window.__TAURI_INTERNALS__.runCallback(listener.handlerId, eventData) + }} + }} + }} + }}); + " + ) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_illegal_event_name() { + let s = EventName::new("some\r illegal event name") + .unwrap_err() + .to_string(); + assert_eq!( + "only alphanumeric, '-', '/', ':', '_' permitted for event names: \"some\\r illegal event name\"", + s + ); + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/event/plugin.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/event/plugin.rs new file mode 100644 index 000000000..fdbbf58dd --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/event/plugin.rs @@ -0,0 +1,82 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +use serde_json::Value as JsonValue; +use serialize_to_javascript::{DefaultTemplate, Template, default_template}; + +use crate::plugin::{Builder, TauriPlugin}; +use crate::{AppHandle, Emitter, Manager, Webview}; +use crate::{EventId, Result, Runtime, command, ipc::CallbackFn}; + +use super::EventName; +use super::EventTarget; + +#[command(root = "crate")] +async fn listen( + webview: Webview, + event: EventName, + target: EventTarget, + handler: CallbackFn, +) -> Result { + webview.listen_js(event.as_str_event(), target, handler) +} + +#[command(root = "crate")] +async fn unlisten( + webview: Webview, + event: EventName, + event_id: EventId, +) -> Result<()> { + webview.unlisten_js(event.as_str_event(), event_id) +} + +#[command(root = "crate")] +async fn emit( + app: AppHandle, + event: EventName, + payload: Option, +) -> Result<()> { + app.emit(event.as_str(), payload) +} + +#[command(root = "crate")] +async fn emit_to( + app: AppHandle, + target: EventTarget, + event: EventName, + payload: Option, +) -> Result<()> { + app.emit_to(target, event.as_str(), payload) +} + +/// Initializes the event plugin. +pub(crate) fn init>(manager: &M) -> TauriPlugin { + let listeners = manager.manager().listeners(); + + #[derive(Template)] + #[default_template("./init.js")] + struct InitJavascript { + #[raw] + unregister_listener_function: String, + } + + let init_script = InitJavascript { + unregister_listener_function: format!( + "(event, eventId) => {}", + crate::event::unlisten_js_script(listeners.listeners_object_name(), "event", "eventId") + ), + }; + + Builder::new("event") + .invoke_handler(crate::generate_handler![ + #![plugin(event)] + listen, unlisten, emit, emit_to + ]) + .js_init_script( + init_script + .render_default(&Default::default()) + .unwrap() + .to_string(), + ) + .build() +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/image/mod.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/image/mod.rs new file mode 100644 index 000000000..f631b00a2 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/image/mod.rs @@ -0,0 +1,550 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Image types used by this crate and also referenced by the JavaScript API layer. + +pub(crate) mod plugin; + +use std::borrow::Cow; +use std::sync::Arc; + +#[cfg(windows)] +use windows::{ + Win32::{ + Foundation::{E_FAIL, ERROR_INVALID_PARAMETER, ERROR_NOT_SUPPORTED, WIN32_ERROR}, + Graphics::Gdi::{ + BI_RGB, BITMAPINFO, BITMAPINFOHEADER, CreateCompatibleDC, DIB_RGB_COLORS, DeleteDC, + GetDIBits, HBITMAP, + }, + System::LibraryLoader::GetModuleHandleW, + UI::WindowsAndMessaging::{ + GetIconInfo, GetSystemMetrics, HICON, ICONINFO, IMAGE_ICON, LR_DEFAULTCOLOR, LoadImageW, + SM_CXICON, SM_CYICON, + }, + }, + core::{Owned, PCWSTR}, +}; + +use crate::{Resource, ResourceId, ResourceTable}; + +/// Identifies an icon resource embedded in the executable. +#[cfg(windows)] +#[cfg_attr(docsrs, doc(cfg(windows)))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum IconResource<'a> { + /// An integer resource identifier (`MAKEINTRESOURCE`). + Id(u16), + /// A string resource name. + Name(&'a str), +} + +#[cfg(windows)] +impl From for IconResource<'_> { + fn from(id: u16) -> Self { + Self::Id(id) + } +} + +#[cfg(windows)] +impl<'a> From<&'a str> for IconResource<'a> { + fn from(name: &'a str) -> Self { + Self::Name(name) + } +} + +/// Loads the default window icon from the application icon resource, reporting failures. +/// +/// Used by [`crate::generate_context!`]; not public API. +#[cfg(windows)] +#[doc(hidden)] +pub fn default_window_icon_from_app_icon_resource() -> Option> { + // the window icon is drawn in the title bar and, as a fallback for the taskbar icon, + // at the system's large icon size (32x32 at 96 DPI, scaled with the system DPI), + // so pick the entry Windows would use for the taskbar instead of a larger one it has to shrink + // `GetSystemMetrics` returns 0 on failure + let metric = |index| match unsafe { GetSystemMetrics(index) } { + n if n > 0 => n as u32, + _ => 32, + }; + let (width, height) = (metric(SM_CXICON), metric(SM_CYICON)); + match Image::from_icon_resource( + crate::utils::platform::WINDOWS_APP_ICON_RESOURCE_ID, + width, + height, + ) { + Ok(icon) => Some(icon), + Err(e) => { + // a logger is usually not installed yet when `generate_context!` runs + #[cfg(debug_assertions)] + eprintln!("failed to load the default window icon from the application icon resource: {e}"); + log::warn!("failed to load the default window icon from the application icon resource: {e}"); + None + } + } +} + +#[cfg(windows)] +const BYTES_PER_PIXEL: usize = 4; + +/// Reads `hbm` as a top-down 32bpp BGRA bitmap of the given dimensions. +/// +/// # Safety +/// +/// `hbm` must be a valid bitmap handle and `width` and `height` must be positive. +#[cfg(windows)] +unsafe fn read_bgra(hbm: HBITMAP, width: i32, height: i32) -> crate::Result> { + let image_bytes = (width as usize) + .checked_mul(height as usize) + .and_then(|n| n.checked_mul(BYTES_PER_PIXEL)) + .ok_or_else(|| resource_error(ERROR_INVALID_PARAMETER, "image size overflows usize"))?; + let mut bgra = vec![0u8; image_bytes]; + + let mut bitmap_info = BITMAPINFO::default(); + bitmap_info.bmiHeader.biSize = std::mem::size_of::() as _; + bitmap_info.bmiHeader.biWidth = width; + // negative value for top-down + bitmap_info.bmiHeader.biHeight = -height; + bitmap_info.bmiHeader.biBitCount = (BYTES_PER_PIXEL * 8) as u16; + bitmap_info.bmiHeader.biPlanes = 1; + bitmap_info.bmiHeader.biCompression = BI_RGB.0; + + unsafe { + let hdc = CreateCompatibleDC(None); + let scan_lines = GetDIBits( + hdc, + hbm, + 0, + height as u32, + Some(bgra.as_mut_ptr() as _), + &mut bitmap_info, + DIB_RGB_COLORS, + ); + // capture the error before `DeleteDC` can overwrite it + let error = (scan_lines != height).then(|| { + last_error_or(&format!( + "GetDIBits copied {scan_lines} of {height} scan lines" + )) + }); + let _ = DeleteDC(hdc); + if let Some(error) = error { + return Err(crate::Error::ImageFromResource(error)); + } + } + + Ok(bgra) +} + +#[cfg(windows)] +fn resource_error(code: WIN32_ERROR, message: &str) -> crate::Error { + crate::Error::ImageFromResource(windows::core::Error::new(code.to_hresult(), message)) +} + +/// Returns the calling thread's last error, or a generic `E_FAIL` with `message` +/// when no error code was set (GDI functions do not always set one). +#[cfg(windows)] +fn last_error_or(message: &str) -> windows::core::Error { + let error = windows::core::Error::from_thread(); + if error.code().is_ok() { + windows::core::Error::new(E_FAIL, message) + } else { + error + } +} + +/// An RGBA Image in row-major order from top to bottom. +#[derive(Clone)] +pub struct Image<'a> { + rgba: Cow<'a, [u8]>, + width: u32, + height: u32, +} + +impl std::fmt::Debug for Image<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Image") + .field( + "rgba", + // Reduces the debug size compared to the derived default, as the default + // would format the raw bytes as numbers `[0, 0, 0, 0]` for 1 pixel. + // The custom format doesn't grow as much with larger images: + // `Image { rgba: Cow::Borrowed([u8; 4096]), width: 32, height: 32 }` + &format_args!( + "Cow::{}([u8; {}])", + match &self.rgba { + Cow::Borrowed(_) => "Borrowed", + Cow::Owned(_) => "Owned", + }, + self.rgba.len() + ), + ) + .field("width", &self.width) + .field("height", &self.height) + .finish() + } +} + +impl Resource for Image<'static> {} + +impl Image<'static> { + /// Creates a new Image using RGBA data, in row-major order from top to bottom, and with specified width and height. + /// + /// Similar to [`Self::new`] but avoids cloning the rgba data to get an owned Image. + pub const fn new_owned(rgba: Vec, width: u32, height: u32) -> Self { + Self { + rgba: Cow::Owned(rgba), + width, + height, + } + } +} + +impl<'a> Image<'a> { + /// Creates a new Image using RGBA data, in row-major order from top to bottom, and with specified width and height. + pub const fn new(rgba: &'a [u8], width: u32, height: u32) -> Self { + Self { + rgba: Cow::Borrowed(rgba), + width, + height, + } + } + + /// Creates a new image using the provided bytes. + /// + /// Only `ico` and `png` are supported (based on activated feature flag). + #[cfg(any(feature = "image-ico", feature = "image-png"))] + #[cfg_attr(docsrs, doc(cfg(any(feature = "image-ico", feature = "image-png"))))] + pub fn from_bytes(bytes: &[u8]) -> crate::Result { + let img = image::load_from_memory(bytes)?; + let (width, height) = (img.width(), img.height()); + Ok(Self { + rgba: Cow::Owned(img.into_rgba8().into_raw()), + width, + height, + }) + } + + /// Creates a new image using the provided path. + /// + /// Only `ico` and `png` are supported (based on activated feature flag). + #[cfg(any(feature = "image-ico", feature = "image-png"))] + #[cfg_attr(docsrs, doc(cfg(any(feature = "image-ico", feature = "image-png"))))] + pub fn from_path>(path: P) -> crate::Result { + let bytes = std::fs::read(path)?; + Self::from_bytes(&bytes) + } + + /// Creates a new image from the application icon embedded in the executable of the current process. + /// + /// The application icon is the one `tauri-build` embeds with the + /// [`WINDOWS_APP_ICON_RESOURCE_ID`](crate::utils::platform::WINDOWS_APP_ICON_RESOURCE_ID) id, + /// this could change in the future. + #[cfg(windows)] + #[cfg_attr(docsrs, doc(cfg(windows)))] + pub fn from_app_icon_resource(size: u32) -> crate::Result { + Image::from_icon_resource( + crate::utils::platform::WINDOWS_APP_ICON_RESOURCE_ID, + size, + size, + ) + } + + /// Create a new image from an icon resource embedded in the executable of the current process. + /// + /// Resources are looked up in the process executable (`GetModuleHandleW(NULL)`), + /// not in the DLL containing this code when tauri is built as a library. + /// + /// **Note**: This might take ~2ms for [`LoadImageW`] to load the image for the first time. + /// + /// ## Examples + /// + /// The resource can be identified by its integer id or by its name, see [`IconResource`]. + /// + /// ```no_run + /// # use tauri::image::Image; + /// # fn main() -> tauri::Result<()> { + /// let icon = Image::from_icon_resource(1, 32, 32)?; + /// let icon = Image::from_icon_resource("icon", 32, 32)?; + /// # Ok(()) + /// # } + /// ``` + #[cfg(windows)] + #[cfg_attr(docsrs, doc(cfg(windows)))] + pub fn from_icon_resource<'r>( + resource: impl Into>, + width: u32, + height: u32, + ) -> crate::Result { + let (width_i32, height_i32) = match (i32::try_from(width), i32::try_from(height)) { + (Ok(w), Ok(h)) if w > 0 && h > 0 => (w, h), + _ => { + return Err(resource_error( + ERROR_INVALID_PARAMETER, + "width and height must be between 1 and i32::MAX", + )); + } + }; + + // keeps the wide string alive for the `LoadImageW` call + let name: Vec; + let resource_id = match resource.into() { + // MAKEINTRESOURCE + IconResource::Id(id) => PCWSTR(id as usize as *const u16), + IconResource::Name(n) => { + name = n.encode_utf16().chain(std::iter::once(0)).collect(); + PCWSTR(name.as_ptr()) + } + }; + + let hicon = unsafe { + Owned::new(HICON( + LoadImageW( + Some( + GetModuleHandleW(PCWSTR::null()) + .map_err(crate::Error::ImageFromResource)? + .into(), + ), + resource_id, + IMAGE_ICON, + width_i32, + height_i32, + LR_DEFAULTCOLOR, + ) + .map_err(crate::Error::ImageFromResource)? + .0, + )) + }; + + let mut icon_info = ICONINFO::default(); + unsafe { GetIconInfo(*hicon, &mut icon_info).map_err(crate::Error::ImageFromResource)? }; + let hbm_mask = unsafe { Owned::new(icon_info.hbmMask) }; + let hbm_color = unsafe { Owned::new(icon_info.hbmColor) }; + + // monochrome icons only have a mask bitmap (AND mask stacked on top of the XOR mask) + if hbm_color.is_invalid() { + return Err(resource_error( + ERROR_NOT_SUPPORTED, + "monochrome icons are not supported", + )); + } + + let mut bgra = unsafe { read_bgra(*hbm_color, width_i32, height_i32)? }; + + // Color bitmaps without an alpha channel (e.g. 24bpp icons) read back with alpha = 0 on every pixel, + // so recover the alpha channel from the AND mask: a set bit means the pixel is transparent. + if bgra + .as_chunks::() + .0 + .iter() + .all(|px| px[3] == 0) + { + let mask = unsafe { read_bgra(*hbm_mask, width_i32, height_i32)? }; + for (px, mask) in bgra + .as_chunks_mut::() + .0 + .iter_mut() + .zip(mask.as_chunks::().0) + { + // the 1bpp mask expands to black (clear bit) or white (set bit) + px[3] = if mask[0] == 0 { 0xFF } else { 0 }; + } + } + + let rgba = { + for px in bgra.as_chunks_mut::().0 { + // Swap Blue and Red channels + px.swap(0, 2); + } + bgra + }; + + Ok(Image::new_owned(rgba, width, height)) + } + + /// Returns the RGBA data for this image, in row-major order from top to bottom. + pub fn rgba(&'a self) -> &'a [u8] { + &self.rgba + } + + /// Returns the width of this image. + pub fn width(&self) -> u32 { + self.width + } + + /// Returns the height of this image. + pub fn height(&self) -> u32 { + self.height + } + + /// Convert into a 'static owned [`Image`]. + /// This will allocate. + pub fn to_owned(self) -> Image<'static> { + Image { + rgba: match self.rgba { + Cow::Owned(v) => Cow::Owned(v), + Cow::Borrowed(v) => Cow::Owned(v.to_vec()), + }, + height: self.height, + width: self.width, + } + } +} + +impl<'a> From> for crate::runtime::Icon<'a> { + fn from(img: Image<'a>) -> Self { + Self { + rgba: img.rgba, + width: img.width, + height: img.height, + } + } +} + +/// Ensures the RGBA buffer matches the image dimensions. +/// +/// The Linux backends of `muda` and `tray-icon` do not validate this, +/// so a mismatch would panic when the icon is rendered. +#[cfg(desktop)] +fn check_rgba_size(img: &Image<'_>) -> crate::Result<()> { + let expected = (img.width as u64) * (img.height as u64) * 4; + if img.rgba.len() as u64 != expected { + return Err(crate::Error::InvalidIcon(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!( + "RGBA buffer has {} bytes but a {}x{} image needs {expected}", + img.rgba.len(), + img.width, + img.height + ), + ))); + } + Ok(()) +} + +#[cfg(desktop)] +impl TryFrom> for muda::Icon { + type Error = crate::Error; + + fn try_from(img: Image<'_>) -> Result { + check_rgba_size(&img)?; + muda::Icon::from_rgba(img.rgba.into_owned(), img.width, img.height).map_err(Into::into) + } +} + +#[cfg(all(desktop, feature = "tray-icon"))] +impl TryFrom> for tray_icon::Icon { + type Error = crate::Error; + + fn try_from(img: Image<'_>) -> Result { + check_rgba_size(&img)?; + tray_icon::Icon::from_rgba(img.rgba.into_owned(), img.width, img.height).map_err(Into::into) + } +} + +/// An image type that accepts file paths, raw bytes, previously loaded images and image objects. +/// +/// This type is meant to be used along the [transformImage](https://v2.tauri.app/reference/javascript/api/namespaceimage/#transformimage) API. +/// +/// # Stability +/// +/// The stability of the variants are not guaranteed, and matching against them is not recommended. +/// Use [`JsImage::into_img`] instead. +#[derive(serde::Deserialize)] +#[serde(untagged)] +#[non_exhaustive] +pub enum JsImage { + /// A reference to an image in the filesystem. This requires `image-ico` or `image-png` cargo features. + #[non_exhaustive] + Path(std::path::PathBuf), + /// ICO or PNG image in raw bytes. This requires `image-ico` or `image-png` cargo features. + #[non_exhaustive] + Bytes(Vec), + /// An image that was previously loaded with the API and is stored in the resource table. + #[non_exhaustive] + Resource(ResourceId), + /// Raw RGBA definition of an image. + #[non_exhaustive] + Rgba { + /// Image bytes. + rgba: Vec, + /// Image width. + width: u32, + /// Image height. + height: u32, + }, +} + +impl JsImage { + /// Converts this intermediate image format into an actual [`Image`]. + /// + /// This will retrieve the image from the passed [`ResourceTable`] if it is [`JsImage::Resource`] + /// and will return an error if it doesn't exist in the passed [`ResourceTable`] so make sure + /// the passed [`ResourceTable`] is the same one used to store the image, usually this should be + /// the webview resources table. + pub fn into_img(self, resources_table: &ResourceTable) -> crate::Result>> { + match self { + Self::Resource(rid) => resources_table.get::>(rid), + #[cfg(any(feature = "image-ico", feature = "image-png"))] + Self::Path(path) => Image::from_path(path).map(Arc::new), + + #[cfg(any(feature = "image-ico", feature = "image-png"))] + Self::Bytes(bytes) => Image::from_bytes(&bytes).map(Arc::new), + + Self::Rgba { + rgba, + width, + height, + } => Ok(Arc::new(Image::new_owned(rgba, width, height))), + + #[cfg(not(any(feature = "image-ico", feature = "image-png")))] + _ => Err( + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!( + "expected RGBA image data, found {}", + match self { + JsImage::Path(_) => "a file path", + JsImage::Bytes(_) => "raw bytes", + _ => unreachable!(), + } + ), + ) + .into(), + ), + } + } +} + +#[cfg(all(test, windows))] +mod tests { + use super::{IconResource, Image, default_window_icon_from_app_icon_resource}; + + /// The test executable has no icon resources, so every lookup must fail with an error + /// (instead of panicking or returning a stretched placeholder). + #[test] + fn from_icon_resource_missing_resource_is_an_error() { + for resource in [ + IconResource::Id(u16::MAX), + IconResource::Name("tauri-image-test-missing-icon"), + ] { + let error = Image::from_icon_resource(resource, 32, 32).unwrap_err(); + assert!( + matches!(error, crate::Error::ImageFromResource(_)), + "{resource:?}: {error:?}" + ); + } + + assert!(default_window_icon_from_app_icon_resource().is_none()); + } + + #[test] + fn from_icon_resource_rejects_invalid_sizes() { + for (width, height) in [(0, 32), (32, 0), (u32::MAX, 32), (32, i32::MAX as u32 + 1)] { + let error = Image::from_icon_resource(1, width, height).unwrap_err(); + assert!( + matches!(error, crate::Error::ImageFromResource(_)), + "{width}x{height}: {error:?}" + ); + } + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/image/plugin.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/image/plugin.rs new file mode 100644 index 000000000..87ac6e722 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/image/plugin.rs @@ -0,0 +1,88 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::Serialize; + +use crate::Manager; +use crate::plugin::{Builder, TauriPlugin}; +use crate::{ResourceId, Runtime, Webview, command, image::Image}; + +#[command(root = "crate")] +fn new( + webview: Webview, + rgba: Vec, + width: u32, + height: u32, +) -> crate::Result { + let image = Image::new_owned(rgba, width, height); + let mut resources_table = webview.resources_table(); + let rid = resources_table.add(image); + Ok(rid) +} + +#[cfg(any(feature = "image-ico", feature = "image-png"))] +#[command(root = "crate")] +fn from_bytes(webview: Webview, bytes: Vec) -> crate::Result { + let image = Image::from_bytes(&bytes)?.to_owned(); + let mut resources_table = webview.resources_table(); + let rid = resources_table.add(image); + Ok(rid) +} + +#[cfg(not(any(feature = "image-ico", feature = "image-png")))] +#[command(root = "crate")] +fn from_bytes() -> std::result::Result<(), &'static str> { + Err("from_bytes is only supported if the `image-ico` or `image-png` Cargo features are enabled") +} + +#[cfg(any(feature = "image-ico", feature = "image-png"))] +#[command(root = "crate")] +fn from_path( + webview: Webview, + path: std::path::PathBuf, +) -> crate::Result { + let image = Image::from_path(path)?.to_owned(); + let mut resources_table = webview.resources_table(); + let rid = resources_table.add(image); + Ok(rid) +} + +#[cfg(not(any(feature = "image-ico", feature = "image-png")))] +#[command(root = "crate")] +fn from_path() -> std::result::Result<(), &'static str> { + Err("from_path is only supported if the `image-ico` or `image-png` Cargo features are enabled") +} + +#[command(root = "crate")] +fn rgba(webview: Webview, rid: ResourceId) -> crate::Result { + let resources_table = webview.resources_table(); + let image = resources_table.get::>(rid)?; + Ok(crate::ipc::Response::new(image.rgba().to_owned())) +} + +#[derive(Serialize)] +struct Size { + width: u32, + height: u32, +} + +#[command(root = "crate")] +fn size(webview: Webview, rid: ResourceId) -> crate::Result { + let resources_table = webview.resources_table(); + let image = resources_table.get::>(rid)?; + Ok(Size { + width: image.width(), + height: image.height(), + }) +} + +/// Initializes the plugin. +pub fn init() -> TauriPlugin { + Builder::new("image") + .invoke_handler(crate::generate_handler![ + #![plugin(image)] + new, from_bytes, from_path, rgba, size + ]) + .build() +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ios.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ios.rs new file mode 100644 index 000000000..bee7d0301 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ios.rs @@ -0,0 +1,49 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use swift_rs::{SRString, SwiftArg, swift}; + +use std::{ + ffi::c_void, + os::raw::{c_char, c_int, c_ulonglong}, +}; + +type PluginMessageCallbackFn = unsafe extern "C" fn(c_int, c_int, *const c_char); +pub struct PluginMessageCallback(pub PluginMessageCallbackFn); + +impl<'a> SwiftArg<'a> for PluginMessageCallback { + type ArgType = PluginMessageCallbackFn; + + unsafe fn as_arg(&'a self) -> Self::ArgType { + self.0 + } +} + +type ChannelSendDataCallbackFn = unsafe extern "C" fn(c_ulonglong, *const c_char); +pub struct ChannelSendDataCallback(pub ChannelSendDataCallbackFn); + +impl<'a> SwiftArg<'a> for ChannelSendDataCallback { + type ArgType = ChannelSendDataCallbackFn; + + unsafe fn as_arg(&'a self) -> Self::ArgType { + self.0 + } +} + +swift!(pub fn run_plugin_command( + id: i32, + name: &SRString, + method: &SRString, + data: &SRString, + callback: PluginMessageCallback, + send_channel_data_callback: ChannelSendDataCallback +)); +swift!(pub fn register_plugin( + name: &SRString, + plugin: *const c_void, + config: &SRString, + webview: *const c_void +)); +swift!(pub fn on_webview_created(webview: *const c_void, controller: *const c_void)); +swift!(pub fn log_stdout()); diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ipc/authority.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ipc/authority.rs new file mode 100644 index 000000000..f51575a47 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ipc/authority.rs @@ -0,0 +1,1617 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::collections::BTreeMap; +use std::fmt::{Debug, Display}; +use std::sync::Arc; + +use serde::de::DeserializeOwned; + +#[cfg(feature = "dynamic-acl")] +use tauri_utils::acl::capability::CapabilityFile; +#[cfg(any(feature = "dynamic-acl", debug_assertions))] +use tauri_utils::acl::manifest::Manifest; +use tauri_utils::acl::{ + APP_ACL_KEY, ExecutionContext, Value, + resolved::{Resolved, ResolvedCommand, ResolvedScope, ScopeKey}, +}; + +use url::Url; + +use crate::{AppHandle, Manager, StateManager, Webview}; +use crate::{Runtime, ipc::InvokeError, sealed::ManagerBase}; + +use super::{CommandArg, CommandItem}; + +/// The runtime authority used to authorize IPC execution based on the Access Control List. +pub struct RuntimeAuthority { + #[cfg(any(feature = "dynamic-acl", debug_assertions))] + acl: BTreeMap, + has_app_acl: bool, + allowed_commands: BTreeMap>, + denied_commands: BTreeMap>, + pub(crate) scope_manager: ScopeManager, +} + +/// The origin trying to access the IPC. +pub enum Origin { + /// Local app origin. + Local, + /// Remote origin. + Remote { + /// Remote URL. + url: Url, + }, +} + +impl Display for Origin { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Local => write!(f, "local"), + Self::Remote { url } => write!(f, "remote: {url}"), + } + } +} + +impl Origin { + fn matches(&self, context: &ExecutionContext) -> bool { + match (self, context) { + (Self::Local, ExecutionContext::Local) => true, + (Self::Remote { url }, ExecutionContext::Remote { url: url_pattern }) => { + url_pattern.test(url) + } + _ => false, + } + } +} + +/// This is used internally by [`crate::generate_handler!`] for constructing [`RuntimeAuthority`] +/// to only include the raw ACL when it's needed +/// +/// ## Stability +/// +/// The output of this macro is managed internally by Tauri, +/// and should not be accessed directly on normal applications. +/// It may have breaking changes in the future. +#[cfg(any(feature = "dynamic-acl", debug_assertions))] +#[doc(hidden)] +#[macro_export] +macro_rules! runtime_authority { + ($acl:expr, $resolved_acl:expr) => { + $crate::ipc::RuntimeAuthority::new($acl, $resolved_acl) + }; +} + +/// This is used internally by [`crate::generate_handler!`] for constructing [`RuntimeAuthority`] +/// to only include the raw ACL when it's needed +/// +/// ## Stability +/// +/// The output of this macro is managed internally by Tauri, +/// and should not be accessed directly on normal applications. +/// It may have breaking changes in the future. +#[cfg(not(any(feature = "dynamic-acl", debug_assertions)))] +#[doc(hidden)] +#[macro_export] +macro_rules! runtime_authority { + ($_acl:expr, $resolved_acl:expr) => { + $crate::ipc::RuntimeAuthority::new($resolved_acl) + }; +} + +impl RuntimeAuthority { + /// Construct a new [`RuntimeAuthority`] from the ACL + /// + /// **Please prefer using the [`runtime_authority`] macro instead of calling this directly** + #[doc(hidden)] + pub fn new( + #[cfg(any(feature = "dynamic-acl", debug_assertions))] acl: BTreeMap, + resolved_acl: Resolved, + ) -> Self { + let command_cache = resolved_acl + .command_scope + .keys() + .map(|key| (*key, StateManager::new())) + .collect(); + Self { + #[cfg(any(feature = "dynamic-acl", debug_assertions))] + acl, + has_app_acl: resolved_acl.has_app_acl, + allowed_commands: resolved_acl.allowed_commands, + denied_commands: resolved_acl.denied_commands, + scope_manager: ScopeManager { + command_scope: resolved_acl.command_scope, + global_scope: resolved_acl.global_scope, + command_cache, + global_scope_cache: StateManager::new(), + }, + } + } + + pub(crate) fn has_app_manifest(&self) -> bool { + self.has_app_acl + } + + #[doc(hidden)] + pub fn __allow_command(&mut self, command: String, context: ExecutionContext) { + self.allowed_commands.insert( + command, + vec![ResolvedCommand { + context, + windows: vec!["*".parse().unwrap()], + ..Default::default() + }], + ); + } + + /// Adds the given capability to the runtime authority. + #[cfg(feature = "dynamic-acl")] + pub fn add_capability(&mut self, capability: impl super::RuntimeCapability) -> crate::Result<()> { + self.add_capability_inner(capability.build()) + } + + #[cfg(feature = "dynamic-acl")] + fn add_capability_inner(&mut self, capability: CapabilityFile) -> crate::Result<()> { + let mut capabilities = BTreeMap::new(); + match capability { + CapabilityFile::Capability(c) => { + capabilities.insert(c.identifier.clone(), c); + } + + CapabilityFile::List(capabilities_list) + | CapabilityFile::NamedList { + capabilities: capabilities_list, + } => { + capabilities.extend( + capabilities_list + .into_iter() + .map(|c| (c.identifier.clone(), c)), + ); + } + } + + let resolved = Resolved::resolve_with_base_scope_id( + &self.acl, + capabilities, + tauri_utils::platform::Target::current(), + self.scope_manager.last_scope_id(), + ) + .unwrap(); + + // fill global scope + for (plugin, global_scope) in resolved.global_scope { + let global_scope_entry = self.scope_manager.global_scope.entry(plugin).or_default(); + + global_scope_entry.allow.extend(global_scope.allow); + global_scope_entry.deny.extend(global_scope.deny); + + self.scope_manager.global_scope_cache = StateManager::new(); + } + + // fill command scope + // the scope ids were assigned past the existing ones, so these are all new entries + for (scope_id, command_scope) in resolved.command_scope { + self + .scope_manager + .command_cache + .entry(scope_id) + .or_insert_with(StateManager::new); + self + .scope_manager + .command_scope + .insert(scope_id, command_scope); + } + + // denied commands + for (cmd_key, resolved_cmds) in resolved.denied_commands { + let entry = self.denied_commands.entry(cmd_key).or_default(); + entry.extend(resolved_cmds); + } + + // allowed commands + for (cmd_key, resolved_cmds) in resolved.allowed_commands { + let entry = self.allowed_commands.entry(cmd_key).or_default(); + entry.extend(resolved_cmds); + } + + Ok(()) + } + + #[cfg(debug_assertions)] + pub(crate) fn resolve_access_message( + &self, + key: &str, + command_name: &str, + window: &str, + webview: &str, + origin: &Origin, + ) -> String { + fn print_references(resolved: &[ResolvedCommand]) -> String { + resolved + .iter() + .map(|r| { + format!( + "capability: {}, permission: {}", + r.referenced_by.capability, r.referenced_by.permission + ) + }) + .collect::>() + .join(" || ") + } + + fn print_allowed_on(resolved: &[ResolvedCommand]) -> String { + if resolved.is_empty() { + "command not allowed on any window/webview/URL context".to_string() + } else { + let mut s = "allowed on: ".to_string(); + + let last_index = resolved.len() - 1; + for (index, cmd) in resolved.iter().enumerate() { + let windows = cmd + .windows + .iter() + .map(|w| format!("\"{}\"", w.as_str())) + .collect::>() + .join(", "); + let webviews = cmd + .webviews + .iter() + .map(|w| format!("\"{}\"", w.as_str())) + .collect::>() + .join(", "); + + s.push('['); + + if !windows.is_empty() { + s.push_str(&format!("windows: {windows}, ")); + } + + if !webviews.is_empty() { + s.push_str(&format!("webviews: {webviews}, ")); + } + + match &cmd.context { + ExecutionContext::Local => s.push_str("URL: local"), + ExecutionContext::Remote { url } => s.push_str(&format!("URL: {}", url.as_str())), + } + + s.push(']'); + + if index != last_index { + s.push_str(", "); + } + } + + s + } + } + + fn has_permissions_allowing_command( + manifest: &Manifest, + set: &crate::utils::acl::PermissionSet, + command: &str, + ) -> bool { + for permission_id in &set.permissions { + if permission_id == "default" { + if let Some(default) = &manifest.default_permission { + if has_permissions_allowing_command(manifest, default, command) { + return true; + } + } + } else if let Some(ref_set) = manifest.permission_sets.get(permission_id) { + if has_permissions_allowing_command(manifest, ref_set, command) { + return true; + } + } else if let Some(permission) = manifest.permissions.get(permission_id) { + if permission.commands.allow.contains(&command.into()) { + return true; + } + } + } + false + } + + let command = if key == APP_ACL_KEY { + command_name.to_string() + } else { + format!("plugin:{key}|{command_name}") + }; + + let command_pretty_name = if key == APP_ACL_KEY { + command_name.to_string() + } else { + format!("{key}.{command_name}") + }; + + let denied_on_origin = self + .denied_on(&command, origin) + .cloned() + .collect::>(); + + if !denied_on_origin.is_empty() { + format!( + "{command_pretty_name} explicitly denied on origin {origin}\n\nreferenced by: {}", + print_references(&denied_on_origin) + ) + } else { + let command_matches = self.allowed_commands.get(&command); + + if let Some(resolved) = self.allowed_commands.get(&command) { + let resolved_matching_origin = resolved + .iter() + .filter(|cmd| origin.matches(&cmd.context)) + .collect::>(); + if resolved_matching_origin + .iter() + .any(|cmd| cmd.webviews.iter().any(|w| w.matches(webview))) + || resolved_matching_origin + .iter() + .any(|cmd| cmd.windows.iter().any(|w| w.matches(window))) + { + "allowed".to_string() + } else { + format!( + "{command_pretty_name} not allowed on window \"{window}\", webview \"{webview}\", URL: {}\n\n{}\n\nreferenced by: {}", + match origin { + Origin::Local => "local", + Origin::Remote { url } => url.as_str(), + }, + print_allowed_on(resolved), + print_references(resolved) + ) + } + } else { + let permission_error_detail = if let Some((key, manifest)) = self + .acl + .get_key_value(key) + .or_else(|| self.acl.get_key_value(&format!("core:{key}"))) + { + let mut permissions_referencing_command = Vec::new(); + + if let Some(default) = &manifest.default_permission { + if has_permissions_allowing_command(manifest, default, command_name) { + permissions_referencing_command.push("default".into()); + } + } + for set in manifest.permission_sets.values() { + if has_permissions_allowing_command(manifest, set, command_name) { + permissions_referencing_command.push(set.identifier.clone()); + } + } + for permission in manifest.permissions.values() { + if permission.commands.allow.contains(&command_name.into()) { + permissions_referencing_command.push(permission.identifier.clone()); + } + } + + permissions_referencing_command.sort(); + + let associated_permissions = permissions_referencing_command + .into_iter() + .map(|permission| { + if key == APP_ACL_KEY { + permission + } else { + format!("{key}:{permission}") + } + }) + .collect::>() + .join(", "); + + if associated_permissions.is_empty() { + "Command not found".to_string() + } else { + format!("Permissions associated with this command: {associated_permissions}") + } + } else { + "Plugin not found".to_string() + }; + + if let Some(resolved_cmds) = command_matches { + format!( + "{command_pretty_name} not allowed on origin [{origin}]. Please create a capability that has this origin on the context field.\n\nFound matches for: {}\n\n{permission_error_detail}", + resolved_cmds + .iter() + .map(|resolved| { + let context = match &resolved.context { + ExecutionContext::Local => "[local]".to_string(), + ExecutionContext::Remote { url } => format!("[remote: {}]", url.as_str()), + }; + format!( + "- context: {context}, referenced by: capability: {}, permission: {}", + resolved.referenced_by.capability, resolved.referenced_by.permission + ) + }) + .collect::>() + .join("\n") + ) + } else { + format!("{command_pretty_name} not allowed. {permission_error_detail}") + } + } + } + } + + /// Returns the deny entries of the given command whose execution context matches the given origin. + /// + /// Deny entries carry the [`ExecutionContext`] of the capability that defined them, + /// so a command denied by a remote capability is not denied for the local app and vice-versa. + fn denied_on<'a>( + &'a self, + command: &str, + origin: &'a Origin, + ) -> impl Iterator + use<'a> { + self + .denied_commands + .get(command) + .into_iter() + .flatten() + .filter(move |cmd| origin.matches(&cmd.context)) + } + + /// Checks if the given command is explicitly denied for the given origin. + fn is_denied(&self, command: &str, origin: &Origin) -> bool { + self.denied_on(command, origin).next().is_some() + } + + /// Checks if the given IPC execution is allowed and returns the [`ResolvedCommand`] if it is. + /// + /// A command is denied when a capability that denies it matches the given origin + /// (execution context), window and webview labels are not taken into account for denial. + pub fn resolve_access( + &self, + command: &str, + window: &str, + webview: &str, + origin: &Origin, + ) -> Option> { + if self.is_denied(command, origin) { + None + } else { + self.allowed_commands.get(command).and_then(|resolved| { + let resolved_cmds = resolved + .iter() + .filter(|cmd| { + origin.matches(&cmd.context) + && (cmd.webviews.iter().any(|w| w.matches(webview)) + || cmd.windows.iter().any(|w| w.matches(window))) + }) + .cloned() + .collect::>(); + if resolved_cmds.is_empty() { + None + } else { + Some(resolved_cmds) + } + }) + } + } +} + +/// List of allowed and denied objects that match either the command-specific or plugin global scope criteria. +#[derive(Debug)] +pub struct ScopeValue { + allow: Arc>>, + deny: Arc>>, +} + +impl ScopeValue { + fn clone(&self) -> Self { + Self { + allow: self.allow.clone(), + deny: self.deny.clone(), + } + } + + /// What this access scope allows. + pub fn allows(&self) -> &Vec> { + &self.allow + } + + /// What this access scope denies. + pub fn denies(&self) -> &Vec> { + &self.deny + } +} + +/// Access scope for a command that can be retrieved directly in the command function. +#[derive(Debug)] +pub struct CommandScope { + allow: Vec>, + deny: Vec>, +} + +impl CommandScope { + pub(crate) fn resolve( + webview: &Webview, + scope_ids: Vec, + ) -> crate::Result { + let mut allow = Vec::new(); + let mut deny = Vec::new(); + + for scope_id in scope_ids { + let scope = webview + .manager() + .runtime_authority + .lock() + .unwrap() + .scope_manager + .get_command_scope_typed::(webview.app_handle(), &scope_id)?; + + for s in scope.allows() { + allow.push(s.clone()); + } + for s in scope.denies() { + deny.push(s.clone()); + } + } + + Ok(CommandScope { allow, deny }) + } + + /// What this access scope allows. + pub fn allows(&self) -> &Vec> { + &self.allow + } + + /// What this access scope denies. + pub fn denies(&self) -> &Vec> { + &self.deny + } +} + +impl CommandScope { + /// Ensure all deny scopes were not matched and any allow scopes were. + /// + /// This **WILL** return `true` if the allow scopes are empty and the deny + /// scopes did not trigger. If you require at least one allow scope, then + /// ensure the allow scopes are not empty before calling this method. + /// + /// ``` + /// # use tauri::ipc::CommandScope; + /// # fn command(scope: CommandScope<()>) -> Result<(), &'static str> { + /// if scope.allows().is_empty() { + /// return Err("you need to specify at least 1 allow scope!"); + /// } + /// # Ok(()) + /// # } + /// ``` + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use url::Url; + /// # use tauri::{ipc::{CommandScope, ScopeObjectMatch}, command}; + /// # + /// #[derive(Debug, Clone, Serialize, Deserialize)] + /// # pub struct Scope; + /// # + /// # impl ScopeObjectMatch for Scope { + /// # type Input = str; + /// # + /// # fn matches(&self, input: &str) -> bool { + /// # true + /// # } + /// # } + /// # + /// # fn do_work(_: String) -> Result { + /// # Ok("Output".into()) + /// # } + /// # + /// #[command] + /// fn my_command(scope: CommandScope, input: String) -> Result { + /// if scope.matches(&input) { + /// do_work(input) + /// } else { + /// Err("Scope didn't match input") + /// } + /// } + /// ``` + pub fn matches(&self, input: &T::Input) -> bool { + // first make sure the input doesn't match any existing deny scope + if self.deny.iter().any(|s| s.matches(input)) { + return false; + } + + // if there are allow scopes, ensure the input matches at least 1 + if self.allow.is_empty() { + true + } else { + self.allow.iter().any(|s| s.matches(input)) + } + } +} + +impl<'a, R: Runtime, T: ScopeObject> CommandArg<'a, R> for CommandScope { + /// Grabs the [`ResolvedScope`] from the [`CommandItem`] and returns the associated [`CommandScope`]. + fn from_command(command: CommandItem<'a, R>) -> Result { + if let Some(resolved) = &command.acl { + let scope_ids = resolved + .iter() + .filter_map(|cmd| cmd.scope_id) + .collect::>(); + CommandScope::resolve(&command.message.webview, scope_ids).map_err(Into::into) + } else { + Ok(CommandScope { + allow: Default::default(), + deny: Default::default(), + }) + } + } +} + +/// Global access scope that can be retrieved directly in the command function. +#[derive(Debug)] +pub struct GlobalScope(ScopeValue); + +impl GlobalScope { + pub(crate) fn resolve(webview: &Webview, plugin: &str) -> crate::Result { + webview + .manager() + .runtime_authority + .lock() + .unwrap() + .scope_manager + .get_global_scope_typed(webview.app_handle(), plugin) + .map(Self) + } + + /// What this access scope allows. + pub fn allows(&self) -> &Vec> { + &self.0.allow + } + + /// What this access scope denies. + pub fn denies(&self) -> &Vec> { + &self.0.deny + } +} + +impl<'a, R: Runtime, T: ScopeObject> CommandArg<'a, R> for GlobalScope { + /// Grabs the [`ResolvedScope`] from the [`CommandItem`] and returns the associated [`GlobalScope`]. + fn from_command(command: CommandItem<'a, R>) -> Result { + GlobalScope::resolve( + &command.message.webview, + command.plugin.unwrap_or(APP_ACL_KEY), + ) + .map_err(InvokeError::from_error) + } +} + +#[derive(Debug)] +pub struct ScopeManager { + command_scope: BTreeMap, + global_scope: BTreeMap, + command_cache: BTreeMap, + global_scope_cache: StateManager, +} + +/// Marks a type as a scope object. +/// +/// Usually you will just rely on [`serde::de::DeserializeOwned`] instead of implementing it manually, +/// though this is useful if you need to do some initialization logic on the type itself. +pub trait ScopeObject: Sized + Send + Sync + Debug + 'static { + /// The error type. + type Error: std::error::Error + Send + Sync; + /// Deserialize the raw scope value. + fn deserialize(app: &AppHandle, raw: Value) -> Result; +} + +impl ScopeObject for T { + type Error = serde_json::Error; + fn deserialize(_app: &AppHandle, raw: Value) -> Result { + serde_json::from_value(raw.into()) + } +} + +/// A [`ScopeObject`] whose validation can be represented as a `bool`. +/// +/// # Example +/// +/// ``` +/// # use serde::{Deserialize, Serialize}; +/// # use tauri::{ipc::ScopeObjectMatch, Url}; +/// # +/// #[derive(Debug, Clone, Serialize, Deserialize)] +/// #[serde(rename_all = "camelCase")] +/// pub enum Scope { +/// Domain(Url), +/// StartsWith(String), +/// } +/// +/// impl ScopeObjectMatch for Scope { +/// type Input = str; +/// +/// fn matches(&self, input: &str) -> bool { +/// match self { +/// Scope::Domain(url) => { +/// let parsed: Url = match input.parse() { +/// Ok(parsed) => parsed, +/// Err(_) => return false, +/// }; +/// +/// let domain = parsed.domain(); +/// +/// domain.is_some() && domain == url.domain() +/// } +/// Scope::StartsWith(start) => input.starts_with(start), +/// } +/// } +/// } +/// ``` +pub trait ScopeObjectMatch: ScopeObject { + /// The type of input expected to validate against the scope. + /// + /// This will be borrowed, so if you want to match on a `&str` this type should be `str`. + type Input: ?Sized; + + /// Check if the input matches against the scope. + fn matches(&self, input: &Self::Input) -> bool; +} + +impl ScopeManager { + /// The highest assigned command scope id, or `0` if there are none. + #[cfg(feature = "dynamic-acl")] + fn last_scope_id(&self) -> ScopeKey { + self.command_scope.keys().next_back().copied().unwrap_or(0) + } + + pub(crate) fn get_global_scope_typed( + &self, + app: &AppHandle, + key: &str, + ) -> crate::Result> { + match self.global_scope_cache.try_get::>() { + Some(cached) => Ok((*cached).clone()), + None => { + let mut allow = Vec::new(); + let mut deny = Vec::new(); + + if let Some(global_scope) = self.global_scope.get(key) { + for allowed in &global_scope.allow { + allow + .push(Arc::new(T::deserialize(app, allowed.clone()).map_err( + |e| crate::Error::CannotDeserializeScope(Box::new(e)), + )?)); + } + for denied in &global_scope.deny { + deny + .push(Arc::new(T::deserialize(app, denied.clone()).map_err( + |e| crate::Error::CannotDeserializeScope(Box::new(e)), + )?)); + } + } + + let scope = ScopeValue { + allow: Arc::new(allow), + deny: Arc::new(deny), + }; + self.global_scope_cache.set(scope.clone()); + Ok(scope) + } + } + } + + fn get_command_scope_typed( + &self, + app: &AppHandle, + key: &ScopeKey, + ) -> crate::Result> { + let cache = self.command_cache.get(key).unwrap(); + match cache.try_get::>() { + Some(cached) => Ok((*cached).clone()), + None => { + let resolved_scope = self + .command_scope + .get(key) + .unwrap_or_else(|| panic!("missing command scope for key {key}")); + + let mut allow = Vec::new(); + let mut deny = Vec::new(); + + for allowed in &resolved_scope.allow { + allow + .push(Arc::new(T::deserialize(app, allowed.clone()).map_err( + |e| crate::Error::CannotDeserializeScope(Box::new(e)), + )?)); + } + for denied in &resolved_scope.deny { + deny + .push(Arc::new(T::deserialize(app, denied.clone()).map_err( + |e| crate::Error::CannotDeserializeScope(Box::new(e)), + )?)); + } + + let value = ScopeValue { + allow: Arc::new(allow), + deny: Arc::new(deny), + }; + + let _ = cache.set(value.clone()); + Ok(value) + } + } + } +} + +#[cfg(test)] +mod tests { + use glob::Pattern; + use tauri_utils::acl::{ + ExecutionContext, + resolved::{Resolved, ResolvedCommand}, + }; + + use crate::ipc::Origin; + + use super::RuntimeAuthority; + + #[test] + fn window_glob_pattern_matches() { + let command = "my-command"; + let window = "main-*"; + let webview = "other-*"; + + let resolved_cmd = vec![ResolvedCommand { + windows: vec![Pattern::new(window).unwrap()], + ..Default::default() + }]; + let allowed_commands = [(command.to_string(), resolved_cmd.clone())] + .into_iter() + .collect(); + + let authority = RuntimeAuthority::new( + Default::default(), + Resolved { + allowed_commands, + ..Default::default() + }, + ); + + assert_eq!( + authority.resolve_access( + command, + &window.replace('*', "something"), + webview, + &Origin::Local + ), + Some(resolved_cmd) + ); + } + + #[test] + fn webview_glob_pattern_matches() { + let command = "my-command"; + let window = "other-*"; + let webview = "main-*"; + + let resolved_cmd = vec![ResolvedCommand { + windows: vec![Pattern::new(window).unwrap()], + webviews: vec![Pattern::new(webview).unwrap()], + ..Default::default() + }]; + let allowed_commands = [(command.to_string(), resolved_cmd.clone())] + .into_iter() + .collect(); + + let authority = RuntimeAuthority::new( + Default::default(), + Resolved { + allowed_commands, + ..Default::default() + }, + ); + + assert_eq!( + authority.resolve_access( + command, + window, + &webview.replace('*', "something"), + &Origin::Local + ), + Some(resolved_cmd) + ); + } + + #[test] + fn remote_domain_matches() { + let url = "https://tauri.app"; + let command = "my-command"; + let window = "main"; + let webview = "main"; + + let resolved_cmd = vec![ResolvedCommand { + windows: vec![Pattern::new(window).unwrap()], + context: ExecutionContext::Remote { + url: url.parse().unwrap(), + }, + ..Default::default() + }]; + let allowed_commands = [(command.to_string(), resolved_cmd.clone())] + .into_iter() + .collect(); + + let authority = RuntimeAuthority::new( + Default::default(), + Resolved { + allowed_commands, + ..Default::default() + }, + ); + + assert_eq!( + authority.resolve_access( + command, + window, + webview, + &Origin::Remote { + url: url.parse().unwrap() + } + ), + Some(resolved_cmd) + ); + } + + #[test] + fn remote_domain_glob_pattern_matches() { + let url = "http://tauri.*"; + let command = "my-command"; + let window = "main"; + let webview = "main"; + + let resolved_cmd = vec![ResolvedCommand { + windows: vec![Pattern::new(window).unwrap()], + context: ExecutionContext::Remote { + url: url.parse().unwrap(), + }, + ..Default::default() + }]; + let allowed_commands = [(command.to_string(), resolved_cmd.clone())] + .into_iter() + .collect(); + + let authority = RuntimeAuthority::new( + Default::default(), + Resolved { + allowed_commands, + ..Default::default() + }, + ); + + assert_eq!( + authority.resolve_access( + command, + window, + webview, + &Origin::Remote { + url: url.replace('*', "studio").parse().unwrap() + } + ), + Some(resolved_cmd) + ); + } + + #[test] + fn remote_context_denied() { + let command = "my-command"; + let window = "main"; + let webview = "main"; + + let resolved_cmd = vec![ResolvedCommand { + windows: vec![Pattern::new(window).unwrap()], + ..Default::default() + }]; + let allowed_commands = [(command.to_string(), resolved_cmd)].into_iter().collect(); + + let authority = RuntimeAuthority::new( + Default::default(), + Resolved { + allowed_commands, + ..Default::default() + }, + ); + + assert!( + authority + .resolve_access( + command, + window, + webview, + &Origin::Remote { + url: "https://tauri.app".parse().unwrap() + } + ) + .is_none() + ); + } + + #[test] + fn denied_command_takes_precedence() { + let command = "my-command"; + let window = "main"; + let webview = "main"; + let windows = vec![Pattern::new(window).unwrap()]; + let allowed_commands = [( + command.to_string(), + vec![ResolvedCommand { + windows: windows.clone(), + ..Default::default() + }], + )] + .into_iter() + .collect(); + let denied_commands = [( + command.to_string(), + vec![ResolvedCommand { + windows, + ..Default::default() + }], + )] + .into_iter() + .collect(); + + let authority = RuntimeAuthority::new( + Default::default(), + Resolved { + allowed_commands, + denied_commands, + ..Default::default() + }, + ); + + assert!( + authority + .resolve_access(command, window, webview, &Origin::Local) + .is_none() + ); + } + + #[test] + fn denied_command_is_scoped_to_the_execution_context() { + let url = "https://tauri.app"; + let command = "my-command"; + let window = "main"; + let webview = "main"; + let windows = vec![Pattern::new(window).unwrap()]; + + // the command is allowed on both the local app and the remote URL + let allowed_commands = [( + command.to_string(), + vec![ + ResolvedCommand { + windows: windows.clone(), + ..Default::default() + }, + ResolvedCommand { + windows: windows.clone(), + context: ExecutionContext::Remote { + url: url.parse().unwrap(), + }, + ..Default::default() + }, + ], + )] + .into_iter() + .collect(); + + // but it is only denied on the remote URL + let denied_commands = [( + command.to_string(), + vec![ResolvedCommand { + windows, + context: ExecutionContext::Remote { + url: url.parse().unwrap(), + }, + ..Default::default() + }], + )] + .into_iter() + .collect(); + + let authority = RuntimeAuthority::new( + Default::default(), + Resolved { + allowed_commands, + denied_commands, + ..Default::default() + }, + ); + + // the remote origin the deny entry was defined for is denied + assert!( + authority + .resolve_access( + command, + window, + webview, + &Origin::Remote { + url: url.parse().unwrap() + } + ) + .is_none() + ); + + // the local app must not be affected by a deny entry of a remote capability + let resolved = authority + .resolve_access(command, window, webview, &Origin::Local) + .expect("local origin must not be denied by a remote capability"); + assert_eq!(resolved.len(), 1); + assert_eq!(resolved[0].context, ExecutionContext::Local); + } + + #[test] + fn denied_command_on_local_context_does_not_deny_remote() { + let url = "https://tauri.app"; + let command = "my-command"; + let window = "main"; + let webview = "main"; + let windows = vec![Pattern::new(window).unwrap()]; + + // the command is allowed on both the local app and the remote URL + let allowed_commands = [( + command.to_string(), + vec![ + ResolvedCommand { + windows: windows.clone(), + ..Default::default() + }, + ResolvedCommand { + windows: windows.clone(), + context: ExecutionContext::Remote { + url: url.parse().unwrap(), + }, + ..Default::default() + }, + ], + )] + .into_iter() + .collect(); + + // but it is only denied on the local app + let denied_commands = [( + command.to_string(), + vec![ResolvedCommand { + windows, + ..Default::default() + }], + )] + .into_iter() + .collect(); + + let authority = RuntimeAuthority::new( + Default::default(), + Resolved { + allowed_commands, + denied_commands, + ..Default::default() + }, + ); + + // the remote origin must not be affected by a deny entry of a local capability + let resolved = authority + .resolve_access( + command, + window, + webview, + &Origin::Remote { + url: url.parse().unwrap(), + }, + ) + .expect("remote origin must not be denied by a local capability"); + assert_eq!(resolved.len(), 1); + assert_eq!( + resolved[0].context, + ExecutionContext::Remote { + url: url.parse().unwrap() + } + ); + + // the local app is denied even though it has an allow entry + assert!( + authority + .resolve_access(command, window, webview, &Origin::Local) + .is_none() + ); + } + + #[cfg(debug_assertions)] + #[test] + fn resolve_access_message_denied_on_origin() { + use tauri_utils::acl::resolved::ResolvedCommandReference; + + let plugin_name = "myplugin"; + let command_name = "my-command"; + let command = format!("plugin:{plugin_name}|{command_name}"); + let window = "main"; + let webview = "main"; + let remote_url = "http://localhost:8080"; + let windows = vec![Pattern::new(window).unwrap()]; + + let remote_context = ExecutionContext::Remote { + url: remote_url.parse().unwrap(), + }; + + let allowed_commands = [( + command.clone(), + vec![ + ResolvedCommand { + windows: windows.clone(), + referenced_by: ResolvedCommandReference { + capability: "maincap".to_string(), + permission: "allow-command".to_string(), + }, + ..Default::default() + }, + ResolvedCommand { + windows: windows.clone(), + context: remote_context.clone(), + referenced_by: ResolvedCommandReference { + capability: "remotecap".to_string(), + permission: "allow-command".to_string(), + }, + ..Default::default() + }, + ], + )] + .into_iter() + .collect(); + + // one capability denies the command locally, another denies it on the remote URL + let denied_commands = [( + command, + vec![ + ResolvedCommand { + windows: windows.clone(), + referenced_by: ResolvedCommandReference { + capability: "localcap".to_string(), + permission: "deny-command".to_string(), + }, + ..Default::default() + }, + ResolvedCommand { + windows, + context: remote_context, + referenced_by: ResolvedCommandReference { + capability: "remotecap".to_string(), + permission: "deny-command".to_string(), + }, + ..Default::default() + }, + ], + )] + .into_iter() + .collect(); + + let authority = RuntimeAuthority::new( + Default::default(), + Resolved { + allowed_commands, + denied_commands, + ..Default::default() + }, + ); + + // only the capability denying the local context is referenced + assert_eq!( + authority.resolve_access_message(plugin_name, command_name, window, webview, &Origin::Local), + "myplugin.my-command explicitly denied on origin local\n\nreferenced by: capability: localcap, permission: deny-command" + ); + + // only the capability denying the remote URL is referenced + assert_eq!( + authority.resolve_access_message( + plugin_name, + command_name, + window, + webview, + &Origin::Remote { + url: remote_url.parse().unwrap() + } + ), + "myplugin.my-command explicitly denied on origin remote: http://localhost:8080/\n\nreferenced by: capability: remotecap, permission: deny-command" + ); + + // a remote URL that matches no deny entry is not reported as explicitly denied + let message = authority.resolve_access_message( + plugin_name, + command_name, + window, + webview, + &Origin::Remote { + url: "http://localhost:123".parse().unwrap(), + }, + ); + assert!( + !message.contains("explicitly denied"), + "unexpected message: {message}" + ); + assert!( + message.starts_with("myplugin.my-command not allowed"), + "unexpected message: {message}" + ); + } + + #[cfg(debug_assertions)] + #[test] + fn resolve_access_message() { + use tauri_utils::acl::manifest::Manifest; + + let plugin_name = "myplugin"; + let command_allowed_on_window = "my-command-window"; + let command_allowed_on_webview_window = "my-command-webview-window"; + let window = "main-*"; + let webview = "webview-*"; + let remote_url = "http://localhost:8080"; + + let referenced_by = tauri_utils::acl::resolved::ResolvedCommandReference { + capability: "maincap".to_string(), + permission: "allow-command".to_string(), + }; + + let resolved_window_cmd = ResolvedCommand { + windows: vec![Pattern::new(window).unwrap()], + referenced_by: referenced_by.clone(), + ..Default::default() + }; + let resolved_webview_window_cmd = ResolvedCommand { + windows: vec![Pattern::new(window).unwrap()], + webviews: vec![Pattern::new(webview).unwrap()], + referenced_by: referenced_by.clone(), + ..Default::default() + }; + let resolved_webview_window_remote_cmd = ResolvedCommand { + windows: vec![Pattern::new(window).unwrap()], + webviews: vec![Pattern::new(webview).unwrap()], + referenced_by, + context: ExecutionContext::Remote { + url: remote_url.parse().unwrap(), + }, + ..Default::default() + }; + + let allowed_commands = [ + ( + format!("plugin:{plugin_name}|{command_allowed_on_window}"), + vec![resolved_window_cmd], + ), + ( + format!("plugin:{plugin_name}|{command_allowed_on_webview_window}"), + vec![ + resolved_webview_window_cmd, + resolved_webview_window_remote_cmd, + ], + ), + ] + .into_iter() + .collect(); + + let authority = RuntimeAuthority::new( + [( + plugin_name.to_string(), + Manifest { + default_permission: None, + permissions: Default::default(), + permission_sets: Default::default(), + global_scope_schema: None, + }, + )] + .into_iter() + .collect(), + Resolved { + allowed_commands, + ..Default::default() + }, + ); + + // unknown plugin + assert_eq!( + authority.resolve_access_message( + "unknown-plugin", + command_allowed_on_window, + window, + webview, + &Origin::Local + ), + "unknown-plugin.my-command-window not allowed. Plugin not found" + ); + + // unknown command + assert_eq!( + authority.resolve_access_message( + plugin_name, + "unknown-command", + window, + webview, + &Origin::Local + ), + "myplugin.unknown-command not allowed. Command not found" + ); + + // window/webview do not match + assert_eq!( + authority.resolve_access_message( + plugin_name, + command_allowed_on_window, + "other-window", + "any-webview", + &Origin::Local + ), + "myplugin.my-command-window not allowed on window \"other-window\", webview \"any-webview\", URL: local\n\nallowed on: [windows: \"main-*\", URL: local]\n\nreferenced by: capability: maincap, permission: allow-command" + ); + + // window matches, but not origin + assert_eq!( + authority.resolve_access_message( + plugin_name, + command_allowed_on_window, + window, + "any-webview", + &Origin::Remote { + url: "http://localhst".parse().unwrap() + } + ), + "myplugin.my-command-window not allowed on window \"main-*\", webview \"any-webview\", URL: http://localhst/\n\nallowed on: [windows: \"main-*\", URL: local]\n\nreferenced by: capability: maincap, permission: allow-command" + ); + + // window/webview do not match + assert_eq!( + authority.resolve_access_message( + plugin_name, + command_allowed_on_webview_window, + "other-window", + "other-webview", + &Origin::Local + ), + "myplugin.my-command-webview-window not allowed on window \"other-window\", webview \"other-webview\", URL: local\n\nallowed on: [windows: \"main-*\", webviews: \"webview-*\", URL: local], [windows: \"main-*\", webviews: \"webview-*\", URL: http://localhost:8080]\n\nreferenced by: capability: maincap, permission: allow-command || capability: maincap, permission: allow-command" + ); + + // window/webview matches, but not origin + assert_eq!( + authority.resolve_access_message( + plugin_name, + command_allowed_on_webview_window, + window, + webview, + &Origin::Remote { + url: "http://localhost:123".parse().unwrap() + } + ), + "myplugin.my-command-webview-window not allowed on window \"main-*\", webview \"webview-*\", URL: http://localhost:123/\n\nallowed on: [windows: \"main-*\", webviews: \"webview-*\", URL: local], [windows: \"main-*\", webviews: \"webview-*\", URL: http://localhost:8080]\n\nreferenced by: capability: maincap, permission: allow-command || capability: maincap, permission: allow-command" + ); + } + + #[cfg(feature = "dynamic-acl")] + #[test] + fn add_capability_assigns_fresh_scope_ids() { + use serde_json::json; + use std::collections::{BTreeMap, BTreeSet}; + use tauri_utils::acl::{ + Permission, + capability::{Capability, CapabilityFile}, + manifest::Manifest, + }; + + fn manifest(permission: serde_json::Value) -> Manifest { + let permission: Permission = serde_json::from_value(permission).unwrap(); + Manifest { + permissions: [(permission.identifier.clone(), permission)].into(), + ..Default::default() + } + } + + fn capability(identifier: &str, permission: &str) -> Capability { + serde_json::from_value(json!({ + "identifier": identifier, + "windows": ["main"], + "permissions": [permission] + })) + .unwrap() + } + + fn scope_ids(authority: &RuntimeAuthority, command: &str) -> BTreeSet { + authority.allowed_commands[command] + .iter() + .filter_map(|cmd| cmd.scope_id) + .collect() + } + + // the scope values the command resolves to, see `CommandScope::resolve` + fn allowed_scope(authority: &RuntimeAuthority, command: &str) -> Vec { + scope_ids(authority, command) + .iter() + .flat_map(|id| &authority.scope_manager.command_scope[id].allow) + .cloned() + .map(serde_json::Value::from) + .collect() + } + + let acl: BTreeMap = [ + ( + "opener".to_string(), + manifest(json!({ + "identifier": "allow-open-path", + "commands": { "allow": ["open_path"] }, + "scope": { "allow": [{ "path": "/tmp/**" }] } + })), + ), + ( + "http".to_string(), + manifest(json!({ + "identifier": "allow-fetch", + "commands": { "allow": ["fetch", "fetch_send"] }, + "scope": { "allow": [{ "url": "https://example.com" }] } + })), + ), + ] + .into(); + + // build time ACL, the opener scope gets id 1 + let baked = Resolved::resolve( + &acl, + [( + "baked".to_string(), + capability("baked", "opener:allow-open-path"), + )] + .into(), + tauri_utils::platform::Target::current(), + ) + .unwrap(); + let mut authority = RuntimeAuthority::new(acl, baked); + assert_eq!(scope_ids(&authority, "plugin:opener|open_path"), [1].into()); + + // a runtime capability for another plugin must not be merged into scope 1 + authority + .add_capability_inner(CapabilityFile::Capability(capability( + "runtime", + "http:allow-fetch", + ))) + .unwrap(); + + assert_eq!( + authority + .scope_manager + .command_scope + .keys() + .copied() + .collect::>(), + vec![1, 2] + ); + assert!(authority.scope_manager.command_cache.contains_key(&2)); + assert_eq!( + allowed_scope(&authority, "plugin:opener|open_path"), + vec![json!({ "path": "/tmp/**" })] + ); + // fetch and fetch_send share the scope, and it must not be duplicated + assert_eq!(scope_ids(&authority, "plugin:http|fetch"), [2].into()); + assert_eq!(scope_ids(&authority, "plugin:http|fetch_send"), [2].into()); + assert_eq!( + allowed_scope(&authority, "plugin:http|fetch"), + vec![json!({ "url": "https://example.com" })] + ); + + // the next runtime capability continues past the previous one + authority + .add_capability_inner(CapabilityFile::Capability(capability( + "runtime-2", + "opener:allow-open-path", + ))) + .unwrap(); + assert_eq!( + authority + .scope_manager + .command_scope + .keys() + .copied() + .collect::>(), + vec![1, 2, 3] + ); + assert!(authority.scope_manager.command_cache.contains_key(&3)); + assert_eq!( + scope_ids(&authority, "plugin:opener|open_path"), + [1, 3].into() + ); + assert_eq!(scope_ids(&authority, "plugin:http|fetch"), [2].into()); + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ipc/capability_builder.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ipc/capability_builder.rs new file mode 100644 index 000000000..53dfcc45b --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ipc/capability_builder.rs @@ -0,0 +1,170 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::Serialize; +use tauri_utils::{ + acl::{ + Scopes, + capability::{Capability, CapabilityFile, PermissionEntry}, + }, + platform::Target, +}; + +/// A capability that can be added at runtime. +pub trait RuntimeCapability { + /// Creates the capability file. + fn build(self) -> CapabilityFile; +} + +impl> RuntimeCapability for T { + fn build(self) -> CapabilityFile { + self.as_ref().parse().expect("invalid capability") + } +} + +/// A builder for a [`Capability`]. +pub struct CapabilityBuilder(Capability); + +impl CapabilityBuilder { + /// Creates a new capability builder with a unique identifier. + pub fn new(identifier: impl Into) -> Self { + Self(Capability { + identifier: identifier.into(), + description: "".into(), + remote: None, + local: true, + windows: Vec::new(), + webviews: Vec::new(), + permissions: Vec::new(), + platforms: None, + }) + } + + /// Allows this capability to be used by a remote URL. + pub fn remote(mut self, url: String) -> Self { + self + .0 + .remote + .get_or_insert_with(Default::default) + .urls + .push(url); + self + } + + /// Whether this capability is applied on local app URLs or not. Defaults to `true`. + pub fn local(mut self, local: bool) -> Self { + self.0.local = local; + self + } + + /// Link this capability to the given window label. + pub fn window(mut self, window: impl Into) -> Self { + self.0.windows.push(window.into()); + self + } + + /// Link this capability to the a list of window labels. + pub fn windows(mut self, windows: impl IntoIterator>) -> Self { + self.0.windows.extend(windows.into_iter().map(|w| w.into())); + self + } + + /// Link this capability to the given webview label. + pub fn webview(mut self, webview: impl Into) -> Self { + self.0.webviews.push(webview.into()); + self + } + + /// Link this capability to the a list of window labels. + pub fn webviews(mut self, webviews: impl IntoIterator>) -> Self { + self + .0 + .webviews + .extend(webviews.into_iter().map(|w| w.into())); + self + } + + /// Add a new permission to this capability. + pub fn permission(mut self, permission: impl Into) -> Self { + let permission = permission.into(); + self.0.permissions.push(PermissionEntry::PermissionRef( + permission + .clone() + .try_into() + .unwrap_or_else(|_| panic!("invalid permission identifier '{permission}'")), + )); + self + } + + /// Add a new scoped permission to this capability. + pub fn permission_scoped( + mut self, + permission: impl Into, + allowed: Vec, + denied: Vec, + ) -> Self { + let permission = permission.into(); + let identifier = permission + .clone() + .try_into() + .unwrap_or_else(|_| panic!("invalid permission identifier '{permission}'")); + + let allowed_scope = allowed + .into_iter() + .map(|a| { + serde_json::to_value(a) + .expect("failed to serialize scope") + .into() + }) + .collect(); + let denied_scope = denied + .into_iter() + .map(|a| { + serde_json::to_value(a) + .expect("failed to serialize scope") + .into() + }) + .collect(); + let scope = Scopes { + allow: Some(allowed_scope), + deny: Some(denied_scope), + }; + + self + .0 + .permissions + .push(PermissionEntry::ExtendedPermission { identifier, scope }); + self + } + + /// Adds a target platform for this capability. + /// + /// By default all platforms are applied. + pub fn platform(mut self, platform: Target) -> Self { + self + .0 + .platforms + .get_or_insert_with(Default::default) + .push(platform); + self + } + + /// Adds target platforms for this capability. + /// + /// By default all platforms are applied. + pub fn platforms(mut self, platforms: impl IntoIterator) -> Self { + self + .0 + .platforms + .get_or_insert_with(Default::default) + .extend(platforms); + self + } +} + +impl RuntimeCapability for CapabilityBuilder { + fn build(self) -> CapabilityFile { + CapabilityFile::Capability(self.0) + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ipc/channel.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ipc/channel.rs new file mode 100644 index 000000000..d008fa2e6 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ipc/channel.rs @@ -0,0 +1,609 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + collections::HashMap, + str::FromStr, + sync::{ + Arc, Mutex, + atomic::{AtomicU32, AtomicUsize, Ordering}, + }, +}; + +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use crate::{ + Manager, Runtime, State, Webview, command, + ipc::{CommandArg, CommandItem}, + plugin::{Builder as PluginBuilder, TauriPlugin}, +}; + +use super::{ + CallbackFn, InvokeError, InvokeResponseBody, IpcResponse, Request, Response, + format_callback::format_raw_js, +}; + +pub const IPC_PAYLOAD_PREFIX: &str = "__CHANNEL__:"; +// TODO: Change this to `channel` in v3 +pub const CHANNEL_PLUGIN_NAME: &str = "__TAURI_CHANNEL__"; +// TODO: Change this to `plugin:channel|fetch` in v3 +pub const FETCH_CHANNEL_DATA_COMMAND: &str = "plugin:__TAURI_CHANNEL__|fetch"; +const CHANNEL_ID_HEADER_NAME: &str = "Tauri-Channel-Id"; + +/// Maximum size a JSON we should send directly without going through the fetch process +// 8192 byte JSON payload runs roughly 2x faster through eval than through fetch on WebView2 v135 +const MAX_JSON_DIRECT_EXECUTE_THRESHOLD: usize = 8192; +// 1024 byte payload runs roughly 30% faster through eval than through fetch on macOS +const MAX_RAW_DIRECT_EXECUTE_THRESHOLD: usize = 1024; + +static CHANNEL_COUNTER: AtomicU32 = AtomicU32::new(0); + +/// Maps channel ids to pending data that must be sent to the JavaScript side via the IPC. +/// +/// Scoped per webview: each webview has its own id sequence and lookups only +/// ever touch the calling webview's entries. +#[derive(Default, Clone)] +pub struct ChannelDataIpcQueue(Arc>>); + +/// Pending channel data of one webview. +#[derive(Default)] +struct WebviewChannelDataQueue { + next_id: u32, + entries: HashMap, +} + +impl ChannelDataIpcQueue { + /// Stores the body for the given webview and returns its id. + /// + /// Ids are sequential per webview and only address that webview's entries. + #[cfg(test)] + fn insert(&self, webview_label: &str, body: InvokeResponseBody) -> u32 { + self.insert_if(webview_label, body, || true).unwrap() + } + + /// Stores the body for the given webview and returns its id, unless `is_alive` returns false. + /// + /// `is_alive` runs under the queue lock, so a webview closing concurrently either fails the check + /// or has the entry purged by [`Self::remove_webview_entries`] right after. + fn insert_if( + &self, + webview_label: &str, + body: InvokeResponseBody, + is_alive: impl FnOnce() -> bool, + ) -> Option { + let mut cache = self.0.lock().unwrap(); + if !is_alive() { + return None; + } + let queue = cache.entry(webview_label.to_string()).or_default(); + let data_id = loop { + let candidate = queue.next_id; + queue.next_id = queue.next_id.wrapping_add(1); + if !queue.entries.contains_key(&candidate) { + break candidate; + } + }; + queue.entries.insert(data_id, body); + Some(data_id) + } + + /// Removes and returns the entry with the given id from the given webview's queue. + fn remove(&self, webview_label: &str, data_id: u32) -> Option { + self + .0 + .lock() + .unwrap() + .get_mut(webview_label)? + .entries + .remove(&data_id) + } + + /// Drops all entries of the given webview. + pub(crate) fn remove_webview_entries(&self, webview_label: &str) { + self.0.lock().unwrap().remove(webview_label); + } +} + +/// An IPC channel, used to stream values from Rust to the frontend. +/// +/// A command can only resolve once, so a channel is how you push an arbitrary number of messages +/// to the JavaScript side after the command returned: download progress, log lines, streamed +/// responses and so on. Each message is a `TSend` value, which must implement [`IpcResponse`] +/// (automatically implemented for every [`serde::Serialize`] type) and is delivered to the +/// `onmessage` handler of the matching `Channel` on the JavaScript side. +/// +/// The usual flow is to create the channel on the frontend and pass it as a command argument, +/// since [`Channel`] implements [`CommandArg`]: +/// +/// ```javascript +/// import { Channel, invoke } from '@tauri-apps/api/core' +/// +/// const onProgress = new Channel() +/// onProgress.onmessage = (message) => console.log(message) +/// await invoke('download', { url, onProgress }) +/// ``` +/// +/// A channel can also be created on the Rust side with [`Channel::new`], or resolved from an id +/// the frontend sent inside a bigger payload with [`JavaScriptChannelId::channel_on`]. +/// +/// Channels are cheap to clone (every clone refers to the same JavaScript callback) and can be +/// stored in the app state or moved to another thread to send messages later. When the last clone +/// of a channel created from the frontend is dropped, the JavaScript side is notified that no more +/// messages will arrive and the callback is unregistered. +/// +/// # Ordering +/// +/// The channel automatically orders the messages: every message carries the index it was sent with, +/// and the JavaScript side buffers out-of-order messages until the missing ones arrive, so the +/// `onmessage` handler always observes messages in the same order [`Channel::send`] was called. +/// See [`Builder::channel_interceptor`](crate::Builder::channel_interceptor) if you need to +/// intercept or replace this delivery mechanism. +/// +/// # Raw payloads +/// +/// Any [`serde::Serialize`] value is sent as JSON. To stream binary data without the JSON overhead, +/// send an [`InvokeResponseBody::Raw`] through a `Channel` (the default type +/// parameter) or a [`Response`]: the payload is then received in JavaScript as an `ArrayBuffer`. +/// Note that a `Channel>` does *not* do this - `Vec` is `Serialize`, +/// so it is sent as a JSON array of numbers. +/// +/// # Examples +/// +/// Streaming progress to the frontend from a command: +/// +/// ```rust +/// use tauri::ipc::Channel; +/// +/// #[derive(Clone, serde::Serialize)] +/// #[serde(rename_all = "camelCase")] +/// struct DownloadProgress { +/// downloaded: usize, +/// content_length: usize, +/// } +/// +/// #[tauri::command] +/// fn download(url: String, on_progress: Channel) -> tauri::Result<()> { +/// let content_length = 1000; +/// for downloaded in (0..=content_length).step_by(100) { +/// on_progress.send(DownloadProgress { downloaded, content_length })?; +/// } +/// Ok(()) +/// } +/// ``` +/// +/// Streaming binary chunks, received as `ArrayBuffer`s in JavaScript: +/// +/// ```rust +/// use tauri::ipc::{Channel, InvokeResponseBody}; +/// +/// #[tauri::command] +/// fn read_file(on_chunk: Channel) -> tauri::Result<()> { +/// for chunk in [vec![0u8; 16], vec![1u8; 16]] { +/// on_chunk.send(InvokeResponseBody::Raw(chunk))?; +/// } +/// Ok(()) +/// } +/// ``` +pub struct Channel { + inner: Arc, + phantom: std::marker::PhantomData, +} + +#[cfg(feature = "specta")] +const _: () = { + #[derive(specta::Type)] + #[specta(remote = super::Channel)] + #[allow(dead_code, non_camel_case_types)] + struct TAURI_CHANNEL(std::marker::PhantomData); +}; + +impl Clone for Channel { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + phantom: self.phantom, + } + } +} + +type OnDropFn = Option>; +type OnMessageFn = Box crate::Result<()> + Send + Sync>; + +struct ChannelInner { + id: u32, + on_message: OnMessageFn, + on_drop: OnDropFn, +} + +impl Drop for ChannelInner { + fn drop(&mut self) { + if let Some(on_drop) = &self.on_drop { + on_drop(); + } + } +} + +impl Serialize for Channel { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&format!("{IPC_PAYLOAD_PREFIX}{}", self.inner.id)) + } +} + +/// The ID of a channel that was defined on the JavaScript layer. +/// +/// Useful when expecting [`Channel`] as part of a JSON object instead of a top-level command argument. +/// +/// # Examples +/// +/// ```rust +/// use tauri::{ipc::JavaScriptChannelId, Runtime, Webview}; +/// +/// #[derive(serde::Deserialize)] +/// #[serde(rename_all = "camelCase")] +/// struct Button { +/// label: String, +/// on_click: JavaScriptChannelId, +/// } +/// +/// #[tauri::command] +/// fn add_button(webview: Webview, button: Button) { +/// let channel = button.on_click.channel_on(webview); +/// channel.send("clicked").unwrap(); +/// } +/// ``` +pub struct JavaScriptChannelId(CallbackFn); + +impl FromStr for JavaScriptChannelId { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + s.strip_prefix(IPC_PAYLOAD_PREFIX) + .ok_or("invalid channel string") + .and_then(|id| id.parse().map_err(|_| "invalid channel ID")) + .map(|id| Self(CallbackFn(id))) + } +} + +impl JavaScriptChannelId { + /// Gets a [`Channel`] for this channel ID on the given [`Webview`]. + pub fn channel_on(&self, webview: Webview) -> Channel { + let callback_fn = self.0; + let callback_id = callback_fn.0; + + let counter = Arc::new(AtomicUsize::new(0)); + let counter_clone = counter.clone(); + let webview_clone = webview.clone(); + + Channel::new_with_id( + callback_id, + Box::new(move |body| { + let current_index = counter.fetch_add(1, Ordering::Relaxed); + + if let Some(interceptor) = &webview.manager.channel_interceptor { + if interceptor(&webview, callback_fn, current_index, &body) { + return Ok(()); + } + } + + match body { + // Don't go through the fetch process if the payload is small + InvokeResponseBody::Json(json_string) + if json_string.len() < MAX_JSON_DIRECT_EXECUTE_THRESHOLD => + { + webview.eval(format_raw_js( + callback_id, + format!("{{ message: {json_string}, index: {current_index} }}"), + ))?; + } + InvokeResponseBody::Raw(bytes) if bytes.len() < MAX_RAW_DIRECT_EXECUTE_THRESHOLD => { + let bytes_as_json_array = serde_json::to_string(&bytes)?; + webview.eval(format_raw_js(callback_id, format!("{{ message: new Uint8Array({bytes_as_json_array}).buffer, index: {current_index} }}")))?; + } + // use the fetch API to speed up larger response payloads + _ => { + // the webview was closed (or replaced by a new one with the same label), + // so nothing would ever fetch this data + let Some(data_id) = + webview + .state::() + .insert_if(webview.label(), body, || webview.is_registered()) + else { + return Ok(()); + }; + + webview.eval(format!( + "window.__TAURI_INTERNALS__.invoke('{FETCH_CHANNEL_DATA_COMMAND}', null, {{ headers: {{ '{CHANNEL_ID_HEADER_NAME}': '{data_id}' }} }}).then((response) => window.__TAURI_INTERNALS__.runCallback({callback_id}, {{ message: response, index: {current_index} }})).catch(console.error)", + ))?; + } + } + + Ok(()) + }), + Some(Box::new(move || { + let current_index = counter_clone.load(Ordering::Relaxed); + let _ = webview_clone.eval(format_raw_js( + callback_id, + format!("{{ end: true, index: {current_index} }}"), + )); + })), + ) + } +} + +impl<'de> Deserialize<'de> for JavaScriptChannelId { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let value: String = Deserialize::deserialize(deserializer)?; + Self::from_str(&value).map_err(|_| { + serde::de::Error::custom(format!( + "invalid channel value `{value}`, expected a string in the `{IPC_PAYLOAD_PREFIX}ID` format" + )) + }) + } +} + +impl Channel { + /// Creates a new channel with the given message handler. + /// + /// This does not involve the frontend: the closure is called with the body of every message sent + /// through [`Channel::send`], and it is up to you to forward it. Use it to create a channel that + /// a plugin or a mobile command expects, or to consume channel messages in Rust. + /// + /// To push messages to a channel that was created by the frontend, receive the [`Channel`] as a + /// command argument (see [`CommandArg`]) or deserialize a [`JavaScriptChannelId`] and call + /// [`JavaScriptChannelId::channel_on`] with the target [`Webview`], which wires the messages + /// through the IPC for you. + /// + /// # Examples + /// + /// ```rust + /// use tauri::ipc::{Channel, InvokeResponseBody}; + /// + /// let channel = Channel::new(|message: InvokeResponseBody| { + /// match message { + /// InvokeResponseBody::Json(json) => println!("channel message: {json}"), + /// InvokeResponseBody::Raw(bytes) => println!("channel message: {} bytes", bytes.len()), + /// } + /// Ok(()) + /// }); + /// + /// channel.send("hello").unwrap(); + /// ``` + pub fn new crate::Result<()> + Send + Sync + 'static>( + on_message: F, + ) -> Self { + Self::new_with_id( + CHANNEL_COUNTER.fetch_add(1, Ordering::Relaxed), + Box::new(on_message), + None, + ) + } + + fn new_with_id(id: u32, on_message: OnMessageFn, on_drop: OnDropFn) -> Self { + #[allow(clippy::let_and_return)] + let channel = Self { + inner: Arc::new(ChannelInner { + id, + on_message, + on_drop, + }), + phantom: Default::default(), + }; + + #[cfg(mobile)] + crate::plugin::mobile::register_channel(Channel { + inner: channel.inner.clone(), + phantom: Default::default(), + }); + + channel + } + + // This is used from the IPC handler + pub(crate) fn from_callback_fn(webview: Webview, callback: CallbackFn) -> Self { + let callback_id = callback.0; + Channel::new_with_id( + callback_id, + Box::new(move |body| { + match body { + // Don't go through the fetch process if the payload is small + InvokeResponseBody::Json(json_string) + if json_string.len() < MAX_JSON_DIRECT_EXECUTE_THRESHOLD => + { + webview.eval(format_raw_js(callback_id, json_string))?; + } + InvokeResponseBody::Raw(bytes) if bytes.len() < MAX_RAW_DIRECT_EXECUTE_THRESHOLD => { + let bytes_as_json_array = serde_json::to_string(&bytes)?; + webview.eval(format_raw_js( + callback_id, + format!("new Uint8Array({bytes_as_json_array}).buffer"), + ))?; + } + // use the fetch API to speed up larger response payloads + _ => { + // the webview was closed (or replaced by a new one with the same label), + // so nothing would ever fetch this data + let Some(data_id) = + webview + .state::() + .insert_if(webview.label(), body, || webview.is_registered()) + else { + return Ok(()); + }; + + webview.eval(format!( + "window.__TAURI_INTERNALS__.invoke('{FETCH_CHANNEL_DATA_COMMAND}', null, {{ headers: {{ '{CHANNEL_ID_HEADER_NAME}': '{data_id}' }} }}).then((response) => window.__TAURI_INTERNALS__.runCallback({callback_id}, response)).catch(console.error)", + ))?; + } + } + + Ok(()) + }), + None, + ) + } + + /// The channel identifier. + pub fn id(&self) -> u32 { + self.inner.id + } + + /// Sends the given data through the channel. + pub fn send(&self, data: TSend) -> crate::Result<()> + where + TSend: IpcResponse, + { + (self.inner.on_message)(data.body()?) + } +} + +impl<'de, R: Runtime, TSend> CommandArg<'de, R> for Channel { + /// Grabs the [`Webview`] from the [`CommandItem`] and returns the associated [`Channel`]. + fn from_command(command: CommandItem<'de, R>) -> Result { + let name = command.name; + let arg = command.key; + let webview = command.message.webview(); + let value: String = + Deserialize::deserialize(command).map_err(|e| crate::Error::InvalidArgs(name, arg, e))?; + JavaScriptChannelId::from_str(&value) + .map(|id| id.channel_on(webview)) + .map_err(|_| { + InvokeError::from(format!( + "invalid channel value `{value}`, expected a string in the `{IPC_PAYLOAD_PREFIX}ID` format" + )) + }) + } +} + +#[command(root = "crate")] +fn fetch( + webview: Webview, + request: Request<'_>, + cache: State<'_, ChannelDataIpcQueue>, +) -> Result { + if let Some(id) = request + .headers() + .get(CHANNEL_ID_HEADER_NAME) + .and_then(|v| v.to_str().ok()) + .and_then(|id| id.parse().ok()) + { + if let Some(data) = cache.remove(webview.label(), id) { + Ok(Response::new(data)) + } else { + Err("data not found") + } + } else { + Err("missing channel id header") + } +} + +pub fn plugin() -> TauriPlugin { + PluginBuilder::new(CHANNEL_PLUGIN_NAME) + .invoke_handler(crate::generate_handler![ + #![plugin(__TAURI_CHANNEL__)] + fetch + ]) + .build() +} + +#[cfg(test)] +mod tests { + use super::*; + + fn json_body(s: &str) -> InvokeResponseBody { + InvokeResponseBody::Json(s.to_string()) + } + + #[test] + fn queue_entries_are_scoped_to_the_owning_webview() { + let queue = ChannelDataIpcQueue::default(); + let id = queue.insert("main", json_body("{}")); + + // foreign webviews cannot fetch the entry + assert!(queue.remove("settings", id).is_none()); + // the owner can, once + assert!(queue.remove("main", id).is_some()); + assert!(queue.remove("main", id).is_none()); + } + + #[test] + fn ids_are_scoped_per_webview() { + let queue = ChannelDataIpcQueue::default(); + let a = queue.insert("a", json_body("1")); + let b = queue.insert("b", json_body("2")); + + // each webview has its own id sequence, so the same id exists twice + // without the entries being visible across webviews + assert_eq!(a, b); + assert!(queue.remove("b", a).is_some()); + assert!(queue.remove("a", a).is_some()); + } + + #[test] + fn purge_removes_only_the_closed_webview_entries() { + let queue = ChannelDataIpcQueue::default(); + let a = queue.insert("a", json_body("1")); + let b = queue.insert("b", json_body("2")); + + queue.remove_webview_entries("a"); + + assert!(queue.remove("a", a).is_none()); + assert!(queue.remove("b", b).is_some()); + } + + #[test] + fn insert_is_skipped_for_dead_webviews() { + let queue = ChannelDataIpcQueue::default(); + assert!(queue.insert_if("a", json_body("1"), || false).is_none()); + assert!(queue.0.lock().unwrap().is_empty()); + } + + #[test] + fn channel_data_is_not_queued_after_the_webview_closes() { + use crate::test::{mock_builder, mock_context, noop_assets}; + + let app = mock_builder().build(mock_context(noop_assets())).unwrap(); + let queue = app.state::().inner().clone(); + let queued = |label: &str| { + queue + .0 + .lock() + .unwrap() + .get(label) + .map_or(0, |q| q.entries.len()) + }; + let large = || InvokeResponseBody::Raw(vec![0; MAX_RAW_DIRECT_EXECUTE_THRESHOLD]); + let open = || { + crate::WebviewWindowBuilder::new(&app, "main", crate::WebviewUrl::default()) + .build() + .unwrap() + }; + let channel_on = |window: &crate::WebviewWindow<_>| { + JavaScriptChannelId::from_str("__CHANNEL__:1") + .unwrap() + .channel_on::<_, InvokeResponseBody>(window.webview.clone()) + }; + + let old_channel = channel_on(&open()); + old_channel.send(large()).unwrap(); + assert_eq!(queued("main"), 1); + + app.handle().manager.on_window_close("main"); + assert_eq!(queued("main"), 0); + old_channel.send(large()).unwrap(); + assert_eq!(queued("main"), 0); + + // a new webview reusing the label does not receive the old channel's data + let new_channel = channel_on(&open()); + old_channel.send(large()).unwrap(); + assert_eq!(queued("main"), 0); + new_channel.send(large()).unwrap(); + assert_eq!(queued("main"), 1); + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ipc/command.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ipc/command.rs new file mode 100644 index 000000000..206f8ee76 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ipc/command.rs @@ -0,0 +1,332 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! The Tauri custom commands types and traits. +//! +//! You usually don't need to create these items yourself. These are created from [command](../attr.command.html) +//! attribute macro along the way and used by [`crate::generate_handler`] macro. + +use crate::{ + Runtime, + ipc::{InvokeBody, InvokeError, InvokeMessage}, +}; +use serde::{ + Deserialize, Deserializer, + de::{Error, Visitor}, +}; + +use tauri_utils::acl::resolved::ResolvedCommand; + +/// Represents a custom command. +pub struct CommandItem<'a, R: Runtime> { + /// Name of the plugin if this command targets one. + pub plugin: Option<&'static str>, + + /// The name of the command, e.g. `handler` on `#[command] fn handler(value: u64)` + pub name: &'static str, + + /// The key of the command item, e.g. `value` on `#[command] fn handler(value: u64)` + pub key: &'static str, + + /// The [`InvokeMessage`] that was passed to this command. + pub message: &'a InvokeMessage, + + /// The resolved ACL for this command. + pub acl: &'a Option>, +} + +/// Trait implemented by command arguments to derive a value from a [`CommandItem`]. +/// +/// # Command Arguments +/// +/// A command argument is any type that represents an item parsable from a [`CommandItem`]. Most +/// implementations will use the data stored in [`InvokeMessage`] since [`CommandItem`] is mostly a +/// wrapper around it. +/// +/// # Provided Implementations +/// +/// Tauri implements [`CommandArg`] automatically for a number of types. +/// * [`crate::AppHandle`] +/// * [`crate::Window`] +/// * [`crate::Webview`] +/// * [`crate::WebviewWindow`] +/// * [`crate::State`] +/// * [`crate::ipc::Channel`] +/// * [`crate::ipc::Request`] +/// * Gives access to the raw request body and headers instead of a deserialized argument. +/// * [`crate::ipc::CommandScope`] and [`crate::ipc::GlobalScope`] +/// * The scope defined by the capabilities that allowed the command, see the +/// [scope documentation](https://v2.tauri.app/security/scope/). +/// * `T where T: serde::Deserialize` +/// * Any type that implements `Deserialize` can automatically be used as a [`CommandArg`]. +/// +/// Note that the types above are resolved from the command message and not from the command +/// arguments sent by the frontend, so they do not need to be provided on the JavaScript side. +pub trait CommandArg<'de, R: Runtime>: Sized { + /// Derives an instance of `Self` from the [`CommandItem`]. + /// + /// If the derivation fails, the corresponding message will be rejected using [`InvokeMessage#reject`]. + fn from_command(command: CommandItem<'de, R>) -> Result; +} + +/// Automatically implement [`CommandArg`] for any type that can be deserialized. +impl<'de, D: Deserialize<'de>, R: Runtime> CommandArg<'de, R> for D { + fn from_command(command: CommandItem<'de, R>) -> Result { + let name = command.name; + let arg = command.key; + #[cfg(feature = "tracing")] + let _span = tracing::trace_span!("ipc::request::deserialize_arg", arg = arg).entered(); + Self::deserialize(command).map_err(|e| crate::Error::InvalidArgs(name, arg, e).into()) + } +} + +/// Pass the result of [`serde_json::Value::get`] into [`serde_json::Value`]'s deserializer. +/// +/// Returns an error if the [`CommandItem`]'s key does not exist in the value. +macro_rules! pass { + ($fn:ident, $($arg:ident: $argt:ty),+) => { + fn $fn>(self, $($arg: $argt),*) -> Result { + self.deserialize_json()?.$fn($($arg),*) + } + } +} + +impl<'a, R: Runtime> CommandItem<'a, R> { + fn deserialize_json(self) -> serde_json::Result<&'a serde_json::Value> { + if self.key.is_empty() { + return Err(serde_json::Error::custom(format!( + "command {} has an argument with no name with a non-optional value", + self.name + ))); + } + + match &self.message.payload { + InvokeBody::Raw(_body) => Err(serde_json::Error::custom(format!( + "command {} expected a value for key {} but the IPC call used a bytes payload", + self.name, self.key + ))), + InvokeBody::Json(v) => match v.get(self.key) { + Some(value) => Ok(value), + None => Err(serde_json::Error::custom(format!( + "command {} missing required key {}", + self.name, self.key + ))), + }, + } + } +} + +/// A [`Deserializer`] wrapper around [`CommandItem`]. +/// +/// If the key doesn't exist, an error will be returned if the deserialized type is not expecting +/// an optional item. If the key does exist, the value will be called with +/// [`Value`](serde_json::Value)'s [`Deserializer`] implementation. +impl<'de, R: Runtime> Deserializer<'de> for CommandItem<'de, R> { + type Error = serde_json::Error; + + pass!(deserialize_any, visitor: V); + pass!(deserialize_bool, visitor: V); + pass!(deserialize_i8, visitor: V); + pass!(deserialize_i16, visitor: V); + pass!(deserialize_i32, visitor: V); + pass!(deserialize_i64, visitor: V); + pass!(deserialize_u8, visitor: V); + pass!(deserialize_u16, visitor: V); + pass!(deserialize_u32, visitor: V); + pass!(deserialize_u64, visitor: V); + pass!(deserialize_f32, visitor: V); + pass!(deserialize_f64, visitor: V); + pass!(deserialize_char, visitor: V); + pass!(deserialize_str, visitor: V); + pass!(deserialize_string, visitor: V); + pass!(deserialize_bytes, visitor: V); + pass!(deserialize_byte_buf, visitor: V); + + fn deserialize_option>(self, visitor: V) -> Result { + match &self.message.payload { + InvokeBody::Raw(_body) => Err(serde_json::Error::custom(format!( + "command {} expected a value for key {} but the IPC call used a bytes payload", + self.name, self.key + ))), + InvokeBody::Json(v) => match v.get(self.key) { + Some(value) => value.deserialize_option(visitor), + None => visitor.visit_none(), + }, + } + } + + pass!(deserialize_unit, visitor: V); + pass!(deserialize_unit_struct, name: &'static str, visitor: V); + pass!(deserialize_newtype_struct, name: &'static str, visitor: V); + pass!(deserialize_seq, visitor: V); + pass!(deserialize_tuple, len: usize, visitor: V); + + pass!( + deserialize_tuple_struct, + name: &'static str, + len: usize, + visitor: V + ); + + pass!(deserialize_map, visitor: V); + + pass!( + deserialize_struct, + name: &'static str, + fields: &'static [&'static str], + visitor: V + ); + + pass!( + deserialize_enum, + name: &'static str, + fields: &'static [&'static str], + visitor: V + ); + + pass!(deserialize_identifier, visitor: V); + pass!(deserialize_ignored_any, visitor: V); +} + +/// [Autoref-based stable specialization](https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md) +/// +/// Nothing in this module is considered stable. +#[doc(hidden)] +pub mod private { + use crate::{ + Runtime, + ipc::{InvokeError, InvokeResolver, InvokeResponseBody, IpcResponse}, + }; + use std::future::Future; + #[cfg(feature = "tracing")] + pub use tracing; + + // ===== impl IpcResponse ===== + + pub struct ResponseTag; + + pub trait ResponseKind { + #[inline(always)] + fn blocking_kind(&self) -> ResponseTag { + ResponseTag + } + + #[inline(always)] + fn async_kind(&self) -> ResponseTag { + ResponseTag + } + } + + impl ResponseKind for &T {} + + impl ResponseTag { + #[inline(always)] + pub fn block(self, value: T, resolver: InvokeResolver) + where + R: Runtime, + T: IpcResponse, + { + resolver.respond(Ok(value)) + } + + #[inline(always)] + pub async fn future(self, value: T) -> Result + where + T: IpcResponse, + { + Ok(value.body()?) + } + } + + // ===== Result> ===== + + pub struct ResultTag; + + pub trait ResultKind { + #[inline(always)] + fn blocking_kind(&self) -> ResultTag { + ResultTag + } + + #[inline(always)] + fn async_kind(&self) -> ResultTag { + ResultTag + } + } + + impl> ResultKind for Result {} + + impl ResultTag { + #[inline(always)] + pub fn block(self, value: Result, resolver: InvokeResolver) + where + R: Runtime, + T: IpcResponse, + E: Into, + { + resolver.respond(value.map_err(Into::into)) + } + + #[inline(always)] + pub async fn future(self, value: Result) -> Result + where + T: IpcResponse, + E: Into, + { + Ok(value.map_err(Into::into)?.body()?) + } + } + + // ===== Future ===== + + pub struct FutureTag; + + pub trait FutureKind { + #[inline(always)] + fn async_kind(&self) -> FutureTag { + FutureTag + } + } + impl> FutureKind for &F {} + + impl FutureTag { + #[inline(always)] + pub async fn future(self, value: F) -> Result + where + T: IpcResponse, + F: Future + Send + 'static, + { + Ok(value.await.body()?) + } + } + + // ===== Future>> ===== + + pub struct ResultFutureTag; + + pub trait ResultFutureKind { + #[inline(always)] + fn async_kind(&self) -> ResultFutureTag { + ResultFutureTag + } + } + + impl, F: Future>> ResultFutureKind + for F + { + } + + impl ResultFutureTag { + #[inline(always)] + pub async fn future(self, value: F) -> Result + where + T: IpcResponse, + E: Into, + F: Future> + Send, + { + let response = value.await.map_err(Into::into)?; + Ok(response.body()?) + } + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ipc/format_callback.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ipc/format_callback.rs new file mode 100644 index 000000000..daa77e6d7 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ipc/format_callback.rs @@ -0,0 +1,344 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::Serialize; +use serde_json::value::RawValue; +use serialize_to_javascript::Serialized; + +use super::CallbackFn; + +/// The information about this is quite limited. On Chrome/Edge and Firefox, [the maximum string size is approximately 1 GB](https://stackoverflow.com/a/34958490). +/// +/// [From MDN:](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length#description) +/// +/// ECMAScript 2016 (ed. 7) established a maximum length of 2^53 - 1 elements. Previously, no maximum length was specified. +/// +/// In Firefox, strings have a maximum length of 2\*\*30 - 2 (~1GB). In versions prior to Firefox 65, the maximum length was 2\*\*28 - 1 (~256MB). +const MAX_JSON_STR_LEN: usize = usize::pow(2, 30) - 2; + +/// Minimum size JSON needs to be in order to convert it to JSON.parse with [`format_json`]. +// TODO: this number should be benchmarked and checked for optimal range, I set 10 KiB arbitrarily +// we don't want to lose the gained object parsing time to extra allocations preparing it +const MIN_JSON_PARSE_LEN: usize = 10_240; + +/// Transforms & escapes a JSON value. +/// +/// If it's an object or array, JSON.parse('{json}') is used, with the '{json}' string properly escaped. +/// The return value of this function can be safely used on [`eval`](crate::Window#method.eval) calls. +/// +/// Single quotes chosen because double quotes are already used in JSON. With single quotes, we only +/// need to escape strings that include backslashes or single quotes. If we used double quotes, then +/// there would be no cases that a string doesn't need escaping. +/// +/// The function takes a closure to handle the escaped string in order to avoid unnecessary allocations. +/// +/// # Safety +/// +/// The ability to safely escape JSON into a JSON.parse('{json}') relies entirely on 2 things. +/// +/// 1. `serde_json`'s ability to correctly escape and format json into a string. +/// 2. JavaScript engines not accepting anything except another unescaped, literal single quote +/// character to end a string that was opened with it. +fn serialize_js_with String>( + json_string: String, + options: serialize_to_javascript::Options, + cb: F, +) -> crate::Result { + // get a raw &str representation of a serialized json value. + + let raw = RawValue::from_string(json_string)?; + + // from here we know json.len() > 1 because an empty string is not a valid json value. + let json = raw.get(); + let first = json.as_bytes()[0]; + + #[cfg(debug_assertions)] + if first == b'"' { + assert!( + json.len() < MAX_JSON_STR_LEN, + "passing a string larger than the max JavaScript literal string size" + ) + } + + let return_val = if json.len() > MIN_JSON_PARSE_LEN && (first == b'{' || first == b'[') { + let serialized = Serialized::new(&raw, &options).into_string(); + // only use JSON.parse('{arg}') for arrays and objects less than the limit + // smaller literals do not benefit from being parsed from json + if serialized.len() < MAX_JSON_STR_LEN { + cb(&serialized) + } else { + cb(json) + } + } else { + cb(json) + }; + + Ok(return_val) +} + +/// Formats a function name and a serializable argument to be evaluated as callback. +/// +/// See [`format_raw`] for more information. +pub fn format(function_name: CallbackFn, arg: &T) -> crate::Result { + format_raw(function_name, serde_json::to_string(arg)?) +} + +/// Formats a function name and a raw JSON string argument to be evaluated as callback. +/// +/// This will serialize primitive JSON types (e.g. booleans, strings, numbers, etc.) as JavaScript literals, +/// but will serialize arrays and objects whose serialized JSON string is smaller than 1 GB and larger +/// than 10 KiB with `JSON.parse('...')`. +/// See [json-parse-benchmark](https://github.com/GoogleChromeLabs/json-parse-benchmark). +pub fn format_raw(function_name: CallbackFn, json_string: String) -> crate::Result { + let callback_id = function_name.0; + serialize_js_with(json_string, Default::default(), |arg| { + format_raw_js(callback_id, arg) + }) +} + +/// Formats a function name and a JavaScript string argument to be evaluated as callback. +pub fn format_raw_js(callback_id: u32, js: impl AsRef) -> String { + fn format_inner(callback_id: u32, js: &str) -> String { + format!("window.__TAURI_INTERNALS__.runCallback({callback_id}, {js})") + } + format_inner(callback_id, js.as_ref()) +} + +/// Formats a serializable Result type to its Promise response. +/// +/// See [`format_result_raw`] for more information. +pub fn format_result( + result: Result, + success_callback: CallbackFn, + error_callback: CallbackFn, +) -> crate::Result { + match result { + Ok(res) => format(success_callback, &res), + Err(err) => format(error_callback, &err), + } +} + +/// Formats a Result type of raw JSON strings to its Promise response. +/// Useful for Promises handling. +/// If the Result `is_ok()`, the callback will be the `success_callback` function name and the argument will be the Ok value. +/// If the Result `is_err()`, the callback will be the `error_callback` function name and the argument will be the Err value. +/// +/// * `result` the Result to check +/// * `success_callback` the function name of the Ok callback. Usually the `resolve` of the JS Promise. +/// * `error_callback` the function name of the Err callback. Usually the `reject` of the JS Promise. +/// +/// Note that the callback strings are automatically generated by the `invoke` helper. +pub fn format_result_raw( + raw_result: Result, + success_callback: CallbackFn, + error_callback: CallbackFn, +) -> crate::Result { + match raw_result { + Ok(res) => format_raw(success_callback, res), + Err(err) => format_raw(error_callback, err), + } +} + +#[cfg(test)] +mod test { + use super::*; + use quickcheck::{Arbitrary, Gen}; + use quickcheck_macros::quickcheck; + + impl Arbitrary for CallbackFn { + fn arbitrary(g: &mut Gen) -> CallbackFn { + CallbackFn(u32::arbitrary(g)) + } + } + + #[derive(Debug, Clone)] + struct JsonStr(String); + + impl Arbitrary for JsonStr { + fn arbitrary(g: &mut Gen) -> Self { + if bool::arbitrary(g) { + Self(format!( + "{{ {}: {} }}", + serde_json::to_string(&String::arbitrary(g)).unwrap(), + serde_json::to_string(&String::arbitrary(g)).unwrap() + )) + } else { + Self(serde_json::to_string(&String::arbitrary(g)).unwrap()) + } + } + } + + fn serialize_js(value: &T) -> crate::Result { + serialize_js_with(serde_json::to_string(value)?, Default::default(), |v| { + v.into() + }) + } + + fn serialize_js_raw(value: impl Into) -> crate::Result { + serialize_js_with(value.into(), Default::default(), |v| v.into()) + } + + #[test] + fn test_serialize_js() { + assert_eq!(serialize_js(&()).unwrap(), "null"); + assert_eq!(serialize_js(&5i32).unwrap(), "5"); + + #[derive(serde::Serialize)] + struct JsonObj { + value: String, + } + + let raw_str = "T".repeat(MIN_JSON_PARSE_LEN); + assert_eq!(serialize_js(&raw_str).unwrap(), format!("\"{raw_str}\"")); + + assert_eq!( + serialize_js(&JsonObj { + value: raw_str.clone() + }) + .unwrap(), + format!("JSON.parse('{{\"value\":\"{raw_str}\"}}')") + ); + + assert_eq!( + serialize_js(&JsonObj { + value: format!("\"{raw_str}\"") + }) + .unwrap(), + format!("JSON.parse('{{\"value\":\"\\\\\"{raw_str}\\\\\"\"}}')") + ); + + let dangerous_json = RawValue::from_string( + r#"{"test":"don\\🚀🐱‍👤\\'t forget to escape me!🚀🐱‍👤","te🚀🐱‍👤st2":"don't forget to escape me!","test3":"\\🚀🐱‍👤\\\\'''\\\\🚀🐱‍👤\\\\🚀🐱‍👤\\'''''"}"#.into() + ).unwrap(); + + let definitely_escaped_dangerous_json = format!( + "JSON.parse('{}')", + dangerous_json + .get() + .replace('\\', "\\\\") + .replace('\'', "\\'") + ); + let escape_single_quoted_json_test = + serialize_to_javascript::Serialized::new(&dangerous_json, &Default::default()).into_string(); + + let result = r#"JSON.parse('{"test":"don\\\\🚀🐱‍👤\\\\\'t forget to escape me!🚀🐱‍👤","te🚀🐱‍👤st2":"don\'t forget to escape me!","test3":"\\\\🚀🐱‍👤\\\\\\\\\'\'\'\\\\\\\\🚀🐱‍👤\\\\\\\\🚀🐱‍👤\\\\\'\'\'\'\'"}')"#; + assert_eq!(definitely_escaped_dangerous_json, result); + assert_eq!(escape_single_quoted_json_test, result); + } + + // check arbitrary strings in the format callback function + #[quickcheck] + fn qc_formatting(f: CallbackFn, a: String) -> bool { + // call format callback + let fc = format(f, &a).unwrap(); + fc.contains(&format!( + "window.__TAURI_INTERNALS__.runCallback({}, JSON.parse('{}'))", + f.0, + serde_json::Value::String(a.clone()), + )) || fc.contains(&format!( + r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#, + f.0, + serde_json::Value::String(a), + )) + } + + // check arbitrary strings in format_result + #[quickcheck] + fn qc_format_res(result: Result, c: CallbackFn, ec: CallbackFn) -> bool { + let resp = format_result(result.clone(), c, ec).expect("failed to format callback result"); + let (function, value) = match result { + Ok(v) => (c, v), + Err(e) => (ec, e), + }; + + resp.contains(&format!( + r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#, + function.0, + serde_json::Value::String(value), + )) + } + + #[test] + fn test_serialize_js_raw() { + assert_eq!(serialize_js_raw("null").unwrap(), "null"); + assert_eq!(serialize_js_raw("5").unwrap(), "5"); + assert_eq!( + serialize_js_raw("{ \"x\": [1, 2, 3] }").unwrap(), + "{ \"x\": [1, 2, 3] }" + ); + + #[derive(serde::Serialize)] + struct JsonObj { + value: String, + } + + let raw_str = "T".repeat(MIN_JSON_PARSE_LEN); + assert_eq!( + serialize_js_raw(format!("\"{raw_str}\"")).unwrap(), + format!("\"{raw_str}\"") + ); + + assert_eq!( + serialize_js_raw(format!("{{\"value\":\"{raw_str}\"}}")).unwrap(), + format!("JSON.parse('{{\"value\":\"{raw_str}\"}}')") + ); + + assert_eq!( + serialize_js(&JsonObj { + value: format!("\"{raw_str}\"") + }) + .unwrap(), + format!("JSON.parse('{{\"value\":\"\\\\\"{raw_str}\\\\\"\"}}')") + ); + + let dangerous_json = RawValue::from_string( + r#"{"test":"don\\🚀🐱‍👤\\'t forget to escape me!🚀🐱‍👤","te🚀🐱‍👤st2":"don't forget to escape me!","test3":"\\🚀🐱‍👤\\\\'''\\\\🚀🐱‍👤\\\\🚀🐱‍👤\\'''''"}"#.into() + ).unwrap(); + + let definitely_escaped_dangerous_json = format!( + "JSON.parse('{}')", + dangerous_json + .get() + .replace('\\', "\\\\") + .replace('\'', "\\'") + ); + let escape_single_quoted_json_test = + serialize_to_javascript::Serialized::new(&dangerous_json, &Default::default()).into_string(); + + let result = r#"JSON.parse('{"test":"don\\\\🚀🐱‍👤\\\\\'t forget to escape me!🚀🐱‍👤","te🚀🐱‍👤st2":"don\'t forget to escape me!","test3":"\\\\🚀🐱‍👤\\\\\\\\\'\'\'\\\\\\\\🚀🐱‍👤\\\\\\\\🚀🐱‍👤\\\\\'\'\'\'\'"}')"#; + assert_eq!(definitely_escaped_dangerous_json, result); + assert_eq!(escape_single_quoted_json_test, result); + } + + // check arbitrary strings in the format callback function + #[quickcheck] + fn qc_formatting_raw(f: CallbackFn, a: JsonStr) -> bool { + let a = a.0; + // call format callback + let fc = format_raw(f, a.clone()).unwrap(); + fc.contains(&format!( + r#"window.__TAURI_INTERNALS__.runCallback({}, JSON.parse('{}'))"#, + f.0, a + )) || fc.contains(&format!( + r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#, + f.0, a + )) + } + + // check arbitrary strings in format_result + #[quickcheck] + fn qc_format_raw_res(result: Result, c: CallbackFn, ec: CallbackFn) -> bool { + let result = result.map(|v| v.0).map_err(|e| e.0); + let resp = format_result_raw(result.clone(), c, ec).expect("failed to format callback result"); + let (function, value) = match result { + Ok(v) => (c, v), + Err(e) => (ec, e), + }; + + resp.contains(&format!( + r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#, + function.0, value + )) + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ipc/mod.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ipc/mod.rs new file mode 100644 index 000000000..824ec6b05 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ipc/mod.rs @@ -0,0 +1,606 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Types and functions related to Inter Procedure Call(IPC). +//! +//! This module includes utilities to send messages to the JS layer of the webview. + +use std::{ + future::Future, + sync::{Arc, Mutex}, +}; + +use http::HeaderMap; +use serde::{ + Deserialize, Serialize, + de::{DeserializeOwned, IntoDeserializer}, +}; +use serde_json::Value as JsonValue; +pub use serialize_to_javascript::Options as SerializeOptions; +use tauri_macros::default_runtime; +use tauri_utils::acl::resolved::ResolvedCommand; + +use crate::{Runtime, StateManager, webview::Webview}; + +mod authority; +#[cfg(feature = "dynamic-acl")] +mod capability_builder; +pub(crate) mod channel; +mod command; +pub(crate) mod format_callback; +pub(crate) mod protocol; + +pub use authority::{ + CommandScope, GlobalScope, Origin, RuntimeAuthority, ScopeObject, ScopeObjectMatch, ScopeValue, +}; +#[cfg(feature = "dynamic-acl")] +pub use capability_builder::{CapabilityBuilder, RuntimeCapability}; +pub use channel::{Channel, JavaScriptChannelId}; +pub use command::{CommandArg, CommandItem, private}; + +/// A closure that is run every time Tauri receives a message it doesn't explicitly handle. +pub type InvokeHandler = dyn Fn(Invoke) -> bool + Send + Sync + 'static; + +/// A closure that is responsible for respond a JS message. +pub type InvokeResponder = + dyn Fn(&Webview, &str, &InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static; +/// Similar to [`InvokeResponder`] but taking owned arguments. +pub type OwnedInvokeResponder = + dyn FnOnce(Webview, String, InvokeResponse, CallbackFn, CallbackFn) + Send + 'static; + +/// Possible values of an IPC payload. +/// +/// ### Android +/// On Android, [InvokeBody::Raw] is not supported. The enum will always contain [InvokeBody::Json]. +/// When targeting Android Devices, consider passing raw bytes as a base64 [[std::string::String]], which is still more efficient than passing them as a number array in [InvokeBody::Json] +#[derive(Debug, Clone)] +#[cfg_attr(test, derive(PartialEq))] +pub enum InvokeBody { + /// Json payload. + Json(JsonValue), + /// Bytes payload. + Raw(Vec), +} + +impl Default for InvokeBody { + fn default() -> Self { + Self::Json(Default::default()) + } +} + +impl From for InvokeBody { + fn from(value: JsonValue) -> Self { + Self::Json(value) + } +} + +impl From> for InvokeBody { + fn from(value: Vec) -> Self { + Self::Raw(value) + } +} + +impl InvokeBody { + #[cfg(mobile)] + pub(crate) fn into_json(self) -> JsonValue { + match self { + Self::Json(v) => v, + Self::Raw(v) => { + JsonValue::Array(v.into_iter().map(|n| JsonValue::Number(n.into())).collect()) + } + } + } +} + +/// Possible values of an IPC response. +#[derive(Debug, Clone)] +#[cfg_attr(test, derive(PartialEq))] +pub enum InvokeResponseBody { + /// Json payload. + Json(String), + /// Bytes payload. + Raw(Vec), +} + +impl From for InvokeResponseBody { + fn from(value: String) -> Self { + Self::Json(value) + } +} + +impl From> for InvokeResponseBody { + fn from(value: Vec) -> Self { + Self::Raw(value) + } +} + +impl From for InvokeResponseBody { + fn from(value: InvokeBody) -> Self { + match value { + InvokeBody::Json(v) => Self::Json(serde_json::to_string(&v).unwrap()), + InvokeBody::Raw(v) => Self::Raw(v), + } + } +} + +impl IpcResponse for InvokeResponseBody { + fn body(self) -> crate::Result { + Ok(self) + } +} + +impl InvokeResponseBody { + /// Attempts to deserialize the response. + pub fn deserialize(self) -> serde_json::Result { + match self { + Self::Json(v) => serde_json::from_str(&v), + Self::Raw(v) => T::deserialize(v.into_deserializer()), + } + } +} + +/// The IPC request. +/// +/// Includes the `body` and `headers` parameters of a Tauri command invocation. +/// This allows commands to accept raw bytes - on all platforms except Android. +#[derive(Debug)] +pub struct Request<'a> { + body: &'a InvokeBody, + headers: &'a HeaderMap, +} + +impl Request<'_> { + /// The request body. + pub fn body(&self) -> &InvokeBody { + self.body + } + + /// The request headers. + pub fn headers(&self) -> &HeaderMap { + self.headers + } +} + +impl<'a, R: Runtime> CommandArg<'a, R> for Request<'a> { + /// Returns the invoke [`Request`]. + fn from_command(command: CommandItem<'a, R>) -> Result { + Ok(Self { + body: command.message.payload(), + headers: command.message.headers(), + }) + } +} + +/// Marks a type as a response to an IPC call. +pub trait IpcResponse { + /// Resolve the IPC response body. + fn body(self) -> crate::Result; +} + +impl IpcResponse for T { + fn body(self) -> crate::Result { + serde_json::to_string(&self) + .map(Into::into) + .map_err(Into::into) + } +} + +/// The IPC response. +pub struct Response { + body: InvokeResponseBody, +} + +impl IpcResponse for Response { + fn body(self) -> crate::Result { + Ok(self.body) + } +} + +impl Response { + /// Defines a response with the given body. + pub fn new(body: impl Into) -> Self { + Self { body: body.into() } + } +} + +/// The message and resolver given to a custom command. +/// +/// This struct is used internally by macros and is explicitly **NOT** stable. +#[default_runtime(crate::Wry, wry)] +pub struct Invoke { + /// The message passed. + pub message: InvokeMessage, + + /// The resolver of the message. + pub resolver: InvokeResolver, + + /// Resolved ACL for this IPC invoke. + pub acl: Option>, +} + +/// Error response from an [`InvokeMessage`]. +#[derive(Debug)] +pub struct InvokeError(pub serde_json::Value); + +impl InvokeError { + /// Create an [`InvokeError`] as a string of the [`std::error::Error`] message. + #[inline(always)] + pub fn from_error(error: E) -> Self { + Self(serde_json::Value::String(error.to_string())) + } + + /// Create an [`InvokeError`] as a string of the [`anyhow::Error`] message. + #[inline(always)] + pub fn from_anyhow(error: anyhow::Error) -> Self { + Self(serde_json::Value::String(format!("{error:#}"))) + } +} + +impl From for InvokeError { + #[inline] + fn from(value: T) -> Self { + serde_json::to_value(value) + .map(Self) + .unwrap_or_else(Self::from_error) + } +} + +impl From for InvokeError { + #[inline(always)] + fn from(error: crate::Error) -> Self { + Self(serde_json::Value::String(error.to_string())) + } +} + +/// Response from a [`InvokeMessage`] passed to the [`InvokeResolver`]. +#[derive(Debug)] +pub enum InvokeResponse { + /// Resolve the promise. + Ok(InvokeResponseBody), + /// Reject the promise. + Err(InvokeError), +} + +impl> From> for InvokeResponse { + #[inline] + fn from(result: Result) -> Self { + match result { + Ok(ok) => match ok.body() { + Ok(value) => Self::Ok(value), + Err(err) => Self::Err(InvokeError::from_error(err)), + }, + Err(err) => Self::Err(err.into()), + } + } +} + +impl From for InvokeResponse { + fn from(error: InvokeError) -> Self { + Self::Err(error) + } +} + +/// Resolver of a invoke message. +#[default_runtime(crate::Wry, wry)] +pub struct InvokeResolver { + webview: Webview, + responder: Arc>>>>, + cmd: String, + pub(crate) callback: CallbackFn, + pub(crate) error: CallbackFn, +} + +impl Clone for InvokeResolver { + fn clone(&self) -> Self { + Self { + webview: self.webview.clone(), + responder: self.responder.clone(), + cmd: self.cmd.clone(), + callback: self.callback, + error: self.error, + } + } +} + +impl InvokeResolver { + pub(crate) fn new( + webview: Webview, + responder: Arc>>>>, + cmd: String, + callback: CallbackFn, + error: CallbackFn, + ) -> Self { + Self { + webview, + responder, + cmd, + callback, + error, + } + } + + /// Reply to the invoke promise with an async task. + pub fn respond_async(self, task: F) + where + T: IpcResponse, + F: Future> + Send + 'static, + { + crate::async_runtime::spawn(async move { + Self::return_task( + self.webview, + self.responder, + task, + self.cmd, + self.callback, + self.error, + ) + .await; + }); + } + + /// Reply to the invoke promise with an async task which is already serialized. + pub fn respond_async_serialized(self, task: F) + where + F: Future> + Send + 'static, + { + // Dynamic dispatch the call in dev for a faster compile time + // TODO: Revisit this and see if we can do this for the release build as well if the performance hit is not a problem + #[cfg(debug_assertions)] + { + self.respond_async_serialized_dyn(Box::pin(task)) + } + #[cfg(not(debug_assertions))] + { + self.respond_async_serialized_inner(task) + } + } + + /// Dynamic dispatch the [`Self::respond_async_serialized`] call + #[cfg(debug_assertions)] + fn respond_async_serialized_dyn( + self, + task: std::pin::Pin< + Box> + Send + 'static>, + >, + ) { + self.respond_async_serialized_inner(task) + } + + /// Reply to the invoke promise with an async task which is already serialized. + fn respond_async_serialized_inner(self, task: F) + where + F: Future> + Send + 'static, + { + crate::async_runtime::spawn(async move { + let response = match task.await { + Ok(ok) => InvokeResponse::Ok(ok), + Err(err) => InvokeResponse::Err(err), + }; + Self::return_result( + self.webview, + self.responder, + response, + self.cmd, + self.callback, + self.error, + ) + }); + } + + /// Reply to the invoke promise with a serializable value. + pub fn respond(self, value: Result) { + Self::return_result( + self.webview, + self.responder, + value.into(), + self.cmd, + self.callback, + self.error, + ) + } + + /// Resolve the invoke promise with a value. + pub fn resolve(self, value: T) { + self.respond(Ok(value)) + } + + /// Reject the invoke promise with a value. + pub fn reject(self, value: T) { + Self::return_result( + self.webview, + self.responder, + Result::<(), _>::Err(value).into(), + self.cmd, + self.callback, + self.error, + ) + } + + /// Reject the invoke promise with an [`InvokeError`]. + pub fn invoke_error(self, error: InvokeError) { + Self::return_result( + self.webview, + self.responder, + error.into(), + self.cmd, + self.callback, + self.error, + ) + } + + /// Asynchronously executes the given task + /// and evaluates its Result to the JS promise described by the `success_callback` and `error_callback` function names. + /// + /// If the Result `is_ok()`, the callback will be the `success_callback` function name and the argument will be the Ok value. + /// If the Result `is_err()`, the callback will be the `error_callback` function name and the argument will be the Err value. + pub async fn return_task( + webview: Webview, + responder: Arc>>>>, + task: F, + cmd: String, + success_callback: CallbackFn, + error_callback: CallbackFn, + ) where + T: IpcResponse, + F: Future> + Send + 'static, + { + let result = task.await; + Self::return_closure( + webview, + responder, + || result, + cmd, + success_callback, + error_callback, + ) + } + + pub(crate) fn return_closure Result>( + webview: Webview, + responder: Arc>>>>, + f: F, + cmd: String, + success_callback: CallbackFn, + error_callback: CallbackFn, + ) { + Self::return_result( + webview, + responder, + f().into(), + cmd, + success_callback, + error_callback, + ) + } + + pub(crate) fn return_result( + webview: Webview, + responder: Arc>>>>, + response: InvokeResponse, + cmd: String, + success_callback: CallbackFn, + error_callback: CallbackFn, + ) { + (responder.lock().unwrap().take().expect("resolver consumed"))( + webview, + cmd, + response, + success_callback, + error_callback, + ); + } +} + +/// An invoke message. +#[default_runtime(crate::Wry, wry)] +#[derive(Debug)] +pub struct InvokeMessage { + /// The webview that received the invoke message. + pub(crate) webview: Webview, + /// The IPC command. + pub(crate) command: String, + /// The JSON argument passed on the invoke message. + pub(crate) payload: InvokeBody, + /// The request headers. + pub(crate) headers: HeaderMap, +} + +impl Clone for InvokeMessage { + fn clone(&self) -> Self { + Self { + webview: self.webview.clone(), + command: self.command.clone(), + payload: self.payload.clone(), + headers: self.headers.clone(), + } + } +} + +impl InvokeMessage { + /// Create an new [`InvokeMessage`] from a payload send by a webview. + pub(crate) fn new( + webview: Webview, + command: String, + payload: InvokeBody, + headers: HeaderMap, + ) -> Self { + Self { + webview, + command, + payload, + headers, + } + } + + /// The invoke command. + #[inline(always)] + pub fn command(&self) -> &str { + &self.command + } + + /// The webview that received the invoke. + #[inline(always)] + pub fn webview(&self) -> Webview { + self.webview.clone() + } + + /// A reference to webview that received the invoke. + #[inline(always)] + pub fn webview_ref(&self) -> &Webview { + &self.webview + } + + /// A reference to the payload the invoke received. + #[inline(always)] + pub fn payload(&self) -> &InvokeBody { + &self.payload + } + + // TODO: make private or remove in v3 + /// The state manager associated with the application + #[deprecated(note = "Use `Manager::state` to access the state: `self.webview_ref().state()`")] + #[inline(always)] + pub fn state(&self) -> Arc { + self.webview.manager.state.clone() + } + + // TODO: make private or remove in v3 + /// A reference to the state manager associated with application. + #[deprecated(note = "Use `Manager::state` to access the state: `self.webview_ref().state()`")] + #[inline(always)] + pub fn state_ref(&self) -> &StateManager { + &self.webview.manager.state + } + + /// The request headers. + #[inline(always)] + pub fn headers(&self) -> &HeaderMap { + &self.headers + } +} + +/// The `Callback` type is the return value of the `transformCallback` JavaScript function. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct CallbackFn(pub u32); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn deserialize_invoke_response_body() { + let json = InvokeResponseBody::Json("[1, 123, 1231]".to_string()); + assert_eq!(json.deserialize::>().unwrap(), vec![1, 123, 1231]); + + let json = InvokeResponseBody::Json("\"string value\"".to_string()); + assert_eq!(json.deserialize::().unwrap(), "string value"); + + let json = InvokeResponseBody::Json("\"string value\"".to_string()); + assert!(json.deserialize::>().is_err()); + + let values = vec![1, 2, 3, 4, 5, 6, 1]; + let raw = InvokeResponseBody::Raw(values.clone()); + assert_eq!(raw.deserialize::>().unwrap(), values); + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ipc/protocol.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ipc/protocol.rs new file mode 100644 index 000000000..cdfe325f4 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/ipc/protocol.rs @@ -0,0 +1,753 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{borrow::Cow, sync::Arc}; + +use crate::{ + Runtime, + ipc::InvokeResponseBody, + manager::AppManager, + webview::{InvokeRequest, UriSchemeProtocolHandler}, +}; +use http::{ + HeaderValue, Method, Request, StatusCode, + header::{ + ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_EXPOSE_HEADERS, + CONTENT_TYPE, + }, +}; +use url::Url; + +use super::{CallbackFn, InvokeResponse}; + +const TAURI_CALLBACK_HEADER_NAME: &str = "Tauri-Callback"; +const TAURI_ERROR_HEADER_NAME: &str = "Tauri-Error"; +const TAURI_INVOKE_KEY_HEADER_NAME: &str = "Tauri-Invoke-Key"; + +const TAURI_RESPONSE_HEADER_NAME: &str = "Tauri-Response"; +const TAURI_RESPONSE_HEADER_ERROR: &str = "error"; +const TAURI_RESPONSE_HEADER_OK: &str = "ok"; + +pub fn message_handler( + manager: Arc>, +) -> crate::runtime::webview::WebviewIpcHandler { + Box::new(move |webview, request| handle_ipc_message(request, &manager, &webview.label)) +} + +pub fn get(manager: Arc>) -> UriSchemeProtocolHandler { + Box::new(move |label, request, responder| { + #[cfg(feature = "tracing")] + let span = tracing::trace_span!( + "ipc::request", + kind = "custom-protocol", + request = tracing::field::Empty + ) + .entered(); + + let respond = move |mut response: http::Response>| { + response + .headers_mut() + .insert(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*")); + response.headers_mut().insert( + ACCESS_CONTROL_EXPOSE_HEADERS, + HeaderValue::from_static(TAURI_RESPONSE_HEADER_NAME), + ); + responder.respond(response); + }; + + match *request.method() { + Method::POST => { + if let Some(webview) = manager.get_webview(label) { + match parse_invoke_request(&manager, request) { + Ok(request) => { + #[cfg(feature = "tracing")] + span.record( + "request", + match &request.body { + super::InvokeBody::Json(j) => serde_json::to_string(j).unwrap(), + super::InvokeBody::Raw(b) => serde_json::to_string(b).unwrap(), + }, + ); + #[cfg(feature = "tracing")] + let request_span = tracing::trace_span!("ipc::request::handle", cmd = request.cmd); + + webview.on_message( + request, + Box::new(move |_webview, _cmd, response, _callback, _error| { + #[cfg(feature = "tracing")] + let _respond_span = tracing::trace_span!( + parent: &request_span, + "ipc::request::respond" + ) + .entered(); + + #[cfg(feature = "tracing")] + let response_span = match &response { + InvokeResponse::Ok(InvokeResponseBody::Json(v)) => tracing::trace_span!( + "ipc::request::response", + response = v, + mime_type = tracing::field::Empty + ) + .entered(), + InvokeResponse::Ok(InvokeResponseBody::Raw(v)) => tracing::trace_span!( + "ipc::request::response", + response = format!("{v:?}"), + mime_type = tracing::field::Empty + ) + .entered(), + InvokeResponse::Err(e) => tracing::trace_span!( + "ipc::request::response", + error = format!("{e:?}"), + mime_type = tracing::field::Empty + ) + .entered(), + }; + + let response_header = match &response { + InvokeResponse::Ok(_) => TAURI_RESPONSE_HEADER_OK, + InvokeResponse::Err(_) => TAURI_RESPONSE_HEADER_ERROR, + }; + + let (mut response, mime_type) = match response { + InvokeResponse::Ok(InvokeResponseBody::Json(v)) => ( + http::Response::new(v.into_bytes().into()), + mime::APPLICATION_JSON, + ), + InvokeResponse::Ok(InvokeResponseBody::Raw(v)) => ( + http::Response::new(v.into()), + mime::APPLICATION_OCTET_STREAM, + ), + InvokeResponse::Err(e) => ( + http::Response::new(serde_json::to_vec(&e.0).unwrap().into()), + mime::APPLICATION_JSON, + ), + }; + + response + .headers_mut() + .insert(TAURI_RESPONSE_HEADER_NAME, response_header.parse().unwrap()); + + #[cfg(feature = "tracing")] + response_span.record("mime_type", mime_type.essence_str()); + + response.headers_mut().insert( + CONTENT_TYPE, + HeaderValue::from_str(mime_type.essence_str()).unwrap(), + ); + + respond(response); + }), + ); + } + Err(e) => { + respond( + http::Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .header(CONTENT_TYPE, mime::TEXT_PLAIN.essence_str()) + .body(e.into_bytes().into()) + .unwrap(), + ); + } + } + } else { + respond( + http::Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .header(CONTENT_TYPE, mime::TEXT_PLAIN.essence_str()) + .body("failed to acquire webview reference".as_bytes().into()) + .unwrap(), + ); + } + } + + Method::OPTIONS => { + let mut r = http::Response::new(Vec::new().into()); + r.headers_mut() + .insert(ACCESS_CONTROL_ALLOW_HEADERS, HeaderValue::from_static("*")); + + respond(r); + } + + _ => { + let mut r = http::Response::new("only POST and OPTIONS are allowed".as_bytes().into()); + *r.status_mut() = StatusCode::METHOD_NOT_ALLOWED; + r.headers_mut().insert( + CONTENT_TYPE, + HeaderValue::from_str(mime::TEXT_PLAIN.essence_str()).unwrap(), + ); + respond(r); + } + } + }) +} + +fn handle_ipc_message(request: Request, manager: &AppManager, label: &str) { + let Some(webview) = manager.get_webview(label) else { + return; + }; + + #[cfg(feature = "tracing")] + let _span = tracing::trace_span!( + "ipc::request", + kind = "post-message", + uri = request.uri().to_string(), + request = request.body() + ) + .entered(); + + use serde::{Deserialize, Deserializer}; + + #[derive(Default)] + pub(crate) struct HeaderMap(http::HeaderMap); + + impl<'de> Deserialize<'de> for HeaderMap { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let map = std::collections::HashMap::::deserialize(deserializer)?; + let mut headers = http::HeaderMap::default(); + for (key, value) in map { + if let (Ok(key), Ok(value)) = ( + http::header::HeaderName::from_bytes(key.as_bytes()), + http::HeaderValue::from_str(&value), + ) { + headers.insert(key, value); + } else { + return Err(serde::de::Error::custom(format!( + "invalid header `{key}` `{value}`" + ))); + } + } + Ok(Self(headers)) + } + } + + #[derive(Deserialize, Default)] + #[serde(rename_all = "camelCase")] + struct RequestOptions { + #[serde(default)] + headers: HeaderMap, + #[serde(default)] + custom_protocol_ipc_blocked: bool, + } + + #[derive(Deserialize)] + struct Message { + cmd: String, + callback: CallbackFn, + error: CallbackFn, + payload: serde_json::Value, + options: Option, + #[serde(rename = "__TAURI_INVOKE_KEY__")] + invoke_key: String, + } + + #[allow(unused_mut)] + let mut invoke_message: Option> = None; + + #[cfg(feature = "isolation")] + { + #[derive(Deserialize)] + struct IsolationMessage<'a> { + cmd: String, + callback: CallbackFn, + error: CallbackFn, + payload: crate::utils::pattern::isolation::RawIsolationPayload<'a>, + options: Option, + #[serde(rename = "__TAURI_INVOKE_KEY__")] + invoke_key: String, + } + + if let crate::Pattern::Isolation { crypto_keys, .. } = &*manager.pattern { + #[cfg(feature = "tracing")] + let _span = tracing::trace_span!("ipc::request::decrypt_isolation_payload").entered(); + + invoke_message.replace( + serde_json::from_str::>(request.body()) + .map_err(Into::into) + .and_then(|message| { + let is_raw = + message.payload.content_type() == &mime::APPLICATION_OCTET_STREAM.to_string(); + let payload = crypto_keys.decrypt(message.payload)?; + Ok(Message { + cmd: message.cmd, + callback: message.callback, + error: message.error, + payload: if is_raw { + payload.into() + } else { + serde_json::from_slice(&payload)? + }, + options: message.options, + invoke_key: message.invoke_key, + }) + }), + ); + } + } + + let message = invoke_message.unwrap_or_else(|| { + #[cfg(feature = "tracing")] + let _span = tracing::trace_span!("ipc::request::deserialize").entered(); + serde_json::from_str::(request.body()).map_err(Into::into) + }); + + match message { + Ok(message) => { + let options = message.options.unwrap_or_default(); + + let request = InvokeRequest { + cmd: message.cmd, + callback: message.callback, + error: message.error, + url: Url::parse(&request.uri().to_string()).expect("invalid IPC request URL"), + body: message.payload.into(), + headers: options.headers.0, + invoke_key: message.invoke_key, + }; + + #[cfg(feature = "tracing")] + let request_span = tracing::trace_span!("ipc::request::handle", cmd = request.cmd); + + webview.on_message( + request, + Box::new(move |webview, cmd, response, callback, error| { + use crate::ipc::Channel; + + #[cfg(feature = "tracing")] + let _respond_span = tracing::trace_span!( + parent: &request_span, + "ipc::request::respond" + ) + .entered(); + + fn responder_eval( + webview: &crate::Webview, + js: crate::Result, + error: CallbackFn, + ) { + let eval_js = js.unwrap_or_else(|e| { + crate::ipc::format_callback::format(error, &e.to_string()) + .expect("unable to serialize response error string to json") + }); + let _ = webview.eval(eval_js); + } + + let can_use_channel_for_response = cmd != crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND + && !options.custom_protocol_ipc_blocked; + + #[cfg(feature = "tracing")] + let mime_type = match &response { + InvokeResponse::Ok(InvokeResponseBody::Json(_)) => mime::APPLICATION_JSON, + InvokeResponse::Ok(InvokeResponseBody::Raw(_)) => mime::APPLICATION_OCTET_STREAM, + InvokeResponse::Err(_) => mime::APPLICATION_JSON, + }; + + #[cfg(feature = "tracing")] + let _response_span = match &response { + InvokeResponse::Ok(InvokeResponseBody::Json(v)) => tracing::trace_span!( + "ipc::request::response", + response = v, + mime_type = mime_type.essence_str() + ) + .entered(), + InvokeResponse::Ok(InvokeResponseBody::Raw(v)) => tracing::trace_span!( + "ipc::request::response", + response = format!("{v:?}"), + mime_type = mime_type.essence_str() + ) + .entered(), + InvokeResponse::Err(e) => tracing::trace_span!( + "ipc::request::response", + response = format!("{e:?}"), + mime_type = mime_type.essence_str() + ) + .entered(), + }; + + match response { + InvokeResponse::Ok(InvokeResponseBody::Json(v)) => { + if !(cfg!(target_os = "macos") || cfg!(target_os = "ios")) + && (v.starts_with('{') || v.starts_with('[')) + && can_use_channel_for_response + { + let _ = + Channel::from_callback_fn(webview, callback).send(InvokeResponseBody::Json(v)); + } else { + responder_eval( + &webview, + crate::ipc::format_callback::format_result_raw( + Result::<_, String>::Ok(v), + callback, + error, + ), + error, + ) + } + } + InvokeResponse::Ok(InvokeResponseBody::Raw(v)) => { + if cfg!(target_os = "macos") + || cfg!(target_os = "ios") + || !can_use_channel_for_response + { + responder_eval( + &webview, + crate::ipc::format_callback::format_result( + Result::<_, ()>::Ok(v), + callback, + error, + ), + error, + ); + } else { + let _ = + Channel::from_callback_fn(webview, callback).send(InvokeResponseBody::Raw(v)); + } + } + InvokeResponse::Err(e) => responder_eval( + &webview, + crate::ipc::format_callback::format_result( + Result::<(), _>::Err(&e.0), + callback, + error, + ), + error, + ), + } + }), + ); + } + Err(e) => { + #[cfg(feature = "tracing")] + tracing::trace!("ipc.request.error {e}"); + + let _ = webview.eval(format!( + r#"console.error({})"#, + serde_json::Value::String(e.to_string()) + )); + } + } +} + +fn parse_invoke_request( + #[allow(unused_variables)] manager: &AppManager, + request: http::Request>, +) -> std::result::Result { + #[allow(unused_mut)] + let (parts, mut body) = request.into_parts(); + + // skip leading `/` + let cmd = percent_encoding::percent_decode(&parts.uri.path().as_bytes()[1..]) + .decode_utf8_lossy() + .to_string(); + + // on Android we cannot read the request body + // so we must ignore it because some commands use the IPC for faster response + let has_payload = !body.is_empty(); + + #[allow(unused_mut)] + let mut content_type = parts + .headers + .get(http::header::CONTENT_TYPE) + .and_then(|h| h.to_str().ok()) + .map(|mime| mime.parse()) + .unwrap_or(Ok(mime::APPLICATION_OCTET_STREAM)) + .map_err(|_| "unknown content type")?; + + #[cfg(feature = "isolation")] + if let crate::Pattern::Isolation { crypto_keys, .. } = &*manager.pattern { + // if the platform does not support request body, we ignore it + if has_payload { + #[cfg(feature = "tracing")] + let _span = tracing::trace_span!("ipc::request::decrypt_isolation_payload").entered(); + + (body, content_type) = crate::utils::pattern::isolation::RawIsolationPayload::try_from(&body) + .and_then(|raw| { + let content_type = raw + .content_type() + .parse() + .unwrap_or(mime::APPLICATION_OCTET_STREAM); + Ok((crypto_keys.decrypt(raw)?, content_type)) + }) + .map_err(|e| e.to_string())?; + } + } + + let invoke_key = parts + .headers + .get(TAURI_INVOKE_KEY_HEADER_NAME) + .ok_or("missing Tauri-Invoke-Key header")? + .to_str() + .map_err(|_| "Tauri invoke key header value must be a string")? + .to_owned(); + + let url = Url::parse( + parts + .headers + .get("Origin") + .ok_or("missing Origin header")? + .to_str() + .map_err(|_| "Origin header value must be a string")?, + ) + .map_err(|_| "Origin header is not a valid URL")?; + + let callback = CallbackFn( + parts + .headers + .get(TAURI_CALLBACK_HEADER_NAME) + .ok_or("missing Tauri-Callback header")? + .to_str() + .map_err(|_| "Tauri callback header value must be a string")? + .parse() + .map_err(|_| "Tauri callback header value must be a numeric string")?, + ); + let error = CallbackFn( + parts + .headers + .get(TAURI_ERROR_HEADER_NAME) + .ok_or("missing Tauri-Error header")? + .to_str() + .map_err(|_| "Tauri error header value must be a string")? + .parse() + .map_err(|_| "Tauri error header value must be a numeric string")?, + ); + + #[cfg(feature = "tracing")] + let span = tracing::trace_span!("ipc::request::deserialize").entered(); + + let body = if content_type == mime::APPLICATION_OCTET_STREAM { + body.into() + } else if content_type == mime::APPLICATION_JSON { + // if the platform does not support request body, we ignore it + if has_payload { + serde_json::from_slice::(&body) + .map_err(|e| e.to_string())? + .into() + } else { + serde_json::Value::Object(Default::default()).into() + } + } else { + return Err(format!("content type {content_type} is not implemented")); + }; + + #[cfg(feature = "tracing")] + drop(span); + + let payload = InvokeRequest { + cmd, + callback, + error, + url, + body, + headers: parts.headers, + invoke_key, + }; + + Ok(payload) +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::*; + use crate::{StateManager, Wry, ipc::InvokeBody, manager::AppManager, plugin::PluginStore}; + use http::header::*; + use serde_json::json; + use tauri_macros::generate_context; + + #[test] + fn parse_invoke_request() { + let context = generate_context!("test/fixture/src-tauri/tauri.conf.json", crate, test = true); + let manager: AppManager = AppManager::with_handlers( + context, + PluginStore::default(), + Box::new(|_| false), + None, + #[cfg(any(target_os = "macos", target_os = "ios"))] + None, + Default::default(), + Default::default(), + StateManager::new(), + Default::default(), + #[cfg(all(desktop, feature = "tray-icon"))] + Default::default(), + Default::default(), + Default::default(), + Default::default(), + "".into(), + None, + crate::generate_invoke_key().unwrap(), + ); + + let cmd = "write_something"; + let url = "tauri://localhost"; + let invoke_key = "1234ahdsjkl123"; + let callback = 12378123; + let error = 6243; + let mut headers = HeaderMap::from_iter(vec![ + ( + CONTENT_TYPE, + HeaderValue::from_str(mime::APPLICATION_OCTET_STREAM.as_ref()).unwrap(), + ), + ( + HeaderName::from_str(TAURI_INVOKE_KEY_HEADER_NAME).unwrap(), + HeaderValue::from_str(invoke_key).unwrap(), + ), + ( + HeaderName::from_str(TAURI_CALLBACK_HEADER_NAME).unwrap(), + HeaderValue::from_str(&callback.to_string()).unwrap(), + ), + ( + HeaderName::from_str(TAURI_ERROR_HEADER_NAME).unwrap(), + HeaderValue::from_str(&error.to_string()).unwrap(), + ), + (ORIGIN, HeaderValue::from_str("tauri://localhost").unwrap()), + ]); + + let mut request = Request::builder().uri(format!("ipc://localhost/{cmd}")); + *request.headers_mut().unwrap() = headers.clone(); + + let body = vec![123, 31, 45]; + let request = request.body(body.clone()).unwrap(); + let invoke_request = super::parse_invoke_request(&manager, request).unwrap(); + + assert_eq!(invoke_request.cmd, cmd); + assert_eq!(invoke_request.callback.0, callback); + assert_eq!(invoke_request.error.0, error); + assert_eq!(invoke_request.invoke_key, invoke_key); + assert_eq!(invoke_request.url, url.parse().unwrap()); + assert_eq!(invoke_request.headers, headers); + assert_eq!(invoke_request.body, InvokeBody::Raw(body)); + + let body = json!({ + "key": 1, + "anotherKey": "asda", + }); + + headers.insert( + CONTENT_TYPE, + HeaderValue::from_str(mime::APPLICATION_JSON.as_ref()).unwrap(), + ); + + let mut request = Request::builder().uri(format!("ipc://localhost/{cmd}")); + *request.headers_mut().unwrap() = headers.clone(); + + let request = request.body(serde_json::to_vec(&body).unwrap()).unwrap(); + let invoke_request = super::parse_invoke_request(&manager, request).unwrap(); + + assert_eq!(invoke_request.headers, headers); + assert_eq!(invoke_request.body, InvokeBody::Json(body)); + } + + #[test] + #[cfg(feature = "isolation")] + fn parse_invoke_request_isolation() { + let context = generate_context!( + "test/fixture/isolation/src-tauri/tauri.conf.json", + crate, + test = false + ); + + let crate::pattern::Pattern::Isolation { crypto_keys, .. } = &context.pattern else { + unreachable!() + }; + + let mut nonce = [0u8; 12]; + getrandom::fill(&mut nonce).unwrap(); + + let body_raw = vec![1, 41, 65, 12, 78]; + let body_bytes = crypto_keys.aes_gcm().encrypt(&nonce, &body_raw).unwrap(); + let isolation_payload_raw = json!({ + "nonce": nonce, + "payload": body_bytes, + "contentType": mime::APPLICATION_OCTET_STREAM.to_string(), + }); + + let body_json = json!({ + "key": 1, + "anotherKey": "string" + }); + let body_bytes = crypto_keys + .aes_gcm() + .encrypt(&nonce, &serde_json::to_vec(&body_json).unwrap()) + .unwrap(); + let isolation_payload_json = json!({ + "nonce": nonce, + "payload": body_bytes, + "contentType": mime::APPLICATION_JSON.to_string(), + }); + + let manager: AppManager = AppManager::with_handlers( + context, + PluginStore::default(), + Box::new(|_| false), + None, + #[cfg(any(target_os = "macos", target_os = "ios"))] + None, + Default::default(), + Default::default(), + StateManager::new(), + Default::default(), + #[cfg(all(desktop, feature = "tray-icon"))] + Default::default(), + Default::default(), + Default::default(), + Default::default(), + "".into(), + None, + crate::generate_invoke_key().unwrap(), + ); + + let cmd = "write_something"; + let url = "tauri://localhost"; + let invoke_key = "1234ahdsjkl123"; + let callback = 12378123; + let error = 6243; + + let headers = HeaderMap::from_iter(vec![ + ( + CONTENT_TYPE, + HeaderValue::from_str(mime::APPLICATION_JSON.as_ref()).unwrap(), + ), + ( + HeaderName::from_str(TAURI_INVOKE_KEY_HEADER_NAME).unwrap(), + HeaderValue::from_str(invoke_key).unwrap(), + ), + ( + HeaderName::from_str(TAURI_CALLBACK_HEADER_NAME).unwrap(), + HeaderValue::from_str(&callback.to_string()).unwrap(), + ), + ( + HeaderName::from_str(TAURI_ERROR_HEADER_NAME).unwrap(), + HeaderValue::from_str(&error.to_string()).unwrap(), + ), + (ORIGIN, HeaderValue::from_str("tauri://localhost").unwrap()), + ]); + + let mut request = Request::builder().uri(format!("ipc://localhost/{cmd}")); + *request.headers_mut().unwrap() = headers.clone(); + let body = serde_json::to_vec(&isolation_payload_raw).unwrap(); + let request = request.body(body).unwrap(); + let invoke_request = super::parse_invoke_request(&manager, request).unwrap(); + + assert_eq!(invoke_request.cmd, cmd); + assert_eq!(invoke_request.callback.0, callback); + assert_eq!(invoke_request.error.0, error); + assert_eq!(invoke_request.invoke_key, invoke_key); + assert_eq!(invoke_request.url, url.parse().unwrap()); + assert_eq!(invoke_request.headers, headers); + assert_eq!(invoke_request.body, InvokeBody::Raw(body_raw)); + + let mut request = Request::builder().uri(format!("ipc://localhost/{cmd}")); + *request.headers_mut().unwrap() = headers.clone(); + let body = serde_json::to_vec(&isolation_payload_json).unwrap(); + let request = request.body(body).unwrap(); + let invoke_request = super::parse_invoke_request(&manager, request).unwrap(); + + assert_eq!(invoke_request.headers, headers); + assert_eq!(invoke_request.body, InvokeBody::Json(body_json)); + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/lib.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/lib.rs new file mode 100644 index 000000000..46b630785 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/lib.rs @@ -0,0 +1,1285 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Tauri is a framework for building tiny, blazing fast binaries for all major desktop platforms. +//! Developers can integrate any front-end framework that compiles to HTML, JS and CSS for building their user interface. +//! The backend of the application is a rust-sourced binary with an API that the front-end can interact with. +//! +//! # Cargo features +//! +//! The following are a list of [Cargo features](https://doc.rust-lang.org/stable/cargo/reference/manifest.html#the-features-section) that can be enabled or disabled: +//! +//! - **wry** *(enabled by default)*: Enables the [wry](https://github.com/tauri-apps/wry) runtime. Only disable it if you want a custom runtime. +//! - **common-controls-v6** *(enabled by default)*: Enables [Common Controls v6](https://learn.microsoft.com/en-us/windows/win32/controls/common-control-versions) support on Windows, mainly for the predefined `about` menu item. +//! - **x11** *(enabled by default)*: Enables X11 support. Disable this if you only target Wayland. +//! - **dbus** *(enabled by default)*: Enables dbus dependency for theme support on Linux. Disable this if you do not need theme support or don't want to build the dbus rust crate. The WebView dependencies use dbus either way. +//! - **unstable**: Enables unstable features. Be careful, it might introduce breaking changes in future minor releases. +//! It unlocks the multiwebview APIs, where a [`Window`] (the native window) and a [`Webview`] (the web content it hosts) +//! are separate objects instead of the single [`WebviewWindow`] you get by default: +//! - `window::WindowBuilder` and `Window::builder` to create a window that has no webview attached to it; +//! - `webview::WebviewBuilder` and `Window::add_child` to attach one or more webviews to an existing window; +//! - [`Webview::reparent`] to move a webview to another window (it always fails on a [`WebviewWindow`] webview +//! when this feature is disabled) and [`Webview::set_auto_resize`] to make a child webview follow the window size; +//! - the [`Manager`] getters for these objects: `get_window`, `get_focused_window`, `windows`, `get_webview` and `webviews`. +//! +//! See the [multiwebview example](https://github.com/tauri-apps/tauri/tree/dev/examples/multiwebview). +//! - **tracing**: Enables [`tracing`](https://docs.rs/tracing/latest/tracing) for window startup, plugins, `Window::eval`, events, IPC, updater and custom protocol request handlers. +//! - **test**: Enables the [`mod@test`] module exposing unit test helpers. +//! - **objc-exception**: This feature flag is no-op since 2.3.0. +//! - **linux-libxdo**: Enables linking to libxdo which enables Cut, Copy, Paste and SelectAll menu items to work on Linux. +//! - **isolation**: Enables the isolation pattern. Enabled by default if the `app > security > pattern > use` config option is set to `isolation` on the `tauri.conf.json` file. +//! - **custom-protocol**: Feature managed by the Tauri CLI. When enabled, Tauri assumes a production environment instead of a development one. +//! - **devtools**: Enables the developer tools (Web inspector) and [`window::Window#method.open_devtools`]. Enabled by default on debug builds. +//! On macOS it uses private APIs, so you can't enable it if your app will be published to the App Store. +//! - **native-tls**: Provides TLS support to connect over HTTPS. +//! - **native-tls-vendored**: Compile and statically link to a vendored copy of OpenSSL. +//! - **rustls-tls**: Provides TLS support to connect over HTTPS using rustls. +//! - **process-relaunch-dangerous-allow-symlink-macos**: Allows the [`process::current_binary`] function to allow symlinks on macOS (this is dangerous, see the Security section in the documentation website). +//! - **tray-icon**: Enables application tray icon APIs. Enabled by default if the `trayIcon` config is defined on the `tauri.conf.json` file. +//! - **macos-private-api**: Enables features only available in **macOS**'s private APIs, currently the `transparent` window functionality and the `fullScreenEnabled` preference setting to `true`. Enabled by default if the `tauri > macosPrivateApi` config flag is set to `true` on the `tauri.conf.json` file. **No-op since Tauri 2.12.1** because the APIs are always enabled now. +//! - **webview-data-url**: Enables usage of data URLs on the webview. +//! - **compression** *(enabled by default)*: Enables asset compression. You should only disable this if you want faster compile times in release builds - it produces larger binaries. +//! - **config-json5**: Adds support to JSON5 format for `tauri.conf.json`. +//! - **config-toml**: Adds support to TOML format for the configuration `Tauri.toml`. +//! - **image-ico**: Adds support to parse `.ico` image, see [`image::Image`]. +//! - **image-png**: Adds support to parse `.png` image, see [`image::Image`]. +//! - **macos-proxy**: Adds support for [`WebviewBuilder::proxy_url`] on macOS. Requires macOS 14+. +//! - **specta**: Add support for [`specta::specta`](https://docs.rs/specta/%5E2.0.0-rc.9/specta/attr.specta.html) with Tauri arguments such as [`State`], [`Window`] and [`AppHandle`] +//! - **dynamic-acl** *(enabled by default)*: Enables you to add ACLs at runtime, notably it enables the [`Manager::add_capability`] function. +//! +//! ## Cargo allowlist features +//! +//! The following are a list of [Cargo features](https://doc.rust-lang.org/stable/cargo/reference/manifest.html#the-features-section) that enables commands for Tauri's API package. +//! These features are automatically enabled by the Tauri CLI based on the `allowlist` configuration under `tauri.conf.json`. +//! +//! ### Protocol allowlist +//! +//! - **protocol-asset**: Enables the `asset` custom protocol. + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png" +)] +#![warn(missing_docs, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg))] + +/// Setups the binding that initializes an iOS plugin. +#[cfg(target_os = "ios")] +#[macro_export] +macro_rules! ios_plugin_binding { + ($fn_name: ident) => { + tauri::swift_rs::swift!(fn $fn_name() -> *const ::std::ffi::c_void); + } +} +#[cfg(target_os = "macos")] +#[doc(hidden)] +pub use embed_plist; +pub use error::{Error, Result}; +use ipc::RuntimeAuthority; +#[cfg(feature = "dynamic-acl")] +use ipc::RuntimeCapability; +pub use resources::{Resource, ResourceId, ResourceTable}; +#[cfg(target_os = "ios")] +#[doc(hidden)] +pub use swift_rs; +pub use tauri_macros::include_image; +#[cfg(mobile)] +pub use tauri_macros::mobile_entry_point; +pub use tauri_macros::{command, generate_handler}; + +use tauri_utils::assets::AssetsIter; +pub use url::Url; + +pub(crate) mod app; +pub mod async_runtime; +mod error; +mod event; +pub mod ipc; +mod manager; +mod pattern; +pub mod plugin; +pub(crate) mod protocol; +mod resources; +mod vibrancy; +pub mod webview; +pub mod window; +use tauri_runtime as runtime; +pub mod image; +#[cfg(target_os = "ios")] +mod ios; +#[cfg(desktop)] +#[cfg_attr(docsrs, doc(cfg(desktop)))] +pub mod menu; +/// Path APIs. +pub mod path; +pub mod process; +/// The allowlist scopes. +pub mod scope; +mod state; + +#[cfg(all(desktop, feature = "tray-icon"))] +#[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "tray-icon"))))] +pub mod tray; +pub use tauri_utils as utils; + +pub use http; + +/// A Tauri [`Runtime`] wrapper around wry. +#[cfg(feature = "wry")] +#[cfg_attr(docsrs, doc(cfg(feature = "wry")))] +pub type Wry = tauri_runtime_wry::Wry; +/// A Tauri [`RuntimeHandle`] wrapper around wry. +#[cfg(feature = "wry")] +#[cfg_attr(docsrs, doc(cfg(feature = "wry")))] +pub type WryHandle = tauri_runtime_wry::WryHandle; + +#[cfg(all(feature = "wry", target_os = "android"))] +#[cfg_attr(docsrs, doc(cfg(all(feature = "wry", target_os = "android"))))] +#[doc(hidden)] +#[macro_export] +macro_rules! android_binding { + ($domain:ident, $app_name:ident, $main:ident, $wry:path) => { + use $wry::{ + android_setup, + prelude::{JClass, JNIEnv, JString}, + }; + + ::tauri::wry::android_binding!($domain, $app_name, $wry); + + ::tauri::tao::android_binding!($domain, $app_name, Rust, android_setup, $main, ::tauri::tao); + + // be careful when renaming this, the `Java_app_tauri_plugin_PluginManager_handlePluginResponse` symbol is checked by the CLI + ::tauri::tao::platform::android::prelude::android_fn!( + app_tauri, + plugin, + PluginManager, + handlePluginResponse, + [i32, JString, JString], + ); + ::tauri::tao::platform::android::prelude::android_fn!( + app_tauri, + plugin, + PluginManager, + sendChannelData, + [i64, JString], + ); + + // this function is a glue between PluginManager.kt > handlePluginResponse and Rust + #[allow(non_snake_case)] + pub fn handlePluginResponse( + mut env: JNIEnv, + _: JClass, + id: i32, + success: JString, + error: JString, + ) { + ::tauri::handle_android_plugin_response(&mut env, id, success, error); + } + + // this function is a glue between PluginManager.kt > sendChannelData and Rust + #[allow(non_snake_case)] + pub fn sendChannelData(mut env: JNIEnv, _: JClass, id: i64, data: JString) { + ::tauri::send_channel_data(&mut env, id, data); + } + }; +} + +#[cfg(all(feature = "wry", target_os = "android"))] +#[doc(hidden)] +pub use plugin::mobile::{handle_android_plugin_response, send_channel_data}; +#[cfg(all(feature = "wry", target_os = "android"))] +#[doc(hidden)] +pub use tauri_runtime_wry::{tao, wry}; + +/// A task to run on the main thread. +pub type SyncTask = Box; + +use serde::Serialize; +use std::{ + borrow::Cow, + collections::HashMap, + fmt::{self, Debug}, + sync::MutexGuard, +}; +use utils::assets::{AssetKey, CspHash, EmbeddedAssets}; + +#[cfg(feature = "wry")] +#[cfg_attr(docsrs, doc(cfg(feature = "wry")))] +pub use tauri_runtime_wry::webview_version; + +#[cfg(target_os = "macos")] +#[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] +pub use runtime::ActivationPolicy; + +pub use self::utils::TitleBarStyle; + +use self::event::EventName; +pub use self::event::{Event, EventId, EventTarget}; +use self::manager::EmitPayload; +pub use { + self::app::{ + App, AppHandle, AssetResolver, Builder, CloseRequestApi, ExitRequestApi, RESTART_EXIT_CODE, + RunEvent, UriSchemeContext, UriSchemeResponder, WebviewEvent, WindowEvent, + }, + self::manager::Asset, + self::runtime::{ + DeviceEventFilter, UserAttentionType, + dpi::{ + LogicalPosition, LogicalRect, LogicalSize, LogicalUnit, PhysicalPosition, PhysicalRect, + PhysicalSize, PhysicalUnit, Pixel, PixelUnit, Position, Rect, Size, + }, + window::{CursorIcon, DragDropEvent, WindowSizeConstraints}, + }, + self::state::{State, StateManager}, + self::utils::{ + Env, PackageInfo, Theme, + config::{Config, WebviewUrl}, + }, + self::webview::{Webview, WebviewWindow, WebviewWindowBuilder}, + self::window::{Monitor, Window}, + scope::*, +}; + +#[cfg(feature = "unstable")] +#[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] +pub use {self::webview::WebviewBuilder, self::window::WindowBuilder}; + +/// The Tauri version. +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg(target_os = "ios")] +#[doc(hidden)] +pub fn log_stdout() { + #[cfg(target_os = "ios")] + unsafe { + crate::ios::log_stdout(); + } +} + +/// The user event type. +#[derive(Debug, Clone)] +pub enum EventLoopMessage { + /// An event from a menu item, could be on the window menu bar, application menu bar (on macOS) or tray icon menu. + #[cfg(desktop)] + MenuEvent(menu::MenuEvent), + /// An event from a menu item, could be on the window menu bar, application menu bar (on macOS) or tray icon menu. + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "tray-icon"))))] + TrayIconEvent(tray::TrayIconEvent), +} + +/// The webview runtime interface. A wrapper around [`runtime::Runtime`] with the proper user event type associated. +pub trait Runtime: runtime::Runtime {} +/// The webview runtime handle. A wrapper around [`runtime::RuntimeHandle`] with the proper user event type associated. +pub trait RuntimeHandle: runtime::RuntimeHandle {} + +impl> Runtime for W {} +impl> RuntimeHandle for R {} + +/// Reads the config file at compile time and generates a [`Context`] based on its content. +/// +/// The default config file path is a `tauri.conf.json` file inside the Cargo manifest directory of +/// the crate being built. +/// +/// # Custom Config Path +/// +/// You may pass a string literal to this macro to specify a custom path for the Tauri config file. +/// If the path is relative, it will be search for relative to the Cargo manifest of the compiling +/// crate. +/// +/// # Note +/// +/// This macro should not be called if you are using [`tauri-build`] to generate the context from +/// inside your build script as it will just cause excess computations that will be discarded. Use +/// either the [`tauri-build`] method or this macro - not both. +/// +/// [`tauri-build`]: https://docs.rs/tauri-build +pub use tauri_macros::generate_context; + +/// Include a [`Context`] that was generated by [`tauri-build`] inside your build script. +/// +/// You should either use [`tauri-build`] and this macro to include the compile time generated code, +/// or [`generate_context!`]. Do not use both at the same time, as they generate the same code and +/// will cause excess computations that will be discarded. +/// +/// [`tauri-build`]: https://docs.rs/tauri-build +#[macro_export] +macro_rules! tauri_build_context { + () => { + include!(concat!(env!("OUT_DIR"), "/tauri-build-context.rs")) + }; +} + +pub use pattern::Pattern; + +/// Whether we are running in development mode or not. +pub const fn is_dev() -> bool { + !cfg!(feature = "custom-protocol") +} + +/// Represents a container of file assets that are retrievable during runtime. +pub trait Assets: Send + Sync + 'static { + /// Initialize the asset provider. + fn setup(&self, app: &App) { + let _ = app; + } + + /// Get the content of the passed [`AssetKey`]. + fn get(&self, key: &AssetKey) -> Option>; + + /// Iterator for the assets. + fn iter(&self) -> Box>; + + /// Gets the hashes for the CSP tag of the HTML on the given path. + fn csp_hashes(&self, html_path: &AssetKey) -> Box> + '_>; +} + +impl Assets for EmbeddedAssets { + fn get(&self, key: &AssetKey) -> Option> { + EmbeddedAssets::get(self, key) + } + + fn iter(&self) -> Box> { + EmbeddedAssets::iter(self) + } + + fn csp_hashes(&self, html_path: &AssetKey) -> Box> + '_> { + EmbeddedAssets::csp_hashes(self, html_path) + } +} + +/// User supplied data required inside of a Tauri application. +/// +/// # Stability +/// This is the output of the [`generate_context`] macro, and is not considered part of the stable API. +/// Unless you know what you are doing and are prepared for this type to have breaking changes, do not create it yourself. +#[tauri_macros::default_runtime(Wry, wry)] +pub struct Context { + pub(crate) config: Config, + #[cfg(dev)] + pub(crate) config_parent: Option, + /// Asset provider. + pub assets: Box>, + pub(crate) default_window_icon: Option>, + pub(crate) app_icon: Option>, + #[cfg(all(desktop, feature = "tray-icon"))] + pub(crate) tray_icon: Option>, + pub(crate) package_info: PackageInfo, + pub(crate) pattern: Pattern, + pub(crate) runtime_authority: RuntimeAuthority, + pub(crate) plugin_global_api_scripts: Option<&'static [&'static str]>, +} + +/// Temporary struct that overrides the Debug formatting for the `app_icon` field. +/// +/// It reduces the output size compared to the default, as that would format the binary +/// data as a slice of numbers `[65, 66, 67]`. This instead shows the length of the Vec. +/// +/// For example: `Some([u8; 493])` +pub(crate) struct DebugAppIcon<'a>(&'a Option>); + +impl std::fmt::Debug for DebugAppIcon<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + Option::None => f.write_str("None"), + Option::Some(icon) => f + .debug_tuple("Some") + .field(&format_args!("[u8; {}]", icon.len())) + .finish(), + } + } +} + +impl fmt::Debug for Context { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut d = f.debug_struct("Context"); + d.field("config", &self.config) + .field("default_window_icon", &self.default_window_icon) + .field("app_icon", &DebugAppIcon(&self.app_icon)) + .field("package_info", &self.package_info) + .field("pattern", &self.pattern) + .field("plugin_global_api_scripts", &self.plugin_global_api_scripts); + + #[cfg(all(desktop, feature = "tray-icon"))] + d.field("tray_icon", &self.tray_icon); + + d.finish() + } +} + +impl Context { + /// The config the application was prepared with. + #[inline(always)] + pub fn config(&self) -> &Config { + &self.config + } + + /// A mutable reference to the config the application was prepared with. + /// + /// This allows changing the configuration at runtime before the app is built, + /// for instance to set the `app > appDirectoriesOverride` config from an environment variable: + /// + /// ```rust,no_run + /// use tauri::utils::config::AppDirectoriesOverride; + /// + /// let mut context = tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"); + /// + /// if let Ok(data_dir) = std::env::var("MY_APP_DATA_DIR") { + /// context.config_mut().app.app_directories_override = + /// Some(AppDirectoriesOverride::Root(data_dir.into())); + /// } + /// + /// tauri::Builder::default() + /// .run(context) + /// .expect("error while running tauri application"); + /// ``` + #[inline(always)] + pub fn config_mut(&mut self) -> &mut Config { + &mut self.config + } + + /// The assets to be served directly by Tauri. + #[inline(always)] + pub fn assets(&self) -> &dyn Assets { + self.assets.as_ref() + } + + /// Replace the [`Assets`] implementation and returns the previous value so you can use it as a fallback if desired. + #[inline(always)] + pub fn set_assets(&mut self, assets: Box>) -> Box> { + std::mem::replace(&mut self.assets, assets) + } + + /// The default window icon Tauri should use when creating windows. + #[inline(always)] + pub fn default_window_icon(&self) -> Option<&image::Image<'_>> { + self.default_window_icon.as_ref() + } + + /// Set the default window icon Tauri should use when creating windows. + #[inline(always)] + pub fn set_default_window_icon(&mut self, icon: Option>) { + self.default_window_icon = icon; + } + + /// The icon to use on the tray icon. + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "tray-icon"))))] + #[inline(always)] + pub fn tray_icon(&self) -> Option<&image::Image<'_>> { + self.tray_icon.as_ref() + } + + /// Set the icon to use on the tray icon. + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "tray-icon"))))] + #[inline(always)] + pub fn set_tray_icon(&mut self, icon: Option>) { + self.tray_icon = icon; + } + + /// Package information. + #[inline(always)] + pub fn package_info(&self) -> &PackageInfo { + &self.package_info + } + + /// A mutable reference to the package information. + #[inline(always)] + pub fn package_info_mut(&mut self) -> &mut PackageInfo { + &mut self.package_info + } + + /// The application pattern. + #[inline(always)] + pub fn pattern(&self) -> &Pattern { + &self.pattern + } + + /// A mutable reference to the resolved ACL. + /// + /// # Stability + /// + /// This API is unstable. + #[doc(hidden)] + #[inline(always)] + pub fn runtime_authority_mut(&mut self) -> &mut RuntimeAuthority { + &mut self.runtime_authority + } + + /// Create a new [`Context`] from the minimal required items. + #[inline(always)] + #[allow(clippy::too_many_arguments)] + pub fn new( + config: Config, + assets: Box>, + default_window_icon: Option>, + app_icon: Option>, + package_info: PackageInfo, + pattern: Pattern, + runtime_authority: RuntimeAuthority, + plugin_global_api_scripts: Option<&'static [&'static str]>, + ) -> Self { + Self { + config, + #[cfg(dev)] + config_parent: None, + assets, + default_window_icon, + app_icon, + #[cfg(all(desktop, feature = "tray-icon"))] + tray_icon: None, + package_info, + pattern, + runtime_authority, + plugin_global_api_scripts, + } + } + + #[cfg(dev)] + #[doc(hidden)] + pub fn with_config_parent(&mut self, config_parent: impl AsRef) { + self + .config_parent + .replace(config_parent.as_ref().to_owned()); + } +} + +// TODO: expand these docs +/// Manages a running application. +pub trait Manager: sealed::ManagerBase { + /// The application handle associated with this manager. + fn app_handle(&self) -> &AppHandle { + self.managed_app_handle() + } + + /// The [`Config`] the manager was created with. + fn config(&self) -> &Config { + self.manager().config() + } + + /// The [`PackageInfo`] the manager was created with. + fn package_info(&self) -> &PackageInfo { + self.manager().package_info() + } + + /// Fetch a single window from the manager. + #[cfg(feature = "unstable")] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] + fn get_window(&self, label: &str) -> Option> { + self.manager().get_window(label) + } + + /// Fetch the focused window. Returns `None` if there is not any focused window. + #[cfg(feature = "unstable")] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] + fn get_focused_window(&self) -> Option> { + self.manager().get_focused_window() + } + + /// Fetch all managed windows. + #[cfg(feature = "unstable")] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] + fn windows(&self) -> HashMap> { + self.manager().windows() + } + + /// Fetch a single webview from the manager. + #[cfg(feature = "unstable")] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] + fn get_webview(&self, label: &str) -> Option> { + self.manager().get_webview(label) + } + + /// Fetch all managed webviews. + #[cfg(feature = "unstable")] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] + fn webviews(&self) -> HashMap> { + self.manager().webviews() + } + + /// Fetch a single webview window from the manager. + fn get_webview_window(&self, label: &str) -> Option> { + self.manager().get_webview(label).and_then(|webview| { + let window = webview.window(); + if window.is_webview_window() { + Some(WebviewWindow { window, webview }) + } else { + None + } + }) + } + + /// Fetch all managed webview windows. + fn webview_windows(&self) -> HashMap> { + self + .manager() + .webviews() + .into_iter() + .filter_map(|(label, webview)| { + let window = webview.window(); + if window.is_webview_window() { + Some((label, WebviewWindow { window, webview })) + } else { + None + } + }) + .collect::>() + } + + /// Add `state` to the state managed by the application. + /// + /// If the state for the `T` type has previously been set, the state is unchanged and false is returned. Otherwise true is returned. + /// + /// Managed state can be retrieved by any command handler via the + /// [`State`] guard. In particular, if a value of type `T` + /// is managed by Tauri, adding `State` to the list of arguments in a + /// command handler instructs Tauri to retrieve the managed value. + /// Additionally, [`state`](Self#method.state) can be used to retrieve the value manually. + /// + /// # Mutability + /// + /// Since the managed state is global and must be [`Send`] + [`Sync`], mutations can only happen through interior mutability: + /// + /// ```rust,no_run + /// use std::{collections::HashMap, sync::Mutex}; + /// use tauri::State; + /// // here we use Mutex to achieve interior mutability + /// struct Storage { + /// store: Mutex>, + /// } + /// struct Connection; + /// struct DbConnection { + /// db: Mutex>, + /// } + /// + /// #[tauri::command] + /// fn connect(connection: State) { + /// // initialize the connection, mutating the state with interior mutability + /// *connection.db.lock().unwrap() = Some(Connection {}); + /// } + /// + /// #[tauri::command] + /// fn storage_insert(key: u64, value: String, storage: State) { + /// // mutate the storage behind the Mutex + /// storage.store.lock().unwrap().insert(key, value); + /// } + /// + /// tauri::Builder::default() + /// .manage(Storage { store: Default::default() }) + /// .manage(DbConnection { db: Default::default() }) + /// .invoke_handler(tauri::generate_handler![connect, storage_insert]) + /// // on an actual app, remove the string argument + /// .run(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) + /// .expect("error while running tauri application"); + /// ``` + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri::{Manager, State}; + /// + /// struct MyInt(isize); + /// struct MyString(String); + /// + /// #[tauri::command] + /// fn int_command(state: State) -> String { + /// format!("The stateful int is: {}", state.0) + /// } + /// + /// #[tauri::command] + /// fn string_command<'r>(state: State<'r, MyString>) { + /// println!("state: {}", state.0); + /// } + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// app.manage(MyInt(0)); + /// app.manage(MyString("tauri".into())); + /// // `MyInt` is already managed, so `manage()` returns false + /// assert!(!app.manage(MyInt(1))); + /// // read the `MyInt` managed state with the turbofish syntax + /// let int = app.state::(); + /// assert_eq!(int.0, 0); + /// // read the `MyString` managed state with the `State` guard + /// let val: State = app.state(); + /// assert_eq!(val.0, "tauri"); + /// Ok(()) + /// }) + /// .invoke_handler(tauri::generate_handler![int_command, string_command]) + /// // on an actual app, remove the string argument + /// .run(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) + /// .expect("error while running tauri application"); + /// ``` + fn manage(&self, state: T) -> bool + where + T: Send + Sync + 'static, + { + self.manager().state.set(state) + } + + /// Removes the state managed by the application for T. Returns the state if it was actually removed. + /// + ///

+ /// + /// This method is *UNSAFE* and calling it will cause previously obtained references through + /// [Manager::state] and [State::inner] to become dangling references. + /// + /// It is currently deprecated and may be removed in the future. + /// + /// If you really want to unmanage a state, use [std::sync::Mutex] and [Option::take] to wrap the state instead. + /// + /// See [tauri-apps/tauri#12721] for more information. + /// + /// [tauri-apps/tauri#12721]: https://github.com/tauri-apps/tauri/issues/12721 + /// + ///
+ #[deprecated( + since = "2.3.0", + note = "This method is unsafe, since it can cause dangling references." + )] + fn unmanage(&self) -> Option + where + T: Send + Sync + 'static, + { + // The caller decides to break the safety here, then OK, just let it go. + unsafe { self.manager().state.unmanage() } + } + + /// Retrieves the managed state for the type `T`. + /// + /// # Panics + /// + /// Panics if the state for the type `T` has not been previously [managed](Self::manage). + /// Use [try_state](Self::try_state) for a non-panicking version. + fn state(&self) -> State<'_, T> + where + T: Send + Sync + 'static, + { + self.manager().state.try_get().unwrap_or_else(|| { + panic!( + "state() called before manage() for {}", + std::any::type_name::() + ) + }) + } + + /// Attempts to retrieve the managed state for the type `T`. + /// + /// Returns `Some` if the state has previously been [managed](Self::manage). Otherwise returns `None`. + fn try_state(&self) -> Option> + where + T: Send + Sync + 'static, + { + self.manager().state.try_get() + } + + /// Get a reference to the resources table of this manager. + fn resources_table(&self) -> MutexGuard<'_, ResourceTable>; + + /// Gets the managed [`Env`]. + fn env(&self) -> Env { + use std::ops::Deref; + self.state::().deref().clone() + } + + /// Gets the scope for the asset protocol. + #[cfg(feature = "protocol-asset")] + fn asset_protocol_scope(&self) -> scope::fs::Scope { + self.state::().asset_protocol.clone() + } + + /// The path resolver. + fn path(&self) -> &crate::path::PathResolver { + self.state::>().inner() + } + + /// Adds a capability to the app. + /// + /// Note that by default every capability file in the `src-tauri/capabilities` folder + /// are automatically enabled unless specific capabilities are configured in [`tauri.conf.json > app > security > capabilities`], + /// so you should use a different director for the runtime-added capabilities or use [tauri_build::Attributes::capabilities_path_pattern]. + /// + /// # Examples + /// ``` + /// use tauri::Manager; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// #[cfg(feature = "beta")] + /// app.add_capability(include_str!("../capabilities/beta/cap.json")); + /// + /// #[cfg(feature = "stable")] + /// app.add_capability(include_str!("../capabilities/stable/cap.json")); + /// Ok(()) + /// }); + /// ``` + /// + /// The above example assumes the following directory layout: + /// ```md + /// ├── capabilities + /// │ ├── app (default capabilities used by any app flavor) + /// | | |-- cap.json + /// │ ├── beta (capabilities only added to a `beta` flavor) + /// | | |-- cap.json + /// │ ├── stable (capabilities only added to a `stable` flavor) + /// | |-- cap.json + /// ``` + /// + /// For this layout to be properly parsed by Tauri, we need to change the build script to + /// + /// ```skip + /// // only pick up capabilities in the capabilities/app folder by default + /// let attributes = tauri_build::Attributes::new().capabilities_path_pattern("./capabilities/app/*.json"); + /// tauri_build::try_build(attributes).unwrap(); + /// ``` + /// + /// [`tauri.conf.json > app > security > capabilities`]: https://tauri.app/reference/config/#capabilities + /// [tauri_build::Attributes::capabilities_path_pattern]: https://docs.rs/tauri-build/2/tauri_build/struct.Attributes.html#method.capabilities_path_pattern + #[cfg(feature = "dynamic-acl")] + fn add_capability(&self, capability: impl RuntimeCapability) -> Result<()> { + self + .manager() + .runtime_authority + .lock() + .unwrap() + .add_capability(capability) + } +} + +/// Listen to events. +pub trait Listener: sealed::ManagerBase { + /// Listen to an emitted event on this manager. + /// + /// # Examples + /// ``` + /// use tauri::{Manager, Listener, Emitter}; + /// + /// #[tauri::command] + /// fn synchronize(window: tauri::Window) { + /// // emits the synchronized event to all windows + /// window.emit("synchronized", ()); + /// } + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// app.listen("synchronized", |event| { + /// println!("app is in sync"); + /// }); + /// Ok(()) + /// }) + /// .invoke_handler(tauri::generate_handler![synchronize]); + /// ``` + /// # Panics + /// Will panic if `event` contains characters other than alphanumeric, `-`, `/`, `:` and `_` + fn listen(&self, event: impl Into, handler: F) -> EventId + where + F: Fn(Event) + Send + 'static; + + /// Listen to an event on this manager only once. + /// + /// See [`Self::listen`] for more information. + /// # Panics + /// Will panic if `event` contains characters other than alphanumeric, `-`, `/`, `:` and `_` + fn once(&self, event: impl Into, handler: F) -> EventId + where + F: FnOnce(Event) + Send + 'static; + + /// Remove an event listener. + /// + /// # Examples + /// ``` + /// use tauri::{Manager, Listener}; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let handle = app.handle().clone(); + /// let handler = app.listen_any("ready", move |event| { + /// println!("app is ready"); + /// + /// // we no longer need to listen to the event + /// // we also could have used `app.once_global` instead + /// handle.unlisten(event.id()); + /// }); + /// + /// // stop listening to the event when you do not need it anymore + /// app.unlisten(handler); + /// + /// + /// Ok(()) + /// }); + /// ``` + fn unlisten(&self, id: EventId); + + /// Listen to an emitted event to any [target](EventTarget). + /// + /// # Examples + /// ``` + /// use tauri::{Manager, Emitter, Listener}; + /// + /// #[tauri::command] + /// fn synchronize(window: tauri::Window) { + /// // emits the synchronized event to all windows + /// window.emit("synchronized", ()); + /// } + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// app.listen_any("synchronized", |event| { + /// println!("app is in sync"); + /// }); + /// Ok(()) + /// }) + /// .invoke_handler(tauri::generate_handler![synchronize]); + /// ``` + /// # Panics + /// Will panic if `event` contains characters other than alphanumeric, `-`, `/`, `:` and `_` + fn listen_any(&self, event: impl Into, handler: F) -> EventId + where + F: Fn(Event) + Send + 'static, + { + let event = EventName::new(event.into()).unwrap(); + self.manager().listen(event, EventTarget::Any, handler) + } + + /// Listens once to an emitted event to any [target](EventTarget) . + /// + /// See [`Self::listen_any`] for more information. + /// # Panics + /// Will panic if `event` contains characters other than alphanumeric, `-`, `/`, `:` and `_` + fn once_any(&self, event: impl Into, handler: F) -> EventId + where + F: FnOnce(Event) + Send + 'static, + { + let event = EventName::new(event.into()).unwrap(); + self.manager().once(event, EventTarget::Any, handler) + } +} + +/// Emit events. +pub trait Emitter: sealed::ManagerBase { + /// Emits an event to all [targets](EventTarget). + /// + /// # Examples + /// ``` + /// use tauri::Emitter; + /// + /// #[tauri::command] + /// fn synchronize(app: tauri::AppHandle) { + /// // emits the synchronized event to all webviews + /// app.emit("synchronized", ()); + /// } + /// ``` + fn emit(&self, event: &str, payload: S) -> Result<()> { + let event = EventName::new(event)?; + let payload = EmitPayload::Serialize(&payload); + self.manager().emit(event, payload) + } + + /// Similar to [`Emitter::emit`] but the payload is json serialized. + fn emit_str(&self, event: &str, payload: String) -> Result<()> { + let event = EventName::new(event)?; + let payload = EmitPayload::<()>::Str(payload); + self.manager().emit(event, payload) + } + + /// Emits an event to all [targets](EventTarget) matching the given target. + /// + /// # Examples + /// ``` + /// use tauri::{Emitter, EventTarget}; + /// + /// #[tauri::command] + /// fn download(app: tauri::AppHandle) { + /// for i in 1..100 { + /// std::thread::sleep(std::time::Duration::from_millis(150)); + /// // emit a download progress event to all listeners + /// app.emit_to(EventTarget::any(), "download-progress", i); + /// // emit an event to listeners that used App::listen or AppHandle::listen + /// app.emit_to(EventTarget::app(), "download-progress", i); + /// // emit an event to any webview/window/webviewWindow matching the given label + /// app.emit_to("updater", "download-progress", i); // similar to using EventTarget::labeled + /// app.emit_to(EventTarget::labeled("updater"), "download-progress", i); + /// // emit an event to listeners that used WebviewWindow::listen + /// app.emit_to(EventTarget::webview_window("updater"), "download-progress", i); + /// } + /// } + /// ``` + fn emit_to(&self, target: I, event: &str, payload: S) -> Result<()> + where + I: Into, + S: Serialize + Clone, + { + let event = EventName::new(event)?; + let payload = EmitPayload::Serialize(&payload); + self.manager().emit_to(target.into(), event, payload) + } + + /// Similar to [`Emitter::emit_to`] but the payload is json serialized. + fn emit_str_to(&self, target: I, event: &str, payload: String) -> Result<()> + where + I: Into, + { + let event = EventName::new(event)?; + let payload = EmitPayload::<()>::Str(payload); + self.manager().emit_to(target.into(), event, payload) + } + + /// Emits an event to all [targets](EventTarget) based on the given filter. + /// + /// # Examples + /// ``` + /// use tauri::{Emitter, EventTarget}; + /// + /// #[tauri::command] + /// fn download(app: tauri::AppHandle) { + /// for i in 1..100 { + /// std::thread::sleep(std::time::Duration::from_millis(150)); + /// // emit a download progress event to the updater window + /// app.emit_filter("download-progress", i, |t| match t { + /// EventTarget::WebviewWindow { label } => label == "main", + /// _ => false, + /// }); + /// } + /// } + /// ``` + fn emit_filter(&self, event: &str, payload: S, filter: F) -> Result<()> + where + S: Serialize + Clone, + F: Fn(&EventTarget) -> bool, + { + let event = EventName::new(event)?; + let payload = EmitPayload::Serialize(&payload); + self.manager().emit_filter(event, payload, filter) + } + + /// Similar to [`Emitter::emit_filter`] but the payload is json serialized. + fn emit_str_filter(&self, event: &str, payload: String, filter: F) -> Result<()> + where + F: Fn(&EventTarget) -> bool, + { + let event = EventName::new(event)?; + let payload = EmitPayload::<()>::Str(payload); + self.manager().emit_filter(event, payload, filter) + } +} + +/// Prevent implementation details from leaking out of the [`Manager`] trait. +pub(crate) mod sealed { + use super::Runtime; + use crate::{app::AppHandle, manager::AppManager}; + use std::sync::Arc; + + /// A running [`Runtime`] or a dispatcher to it. + pub enum RuntimeOrDispatch<'r, R: Runtime> { + /// Reference to the running [`Runtime`]. + Runtime(&'r R), + + /// Handle to the running [`Runtime`]. + RuntimeHandle(R::Handle), + + /// A dispatcher to the running [`Runtime`]. + Dispatch(R::WindowDispatcher), + } + + /// Managed handle to the application runtime. + pub trait ManagerBase { + fn manager(&self) -> &AppManager; + fn manager_owned(&self) -> Arc>; + fn runtime(&self) -> RuntimeOrDispatch<'_, R>; + fn managed_app_handle(&self) -> &AppHandle; + #[cfg(target_os = "android")] + fn activity_name(&self) -> Option>; + #[cfg(target_os = "ios")] + fn scene_identifier(&self) -> Option>; + } +} + +#[cfg(desktop)] +struct UnsafeSend(T); +#[cfg(desktop)] +unsafe impl Send for UnsafeSend {} + +#[cfg(desktop)] +impl UnsafeSend { + fn take(self) -> T { + self.0 + } +} + +#[cfg(desktop)] +macro_rules! run_main_thread { + ($handle:ident, $ex:expr) => {{ + use std::sync::mpsc::channel; + let (tx, rx) = channel(); + let task = move || { + let f = $ex; + let _ = tx.send(f()); + }; + $handle + .run_on_main_thread(task) + .and_then(|_| rx.recv().map_err(|_| crate::Error::FailedToReceiveMessage)) + }}; +} + +#[cfg(desktop)] +pub(crate) use run_main_thread; + +#[cfg(any(test, feature = "test"))] +#[cfg_attr(docsrs, doc(cfg(feature = "test")))] +pub mod test; + +#[cfg(feature = "specta")] +const _: () = { + use specta::{TypeMap, datatype::DataType, function::FunctionArg}; + + impl FunctionArg for crate::State<'_, T> { + fn to_datatype(_: &mut TypeMap) -> Option { + None + } + } + + impl FunctionArg for crate::AppHandle { + fn to_datatype(_: &mut TypeMap) -> Option { + None + } + } + + impl FunctionArg for crate::Window { + fn to_datatype(_: &mut TypeMap) -> Option { + None + } + } + + impl FunctionArg for crate::Webview { + fn to_datatype(_: &mut TypeMap) -> Option { + None + } + } + + impl FunctionArg for crate::WebviewWindow { + fn to_datatype(_: &mut TypeMap) -> Option { + None + } + } +}; + +#[cfg(test)] +mod tests { + use cargo_toml::Manifest; + use std::{env::var, fs::read_to_string, path::PathBuf, sync::OnceLock}; + + static MANIFEST: OnceLock = OnceLock::new(); + const CHECKED_FEATURES: &str = include_str!(concat!(env!("OUT_DIR"), "/checked_features")); + + fn get_manifest() -> &'static Manifest { + MANIFEST.get_or_init(|| { + let manifest_dir = PathBuf::from(var("CARGO_MANIFEST_DIR").unwrap()); + Manifest::from_path(manifest_dir.join("Cargo.toml")).expect("failed to parse Cargo manifest") + }) + } + + #[test] + fn features_are_documented() { + let manifest_dir = PathBuf::from(var("CARGO_MANIFEST_DIR").unwrap()); + let lib_code = read_to_string(manifest_dir.join("src/lib.rs")).expect("failed to read lib.rs"); + + for f in get_manifest().features.keys() { + if !(f.starts_with("__") || f == "default" || lib_code.contains(&format!("*{f}**"))) { + panic!("Feature {f} is not documented"); + } + } + } + + #[test] + fn aliased_features_exist() { + let checked_features = CHECKED_FEATURES.split(','); + let manifest = get_manifest(); + for checked_feature in checked_features { + if !manifest.features.iter().any(|(f, _)| f == checked_feature) { + panic!( + "Feature {checked_feature} was checked in the alias build step but it does not exist in crates/tauri/Cargo.toml" + ); + } + } + } +} + +#[cfg(test)] +mod test_utils { + use proptest::prelude::*; + + pub fn assert_send() {} + pub fn assert_sync() {} + + #[allow(dead_code)] + pub fn assert_not_allowlist_error(res: anyhow::Result) { + if let Err(e) = res { + assert!(!e.to_string().contains("not on the allowlist")); + } + } + + proptest! { + #![proptest_config(ProptestConfig::with_cases(10000))] + #[test] + // check to see if spawn executes a function. + fn check_spawn_task(task in "[a-z]+") { + // create dummy task function + let dummy_task = async move { + let _ = format!("{task}-run-dummy-task"); + }; + // call spawn + crate::async_runtime::spawn(dummy_task); + } + } +} + +/// Simple dependency-free string encoder using [Z85]. +mod z85 { + const TABLE: &[u8; 85] = + b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#"; + + /// Encode bytes with [Z85]. + /// + /// # Panics + /// + /// Will panic if the input bytes are not a multiple of 4. + pub fn encode(bytes: &[u8]) -> String { + assert_eq!(bytes.len() % 4, 0); + + let mut buf = String::with_capacity(bytes.len() * 5 / 4); + for chunk in bytes.as_chunks::<4>().0 { + let mut chars = [0u8; 5]; + let mut chunk = u32::from_be_bytes(*chunk) as usize; + for byte in chars.iter_mut().rev() { + *byte = TABLE[chunk % 85]; + chunk /= 85; + } + + buf.push_str(std::str::from_utf8(&chars).unwrap()); + } + + buf + } + + #[cfg(test)] + mod tests { + #[test] + fn encode() { + assert_eq!( + super::encode(&[0x86, 0x4F, 0xD2, 0x6F, 0xB5, 0x59, 0xF7, 0x5B]), + "HelloWorld" + ); + } + } +} + +/// Generate a random 128-bit [Z85] encoded [`String`]. +/// +/// [Z85]: https://rfc.zeromq.org/spec/32/ +pub(crate) fn generate_invoke_key() -> Result { + let mut bytes = [0u8; 16]; + getrandom::fill(&mut bytes)?; + Ok(z85::encode(&bytes)) +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/manager/menu.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/manager/menu.rs new file mode 100644 index 000000000..10c6b0b54 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/manager/menu.rs @@ -0,0 +1,100 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + collections::HashMap, + sync::{Arc, Mutex, MutexGuard}, +}; + +use crate::{ + AppHandle, Runtime, Window, + menu::{Menu, MenuEvent, MenuId}, +}; + +pub struct MenuManager { + /// A set containing a reference to the active menus, including + /// the app-wide menu and the window-specific menus + /// + /// This should be mainly used to access [`Menu::haccel`] + /// to setup the accelerator handling in the event loop + pub menus: Arc>>>, + /// The menu set to all windows. + pub menu: Mutex>>, + /// Menu event listeners to all windows. + pub global_event_listeners: Mutex>>>, + /// Menu event listeners to specific windows. + pub event_listeners: Mutex>>>, +} + +impl MenuManager { + /// App-wide menu. + pub fn menu_lock(&self) -> MutexGuard<'_, Option>> { + self.menu.lock().expect("poisoned menu mutex") + } + + pub fn menus_stash_lock(&self) -> MutexGuard<'_, HashMap>> { + self.menus.lock().expect("poisoned menu mutex") + } + + pub fn is_menu_in_use>(&self, id: &I) -> bool { + self + .menu_lock() + .as_ref() + .map(|m| id.eq(m.id())) + .unwrap_or(false) + } + + pub fn insert_menu_into_stash(&self, menu: &Menu) { + self + .menus_stash_lock() + .insert(menu.id().clone(), menu.clone()); + } + + pub(crate) fn prepare_window_menu_creation_handler( + &self, + window_menu: Option<&crate::window::WindowMenu>, + #[allow(unused)] theme: Option, + ) -> Option) + use> { + let window_menu = window_menu?; + + self + .menus_stash_lock() + .insert(window_menu.menu.id().clone(), window_menu.menu.clone()); + + #[cfg(target_os = "macos")] + return None; + + #[cfg_attr(target_os = "macos", allow(unused_variables, unreachable_code))] + { + let menu = window_menu.menu.clone(); + Some(move |raw: tauri_runtime::window::RawWindow<'_>| { + #[cfg(target_os = "windows")] + { + let theme = theme + .map(crate::menu::map_to_menu_theme) + .unwrap_or(muda::MenuTheme::Auto); + let _ = unsafe { menu.inner().init_for_hwnd_with_theme(raw.hwnd as _, theme) }; + } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + let _ = menu + .inner() + .init_for_gtk_window(raw.gtk_window, raw.default_vbox); + }) + } + } + + pub fn on_menu_event, MenuEvent) + Send + Sync + 'static>(&self, handler: F) { + self + .global_event_listeners + .lock() + .unwrap() + .push(Box::new(handler)); + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/manager/mod.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/manager/mod.rs new file mode 100644 index 000000000..1cfd5e47c --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/manager/mod.rs @@ -0,0 +1,1041 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + borrow::Cow, + collections::HashMap, + fmt, + sync::{Arc, Mutex, MutexGuard, atomic::AtomicBool}, +}; + +use serde::Serialize; +use url::Url; + +use tauri_macros::default_runtime; +use tauri_utils::{ + assets::{AssetKey, CspHash, SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN}, + config::{Csp, CspDirectiveSources}, +}; + +use crate::{ + Assets, Context, DebugAppIcon, EventName, Pattern, Runtime, StateManager, Webview, Window, + app::{ + AppHandle, ChannelInterceptor, GlobalWebviewEventListener, GlobalWindowEventListener, + OnPageLoad, + }, + event::{EmitArgs, Event, EventId, EventTarget, Listeners}, + ipc::{Invoke, InvokeHandler, RuntimeAuthority}, + plugin::PluginStore, + resources::ResourceTable, + utils::{PackageInfo, config::Config}, +}; + +#[cfg(any(target_os = "macos", target_os = "ios"))] +use crate::app::OnWebContentProcessTerminate; + +#[cfg(desktop)] +mod menu; +#[cfg(all(desktop, feature = "tray-icon"))] +mod tray; +pub mod webview; +pub mod window; + +#[derive(Default)] +/// Spaced and quoted Content-Security-Policy hash values. +struct CspHashStrings { + script: Vec, + style: Vec, +} + +/// Sets the CSP value to the asset HTML if needed (on Linux). +/// Returns the CSP string for access on the response header (on Windows and macOS). +pub(crate) fn set_csp( + asset: &mut String, + assets: &impl std::borrow::Borrow>, + asset_path: &AssetKey, + manager: &AppManager, + csp: Csp, +) -> HashMap { + let mut csp = csp.into(); + let hash_strings = + assets + .borrow() + .csp_hashes(asset_path) + .fold(CspHashStrings::default(), |mut acc, hash| { + match hash { + CspHash::Script(hash) => { + acc.script.push(hash.into()); + } + CspHash::Style(hash) => { + acc.style.push(hash.into()); + } + _csp_hash => { + log::debug!("Unknown CspHash variant encountered: {:?}", _csp_hash); + } + } + + acc + }); + + let dangerous_disable_asset_csp_modification = &manager + .config() + .app + .security + .dangerous_disable_asset_csp_modification; + if dangerous_disable_asset_csp_modification.can_modify("script-src") { + replace_csp_nonce( + asset, + SCRIPT_NONCE_TOKEN, + &mut csp, + "script-src", + hash_strings.script, + ); + } + + if dangerous_disable_asset_csp_modification.can_modify("style-src") { + replace_csp_nonce( + asset, + STYLE_NONCE_TOKEN, + &mut csp, + "style-src", + hash_strings.style, + ); + } + + csp +} + +// inspired by +fn replace_with_callback String>( + original: &str, + pattern: &str, + mut replacement: F, +) -> String { + let mut result = String::new(); + let mut last_end = 0; + for (start, part) in original.match_indices(pattern) { + result.push_str(unsafe { original.get_unchecked(last_end..start) }); + result.push_str(&replacement()); + last_end = start + part.len(); + } + result.push_str(unsafe { original.get_unchecked(last_end..original.len()) }); + result +} + +fn replace_csp_nonce( + asset: &mut String, + token: &str, + csp: &mut HashMap, + directive: &str, + hashes: Vec, +) { + let mut nonces = Vec::new(); + *asset = replace_with_callback(asset, token, || { + let nonce = getrandom::u64().expect("failed to get random bytes"); + nonces.push(nonce); + nonce.to_string() + }); + + if !(nonces.is_empty() && hashes.is_empty()) { + let nonce_sources = nonces + .into_iter() + .map(|n| format!("'nonce-{n}'")) + .collect::>(); + let sources = csp.entry(directive.into()).or_default(); + let self_source = "'self'".to_string(); + if !sources.contains(&self_source) { + sources.push(self_source); + } + sources.extend(nonce_sources); + sources.extend(hashes); + } +} + +/// A resolved asset. +#[non_exhaustive] +pub struct Asset { + /// The asset bytes. + pub bytes: Vec, + /// The asset's mime type. + pub mime_type: String, + /// The `Content-Security-Policy` header value. + pub csp_header: Option, +} + +impl Asset { + /// The asset bytes. + pub fn bytes(&self) -> &[u8] { + &self.bytes + } + + /// The asset's mime type. + pub fn mime_type(&self) -> &str { + &self.mime_type + } + + /// The `Content-Security-Policy` header value. + pub fn csp_header(&self) -> Option<&str> { + self.csp_header.as_deref() + } +} + +#[default_runtime(crate::Wry, wry)] +pub struct AppManager { + pub runtime_authority: Mutex, + pub window: window::WindowManager, + pub webview: webview::WebviewManager, + #[cfg(all(desktop, feature = "tray-icon"))] + pub tray: tray::TrayManager, + #[cfg(desktop)] + pub menu: menu::MenuManager, + + pub(crate) plugins: Mutex>, + pub listeners: Listeners, + pub state: Arc, + pub config: Config, + #[cfg(dev)] + pub config_parent: Option, + pub assets: Box>, + + pub app_icon: Option>, + + pub package_info: PackageInfo, + + /// Application pattern. + pub pattern: Arc, + + /// Global API scripts collected from plugins. + pub plugin_global_api_scripts: Arc>, + + /// Application Resources Table + pub(crate) resources_table: Arc>, + + /// Runtime-generated invoke key. + pub(crate) invoke_key: String, + + pub(crate) channel_interceptor: Option>, + + /// Sets to true in [`AppHandle::request_restart`] and [`AppHandle::restart`] + /// and we will call `restart` on the next `RuntimeRunEvent::Exit` event + pub(crate) restart_on_exit: AtomicBool, +} + +impl fmt::Debug for AppManager { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut d = f.debug_struct("AppManager"); + + d.field("window", &self.window) + .field("plugins", &self.plugins) + .field("state", &self.state) + .field("config", &self.config) + .field("app_icon", &DebugAppIcon(&self.app_icon)) + .field("package_info", &self.package_info) + .field("pattern", &self.pattern); + + #[cfg(all(desktop, feature = "tray-icon"))] + { + d.field("tray", &self.tray); + } + + d.finish() + } +} + +pub(crate) enum EmitPayload<'a, S: Serialize> { + Serialize(&'a S), + Str(String), +} + +impl AppManager { + #[allow(clippy::too_many_arguments, clippy::type_complexity)] + pub(crate) fn with_handlers( + #[allow(unused_mut)] mut context: Context, + plugins: PluginStore, + invoke_handler: Box>, + on_page_load: Option>>, + on_permission_request: Option>>, + #[cfg(any(target_os = "macos", target_os = "ios"))] on_web_content_process_terminate: Option< + Arc>, + >, + uri_scheme_protocols: HashMap>>, + state: StateManager, + #[cfg(desktop)] menu_event_listener: Vec>>, + #[cfg(all(desktop, feature = "tray-icon"))] tray_icon_event_listeners: Vec< + crate::app::GlobalTrayIconEventListener>, + >, + window_event_listeners: Vec>, + webview_event_listeners: Vec>, + #[cfg(desktop)] window_menu_event_listeners: HashMap< + String, + crate::app::GlobalMenuEventListener>, + >, + invoke_initialization_script: String, + channel_interceptor: Option>, + invoke_key: String, + ) -> Self { + // generate a random isolation key at runtime + #[cfg(feature = "isolation")] + if let Pattern::Isolation { key, .. } = &mut context.pattern { + *key = uuid::Uuid::new_v4().to_string(); + } + + Self { + runtime_authority: Mutex::new(context.runtime_authority), + window: window::WindowManager { + windows: Mutex::default(), + default_icon: context.default_window_icon, + event_listeners: Arc::new(window_event_listeners), + }, + webview: webview::WebviewManager { + webviews: Mutex::default(), + invoke_handler, + on_page_load, + on_permission_request, + #[cfg(any(target_os = "macos", target_os = "ios"))] + on_web_content_process_terminate, + uri_scheme_protocols: Mutex::new(uri_scheme_protocols), + event_listeners: Arc::new(webview_event_listeners), + invoke_initialization_script, + invoke_key: invoke_key.clone(), + }, + #[cfg(all(desktop, feature = "tray-icon"))] + tray: tray::TrayManager { + icon: context.tray_icon, + icons: Default::default(), + global_event_listeners: Mutex::new(tray_icon_event_listeners), + event_listeners: Default::default(), + }, + #[cfg(desktop)] + menu: menu::MenuManager { + menus: Default::default(), + menu: Default::default(), + global_event_listeners: Mutex::new(menu_event_listener), + event_listeners: Mutex::new(window_menu_event_listeners), + }, + plugins: Mutex::new(plugins), + listeners: Listeners::default(), + state: Arc::new(state), + config: context.config, + #[cfg(dev)] + config_parent: context.config_parent, + assets: context.assets, + app_icon: context.app_icon, + package_info: context.package_info, + pattern: Arc::new(context.pattern), + plugin_global_api_scripts: Arc::new(context.plugin_global_api_scripts), + resources_table: Arc::default(), + invoke_key, + channel_interceptor, + restart_on_exit: AtomicBool::new(false), + } + } + + /// The `tauri` custom protocol URL we use to serve the embedded assets. + /// Returns `tauri://localhost` or its `wry` workaround URL `http://tauri.localhost`/`https://tauri.localhost` + pub(crate) fn tauri_protocol_url(&self, https: bool) -> Cow<'_, Url> { + Cow::Owned(Url::parse(&crate::protocol::origin("tauri", https)).unwrap()) + } + + /// Get the base app URL for [`WebviewUrl::App`](tauri_utils::config::WebviewUrl::App). + /// + /// * In dev mode, this is the [`devUrl`](tauri_utils::config::BuildConfig::dev_url) configuration value if it exists. + /// * In production mode, this is the [`frontendDist`](tauri_utils::config::BuildConfig::frontend_dist) configuration value if it's a [`FrontendDist::Url`](tauri_utils::config::FrontendDist::Url). + /// * Returns [`Self::tauri_protocol_url`] (e.g. `tauri://localhost`) otherwise. + pub(crate) fn get_app_url(&self, https: bool) -> Cow<'_, Url> { + #[cfg(dev)] + let url = self.config.build.dev_url.as_ref(); + #[cfg(not(dev))] + let url = match self.config.build.frontend_dist.as_ref() { + Some(crate::utils::config::FrontendDist::Url(url)) => Some(url), + _ => None, + }; + + if let Some(url) = url { + Cow::Borrowed(url) + } else { + self.tauri_protocol_url(https) + } + } + + fn csp(&self) -> Option { + if !crate::is_dev() { + self.config.app.security.csp.clone() + } else { + self + .config + .app + .security + .dev_csp + .clone() + .or_else(|| self.config.app.security.csp.clone()) + } + } + + pub fn get_asset(&self, mut path: String, _use_https_schema: bool) -> crate::Result { + let assets = &self.assets; + if path.ends_with('/') { + path.pop(); + } + path = percent_encoding::percent_decode(path.as_bytes()) + .decode_utf8_lossy() + .to_string(); + let path = if path.is_empty() { + // if the url is `tauri://localhost`, we should load `index.html` + "index.html".to_string() + } else { + // skip the leading `/`, if it starts with one. + path.strip_prefix('/').unwrap_or(path.as_str()).to_string() + }; + + let mut asset_path = AssetKey::from(path.as_str()); + + let asset_response = assets + .get(&asset_path) + .or_else(|| { + log::debug!("Asset `{path}` not found; fallback to {path}.html"); + let fallback = format!("{path}.html").into(); + let asset = assets.get(&fallback); + asset_path = fallback; + asset + }) + .or_else(|| { + log::debug!("Asset `{path}` not found; fallback to {path}/index.html",); + let fallback = format!("{path}/index.html").into(); + let asset = assets.get(&fallback); + asset_path = fallback; + asset + }) + .or_else(|| { + log::debug!("Asset `{path}` not found; fallback to index.html"); + let fallback = AssetKey::from("index.html"); + let asset = assets.get(&fallback); + asset_path = fallback; + asset + }) + .ok_or_else(|| { + let error = crate::Error::AssetNotFound(path.clone()); + log::error!("{error}"); + error + })?; + + let mut csp_header = None; + let is_html = asset_path.as_ref().ends_with(".html"); + + let final_data = if is_html { + let mut asset = String::from_utf8_lossy(&asset_response).into_owned(); + if let Some(csp) = self.csp() { + #[allow(unused_mut)] + let mut csp_map = set_csp(&mut asset, &self.assets, &asset_path, self, csp); + #[cfg(feature = "isolation")] + if let Pattern::Isolation { schema, .. } = &*self.pattern { + let default_src = csp_map.entry("default-src".to_owned()).or_default(); + default_src.push(crate::pattern::format_real_schema( + schema, + _use_https_schema, + )); + } + + csp_header.replace(Csp::DirectiveMap(csp_map).to_string()); + } + + asset.into_bytes() + } else { + asset_response.into_owned() + }; + let mime_type = tauri_utils::mime_type::MimeType::parse(&final_data, &path); + Ok(Asset { + bytes: final_data, + mime_type, + csp_header, + }) + } + + pub(crate) fn listeners(&self) -> &Listeners { + &self.listeners + } + + pub fn run_invoke_handler(&self, invoke: Invoke) -> bool { + (self.webview.invoke_handler)(invoke) + } + + /// Runs the plugin [`crate::plugin::Plugin::extend_api`] hook if it exists. Returns whether the invoke message was handled or not. + /// + /// The message is not handled when the plugin exists **and** the command does not. + pub fn run_plugin_invoke_handler(&self, plugin: &str, invoke: Invoke) -> bool { + self + .plugins + .lock() + .expect("poisoned plugin store") + .run_invoke_handler(plugin, invoke) + } + + pub fn initialize_plugins(&self, app: &AppHandle) -> crate::Result<()> { + self + .plugins + .lock() + .expect("poisoned plugin store") + .initialize_all(app, &self.config.plugins) + } + + pub fn config(&self) -> &Config { + &self.config + } + + #[cfg(dev)] + pub fn config_parent(&self) -> Option<&std::path::PathBuf> { + self.config_parent.as_ref() + } + + pub fn package_info(&self) -> &PackageInfo { + &self.package_info + } + + /// # Panics + /// Will panic if `event` contains characters other than alphanumeric, `-`, `/`, `:` and `_` + pub(crate) fn listen( + &self, + event: EventName, + target: EventTarget, + handler: F, + ) -> EventId { + self.listeners().listen(event, target, handler) + } + + /// # Panics + /// Will panic if `event` contains characters other than alphanumeric, `-`, `/`, `:` and `_` + pub(crate) fn once( + &self, + event: EventName, + target: EventTarget, + handler: F, + ) -> EventId { + self.listeners().once(event, target, handler) + } + + pub fn unlisten(&self, id: EventId) { + self.listeners().unlisten(id) + } + + #[cfg_attr( + feature = "tracing", + tracing::instrument("app::emit", skip(self, payload)) + )] + pub(crate) fn emit( + &self, + event: EventName<&str>, + payload: EmitPayload<'_, S>, + ) -> crate::Result<()> { + #[cfg(feature = "tracing")] + let _span = tracing::debug_span!("emit::run").entered(); + let emit_args = match payload { + EmitPayload::Serialize(payload) => EmitArgs::new(event, payload)?, + EmitPayload::Str(payload) => EmitArgs::new_str(event, payload)?, + }; + + let listeners = self.listeners(); + + listeners.emit_js(self.webview.webviews_lock().values(), &emit_args)?; + listeners.emit(emit_args)?; + + Ok(()) + } + + #[cfg_attr( + feature = "tracing", + tracing::instrument("app::emit::filter", skip(self, payload, filter)) + )] + pub(crate) fn emit_filter( + &self, + event: EventName<&str>, + payload: EmitPayload<'_, S>, + filter: F, + ) -> crate::Result<()> + where + S: Serialize, + F: Fn(&EventTarget) -> bool, + { + #[cfg(feature = "tracing")] + let _span = tracing::debug_span!("emit::run").entered(); + let emit_args = match payload { + EmitPayload::Serialize(payload) => EmitArgs::new(event, payload)?, + EmitPayload::Str(payload) => EmitArgs::new_str(event, payload)?, + }; + + let listeners = self.listeners(); + + listeners.emit_js_filter( + self.webview.webviews_lock().values(), + &emit_args, + Some(&filter), + )?; + + listeners.emit_filter(emit_args, Some(filter))?; + + Ok(()) + } + + #[cfg_attr( + feature = "tracing", + tracing::instrument("app::emit::to", skip(self, target, payload), fields(target)) + )] + pub(crate) fn emit_to( + &self, + target: EventTarget, + event: EventName<&str>, + payload: EmitPayload<'_, S>, + ) -> crate::Result<()> + where + S: Serialize, + { + #[cfg(feature = "tracing")] + tracing::Span::current().record("target", format!("{target:?}")); + + fn filter_target(target: &EventTarget, candidate: &EventTarget) -> bool { + match target { + // if targeting any label, filter matching labels + EventTarget::AnyLabel { label } => match candidate { + EventTarget::Window { label: l } + | EventTarget::Webview { label: l } + | EventTarget::WebviewWindow { label: l } + | EventTarget::AnyLabel { label: l } => l == label, + _ => false, + }, + EventTarget::Window { label } => match candidate { + EventTarget::AnyLabel { label: l } | EventTarget::Window { label: l } => l == label, + _ => false, + }, + EventTarget::Webview { label } => match candidate { + EventTarget::AnyLabel { label: l } | EventTarget::Webview { label: l } => l == label, + _ => false, + }, + EventTarget::WebviewWindow { label } => match candidate { + EventTarget::AnyLabel { label: l } | EventTarget::WebviewWindow { label: l } => { + l == label + } + _ => false, + }, + // otherwise match same target + _ => target == candidate, + } + } + + match target { + // if targeting all, emit to all using emit without filter + EventTarget::Any => self.emit(event, payload), + target => self.emit_filter(event, payload, |t| filter_target(&target, t)), + } + } + + pub fn get_window(&self, label: &str) -> Option> { + self.window.windows_lock().get(label).cloned() + } + + pub fn get_focused_window(&self) -> Option> { + self + .window + .windows_lock() + .values() + .find(|w| w.is_focused().unwrap_or(false)) + .cloned() + } + + pub(crate) fn on_window_close(&self, label: &str) { + let window = self.window.windows_lock().remove(label); + if let Some(window) = window { + for webview in window.webviews() { + self.webview.webviews_lock().remove(webview.label()); + self.listeners().remove_webview_listeners(webview.label()); + self + .state + .get::() + .remove_webview_entries(webview.label()); + } + } + // window listeners are removed by the window's own event handler after it emits `tauri://destroyed`, + // since the run callback (which calls this function) runs before the window event handlers + } + + #[cfg(desktop)] + pub(crate) fn on_webview_close(&self, label: &str) { + self.webview.webviews_lock().remove(label); + self.listeners().remove_webview_listeners(label); + self + .state + .get::() + .remove_webview_entries(label); + } + + pub fn windows(&self) -> HashMap> { + self.window.windows_lock().clone() + } + + pub fn get_webview(&self, label: &str) -> Option> { + self.webview.webviews_lock().get(label).cloned() + } + + pub fn webviews(&self) -> HashMap> { + self.webview.webviews_lock().clone() + } + + pub(crate) fn resources_table(&self) -> MutexGuard<'_, ResourceTable> { + self + .resources_table + .lock() + .expect("poisoned window manager") + } + + pub(crate) fn invoke_key(&self) -> &str { + &self.invoke_key + } +} + +#[cfg(desktop)] +impl AppManager { + pub fn remove_menu_from_stash_by_id(&self, id: Option<&crate::menu::MenuId>) { + if let Some(id) = id { + let is_used_by_a_window = self + .window + .windows_lock() + .values() + .any(|w| w.is_menu_in_use(id)); + if !(self.menu.is_menu_in_use(id) || is_used_by_a_window) { + self.menu.menus_stash_lock().remove(id); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::replace_with_callback; + + #[test] + fn string_replace_with_callback() { + let mut tauri_index = 0; + #[allow(clippy::single_element_loop)] + for (src, pattern, replacement, result) in [( + "tauri is awesome, tauri is amazing", + "tauri", + || { + tauri_index += 1; + tauri_index.to_string() + }, + "1 is awesome, 2 is amazing", + )] { + assert_eq!(replace_with_callback(src, pattern, replacement), result); + } + } +} + +#[cfg(test)] +mod test { + use std::{ + sync::mpsc::{Receiver, Sender, channel}, + time::Duration, + }; + + use crate::{ + App, Emitter, Listener, Manager, StateManager, Webview, WebviewWindow, WebviewWindowBuilder, + Window, Wry, + event::EventTarget, + generate_context, + plugin::PluginStore, + test::{MockRuntime, mock_app}, + webview::WebviewBuilder, + window::WindowBuilder, + }; + + use super::AppManager; + + const APP_LISTEN_ID: &str = "App::listen"; + const APP_LISTEN_ANY_ID: &str = "App::listen_any"; + const WINDOW_LISTEN_ID: &str = "Window::listen"; + const WINDOW_LISTEN_ANY_ID: &str = "Window::listen_any"; + const WEBVIEW_LISTEN_ID: &str = "Webview::listen"; + const WEBVIEW_LISTEN_ANY_ID: &str = "Webview::listen_any"; + const WEBVIEW_WINDOW_LISTEN_ID: &str = "WebviewWindow::listen"; + const WEBVIEW_WINDOW_LISTEN_ANY_ID: &str = "WebviewWindow::listen_any"; + const TEST_EVENT_NAME: &str = "event"; + + #[test] + fn check_get_url() { + let context = generate_context!("test/fixture/src-tauri/tauri.conf.json", crate, test = true); + let manager: AppManager = AppManager::with_handlers( + context, + PluginStore::default(), + Box::new(|_| false), + None, + #[cfg(any(target_os = "macos", target_os = "ios"))] + None, + Default::default(), + Default::default(), + StateManager::new(), + Default::default(), + #[cfg(all(desktop, feature = "tray-icon"))] + Default::default(), + Default::default(), + Default::default(), + Default::default(), + "".into(), + None, + crate::generate_invoke_key().unwrap(), + ); + + #[cfg(custom_protocol)] + { + assert_eq!( + manager.get_app_url(false).to_string(), + if cfg!(windows) || cfg!(target_os = "android") { + "http://tauri.localhost/" + } else { + "tauri://localhost" + } + ); + assert_eq!( + manager.get_app_url(true).to_string(), + if cfg!(windows) || cfg!(target_os = "android") { + "https://tauri.localhost/" + } else { + "tauri://localhost" + } + ); + } + + #[cfg(dev)] + assert_eq!( + manager.get_app_url(false).to_string(), + "http://localhost:4000/" + ); + } + + struct EventSetup { + app: App, + window: Window, + webview: Webview, + webview_window: WebviewWindow, + tx: Sender<(&'static str, String)>, + rx: Receiver<(&'static str, String)>, + } + + fn setup_events(setup_any: bool) -> EventSetup { + let app = mock_app(); + + let window = WindowBuilder::new(&app, "main-window").build().unwrap(); + + let webview = window + .add_child( + WebviewBuilder::new("main-webview", Default::default()), + crate::LogicalPosition::new(0, 0), + window.inner_size().unwrap(), + ) + .unwrap(); + + let webview_window = WebviewWindowBuilder::new(&app, "main-webview-window", Default::default()) + .build() + .unwrap(); + + let (tx, rx) = channel(); + + macro_rules! setup_listener { + ($type:ident, $id:ident, $any_id:ident) => { + let tx_ = tx.clone(); + $type.listen(TEST_EVENT_NAME, move |evt| { + tx_ + .send(($id, serde_json::from_str::(evt.payload()).unwrap())) + .unwrap(); + }); + + if setup_any { + let tx_ = tx.clone(); + $type.listen_any(TEST_EVENT_NAME, move |evt| { + tx_ + .send(( + $any_id, + serde_json::from_str::(evt.payload()).unwrap(), + )) + .unwrap(); + }); + } + }; + } + + setup_listener!(app, APP_LISTEN_ID, APP_LISTEN_ANY_ID); + setup_listener!(window, WINDOW_LISTEN_ID, WINDOW_LISTEN_ANY_ID); + setup_listener!(webview, WEBVIEW_LISTEN_ID, WEBVIEW_LISTEN_ANY_ID); + setup_listener!( + webview_window, + WEBVIEW_WINDOW_LISTEN_ID, + WEBVIEW_WINDOW_LISTEN_ANY_ID + ); + + EventSetup { + app, + window, + webview, + webview_window, + tx, + rx, + } + } + + fn assert_events(kind: &str, received: &[&str], expected: &[&str]) { + for e in expected { + assert!(received.contains(e), "{e} did not receive `{kind}` event"); + } + assert_eq!( + received.len(), + expected.len(), + "received {received:?} `{kind}` events but expected {expected:?}" + ); + } + + #[test] + fn emit() { + let EventSetup { + app, + window, + webview, + webview_window, + tx: _, + rx, + } = setup_events(true); + + run_emit_test("emit (app)", app, &rx); + run_emit_test("emit (window)", window, &rx); + run_emit_test("emit (webview)", webview, &rx); + run_emit_test("emit (webview_window)", webview_window, &rx); + } + + fn run_emit_test + Emitter>( + kind: &str, + m: M, + rx: &Receiver<(&str, String)>, + ) { + let mut received = Vec::new(); + let payload = "global-payload"; + m.emit(TEST_EVENT_NAME, payload).unwrap(); + while let Ok((source, p)) = rx.recv_timeout(Duration::from_secs(1)) { + assert_eq!(p, payload); + received.push(source); + } + assert_events( + kind, + &received, + &[ + APP_LISTEN_ID, + APP_LISTEN_ANY_ID, + WINDOW_LISTEN_ID, + WINDOW_LISTEN_ANY_ID, + WEBVIEW_LISTEN_ID, + WEBVIEW_LISTEN_ANY_ID, + WEBVIEW_WINDOW_LISTEN_ID, + WEBVIEW_WINDOW_LISTEN_ANY_ID, + ], + ); + } + + #[test] + fn emit_to() { + let EventSetup { + app, + window, + webview, + webview_window, + tx, + rx, + } = setup_events(false); + + run_emit_to_test( + "emit_to (App)", + &app, + &window, + &webview, + &webview_window, + tx.clone(), + &rx, + ); + run_emit_to_test( + "emit_to (window)", + &window, + &window, + &webview, + &webview_window, + tx.clone(), + &rx, + ); + run_emit_to_test( + "emit_to (webview)", + &webview, + &window, + &webview, + &webview_window, + tx.clone(), + &rx, + ); + run_emit_to_test( + "emit_to (webview_window)", + &webview_window, + &window, + &webview, + &webview_window, + tx.clone(), + &rx, + ); + } + + fn run_emit_to_test + Emitter>( + kind: &str, + m: &M, + window: &Window, + webview: &Webview, + webview_window: &WebviewWindow, + tx: Sender<(&'static str, String)>, + rx: &Receiver<(&'static str, String)>, + ) { + let mut received = Vec::new(); + let payload = "global-payload"; + + macro_rules! test_target { + ($target:expr, $id:ident) => { + m.emit_to($target, TEST_EVENT_NAME, payload).unwrap(); + while let Ok((source, p)) = rx.recv_timeout(Duration::from_secs(1)) { + assert_eq!(p, payload); + received.push(source); + } + assert_events(kind, &received, &[$id]); + + received.clear(); + }; + } + + test_target!(EventTarget::App, APP_LISTEN_ID); + test_target!(window.label(), WINDOW_LISTEN_ID); + test_target!(webview.label(), WEBVIEW_LISTEN_ID); + test_target!(webview_window.label(), WEBVIEW_WINDOW_LISTEN_ID); + + let other_webview_listen_id = "OtherWebview::listen"; + let other_webview = WebviewWindowBuilder::new( + window, + kind.replace(['(', ')', ' '], ""), + Default::default(), + ) + .build() + .unwrap(); + + other_webview.listen(TEST_EVENT_NAME, move |evt| { + tx.send(( + other_webview_listen_id, + serde_json::from_str::(evt.payload()).unwrap(), + )) + .unwrap(); + }); + m.emit_to(other_webview.label(), TEST_EVENT_NAME, payload) + .unwrap(); + while let Ok((source, p)) = rx.recv_timeout(Duration::from_secs(1)) { + assert_eq!(p, payload); + received.push(source); + } + assert_events("emit_to", &received, &[other_webview_listen_id]); + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/manager/tray.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/manager/tray.rs new file mode 100644 index 000000000..e233d6948 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/manager/tray.rs @@ -0,0 +1,92 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + collections::HashMap, + fmt, + sync::{Arc, Mutex}, +}; + +use crate::{ + AppHandle, Manager, Resource, ResourceId, Runtime, + app::GlobalTrayIconEventListener, + image::Image, + tray::{TrayIcon, TrayIconEvent, TrayIconId}, +}; + +pub struct TrayManager { + /// icon passed in from [`tauri_utils::config::TrayIconConfig::icon_path`], + /// used when creating tray icons from the configs. + pub(crate) icon: Option>, + /// Tray icons + pub(crate) icons: Mutex>, + /// Global Tray icon event listeners. + pub(crate) global_event_listeners: Mutex>>>, + /// Tray icon event listeners. + pub(crate) event_listeners: Mutex>>>, +} + +impl fmt::Debug for TrayManager { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TrayManager") + .field("icon", &self.icon) + .finish() + } +} + +impl TrayManager { + pub fn on_tray_icon_event, TrayIconEvent) + Send + Sync + 'static>( + &self, + handler: F, + ) { + self + .global_event_listeners + .lock() + .unwrap() + .push(Box::new(handler)); + } + + pub fn tray_by_id<'a, I>(&self, app: &AppHandle, id: &'a I) -> Option> + where + I: ?Sized, + TrayIconId: PartialEq<&'a I>, + { + let icons = self.icons.lock().unwrap(); + icons.iter().find_map(|(tray_icon_id, rid)| { + if tray_icon_id == &id { + let icon = app.resources_table().get::>(*rid).ok()?; + Some(Arc::unwrap_or_clone(icon)) + } else { + None + } + }) + } + + pub fn tray_resource_by_id<'a, I>(&self, id: &'a I) -> Option + where + I: ?Sized, + TrayIconId: PartialEq<&'a I>, + { + let icons = self.icons.lock().unwrap(); + icons.iter().find_map(|(tray_icon_id, rid)| { + if tray_icon_id == &id { + Some(*rid) + } else { + None + } + }) + } + + pub fn remove_tray_by_id<'a, I>(&self, app: &AppHandle, id: &'a I) -> Option> + where + I: ?Sized, + TrayIconId: PartialEq<&'a I>, + { + let rid = self.tray_resource_by_id(id)?; + let icon = app.resources_table().take::>(rid).ok()?; + let icon_to_return = icon.clone(); + icon.close(); + Some(Arc::unwrap_or_clone(icon_to_return)) + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/manager/webview.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/manager/webview.rs new file mode 100644 index 000000000..142297693 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/manager/webview.rs @@ -0,0 +1,792 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + borrow::Cow, + collections::{HashMap, HashSet}, + fmt, + fs::create_dir_all, + sync::{Arc, Mutex, MutexGuard}, +}; + +use serde::Serialize; +use serialize_to_javascript::{DefaultTemplate, Template, default_template}; +use tauri_runtime::{ + webview::{DetachedWebview, InitializationScript, PendingWebview}, + window::DragDropEvent, +}; +use tauri_utils::config::WebviewUrl; +use url::Url; + +use crate::{ + EventLoopMessage, EventTarget, Manager, Runtime, Scopes, UriSchemeContext, Webview, Window, + app::{GlobalWebviewEventListener, OnPageLoad, UriSchemeResponder, WebviewEvent}, + ipc::InvokeHandler, + pattern::PatternJavascript, + sealed::ManagerBase, + webview::PageLoadPayload, +}; + +#[cfg(any(target_os = "macos", target_os = "ios"))] +use crate::app::OnWebContentProcessTerminate; + +use super::{ + window::{DRAG_DROP_EVENT, DRAG_ENTER_EVENT, DRAG_LEAVE_EVENT, DRAG_OVER_EVENT, DragDropPayload}, + {AppManager, EmitPayload}, +}; + +// we need to proxy the dev server on mobile because we can't use `localhost`, so we use the local IP address +// and we do not get a secure context without the custom protocol that proxies to the dev server +// additionally, we need the custom protocol to inject the initialization scripts on Android +// must also keep in sync with the `let mut response` assignment in prepare_uri_scheme_protocol +pub(crate) const PROXY_DEV_SERVER: bool = cfg!(all(dev, mobile)); + +pub(crate) const PROCESS_IPC_MESSAGE_FN: &str = + include_str!("../../scripts/process-ipc-message-fn.js"); + +#[cfg(feature = "isolation")] +#[derive(Template)] +#[default_template("../../scripts/isolation.js")] +pub(crate) struct IsolationJavascript<'a> { + pub(crate) isolation_src: &'a str, + pub(crate) style: &'a str, +} + +#[derive(Template)] +#[default_template("../../scripts/ipc.js")] +pub(crate) struct IpcJavascript<'a> { + pub(crate) isolation_origin: &'a str, +} + +/// Uses a custom URI scheme handler to resolve file requests +pub struct UriSchemeProtocol { + /// Handler for protocol + #[allow(clippy::type_complexity)] + pub handler: + Box, http::Request>, UriSchemeResponder) + Send + Sync>, +} + +pub struct WebviewManager { + pub webviews: Mutex>>, + /// The JS message handler. + pub invoke_handler: Box>, + /// The page load hook, invoked when the webview performs a navigation. + pub on_page_load: Option>>, + /// The permission request hook, invoked when the webview requests a permission. + pub on_permission_request: Option>>, + /// The web content process termination hook. + #[cfg(any(target_os = "macos", target_os = "ios"))] + pub on_web_content_process_terminate: Option>>, + /// The webview protocols available to all webviews. + pub uri_scheme_protocols: Mutex>>>, + /// Webview event listeners to all webviews. + pub event_listeners: Arc>>, + + /// The script that initializes the invoke system. + pub invoke_initialization_script: String, + + /// A runtime generated invoke key. + pub(crate) invoke_key: String, +} + +impl fmt::Debug for WebviewManager { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("WebviewManager") + .field( + "invoke_initialization_script", + &self.invoke_initialization_script, + ) + .field("invoke_key", &self.invoke_key) + .finish() + } +} + +impl WebviewManager { + pub(crate) fn register_uri_scheme_protocol>( + &self, + uri_scheme: N, + protocol: Arc>, + ) { + let uri_scheme = uri_scheme.into(); + self + .uri_scheme_protocols + .lock() + .unwrap() + .insert(uri_scheme, protocol); + } + + /// Get a locked handle to the webviews. + pub(crate) fn webviews_lock(&self) -> MutexGuard<'_, HashMap>> { + self.webviews.lock().expect("poisoned webview manager") + } + + fn prepare_pending_webview>( + &self, + mut pending: PendingWebview, + label: &str, + window_label: &str, + manager: &M, + ) -> crate::Result> { + let app_manager = manager.manager(); + + let plugin_init_scripts = app_manager + .plugins + .lock() + .expect("poisoned plugin store") + .initialization_script(); + + let pattern_init = PatternJavascript { + pattern: (&*app_manager.pattern).into(), + } + .render_default(&Default::default())?; + + let mut webview_attributes = pending.webview_attributes; + + let use_https_scheme = webview_attributes.use_https_scheme; + + let ipc_init = IpcJavascript { + isolation_origin: &match &*app_manager.pattern { + #[cfg(feature = "isolation")] + crate::Pattern::Isolation { schema, .. } => { + crate::pattern::format_real_schema(schema, use_https_scheme) + } + _ => "".to_owned(), + }, + } + .render_default(&Default::default())?; + + let mut all_initialization_scripts: Vec = vec![]; + + fn main_frame_script(script: String) -> InitializationScript { + InitializationScript { + script, + for_main_frame_only: true, + } + } + + all_initialization_scripts.push(main_frame_script( + r" + Object.defineProperty(window, 'isTauri', { + value: true, + }); + + if (!window.__TAURI_INTERNALS__) { + Object.defineProperty(window, '__TAURI_INTERNALS__', { + value: { + plugins: {} + } + }) + } + " + .to_owned(), + )); + all_initialization_scripts.push(main_frame_script(self.invoke_initialization_script.clone())); + all_initialization_scripts.push(main_frame_script(format!( + r#" + Object.defineProperty(window.__TAURI_INTERNALS__, 'metadata', {{ + value: {{ + currentWindow: {{ label: {current_window_label} }}, + currentWebview: {{ label: {current_webview_label} }} + }} + }}) + "#, + current_window_label = serde_json::to_string(window_label)?, + current_webview_label = serde_json::to_string(&label)?, + ))); + all_initialization_scripts.push(main_frame_script(self.initialization_script( + app_manager, + &ipc_init.into_string(), + &pattern_init.into_string(), + use_https_scheme, + )?)); + + all_initialization_scripts.extend(plugin_init_scripts); + + #[cfg(feature = "isolation")] + if let crate::Pattern::Isolation { schema, .. } = &*app_manager.pattern { + all_initialization_scripts.push(main_frame_script( + IsolationJavascript { + isolation_src: &crate::pattern::format_real_schema(schema, use_https_scheme), + style: tauri_utils::pattern::isolation::IFRAME_STYLE, + } + .render_default(&Default::default())? + .into_string(), + )); + } + + if let Some(plugin_global_api_scripts) = &*app_manager.plugin_global_api_scripts { + for &script in plugin_global_api_scripts.iter() { + all_initialization_scripts.push(main_frame_script(script.to_owned())); + } + } + + // Prepend `all_initialization_scripts` to `webview_attributes.initialization_scripts` + all_initialization_scripts.extend(webview_attributes.initialization_scripts); + webview_attributes.initialization_scripts = all_initialization_scripts; + + pending.webview_attributes = webview_attributes; + + let mut registered_scheme_protocols = Vec::new(); + + for (uri_scheme, protocol) in &*self.uri_scheme_protocols.lock().unwrap() { + registered_scheme_protocols.push(uri_scheme.clone()); + let protocol = protocol.clone(); + let app_handle = manager.app_handle().clone(); + + pending.register_uri_scheme_protocol(uri_scheme, move |webview_id, request, responder| { + let context = UriSchemeContext { + app_handle: &app_handle, + webview_label: webview_id, + }; + (protocol.handler)(context, request, UriSchemeResponder(responder)) + }); + } + + let window_url = Url::parse(&pending.url).unwrap(); + let window_origin = if window_url.scheme() == "data" { + "null".into() + } else if (cfg!(windows) || cfg!(target_os = "android")) + && window_url.scheme() != "http" + && window_url.scheme() != "https" + { + crate::protocol::origin(window_url.scheme(), use_https_scheme) + } else if let Some(host) = window_url.host() { + format!( + "{}://{}{}", + window_url.scheme(), + host, + window_url + .port() + .map(|p| format!(":{p}")) + .unwrap_or_default() + ) + } else { + "null".into() + }; + + if !registered_scheme_protocols.contains(&"tauri".into()) { + let web_resource_request_handler = pending.web_resource_request_handler.take(); + let protocol = crate::protocol::tauri::get( + manager.manager_owned(), + window_origin.clone(), + web_resource_request_handler, + ); + pending.register_uri_scheme_protocol("tauri", move |webview_id, request, responder| { + protocol(webview_id, request, UriSchemeResponder(responder)) + }); + registered_scheme_protocols.push("tauri".into()); + } + + if !registered_scheme_protocols.contains(&"ipc".into()) { + let protocol = crate::ipc::protocol::get(manager.manager_owned()); + pending.register_uri_scheme_protocol("ipc", move |webview_id, request, responder| { + protocol(webview_id, request, UriSchemeResponder(responder)) + }); + registered_scheme_protocols.push("ipc".into()); + } + + let label = pending.label.clone(); + let app_manager_ = manager.manager_owned(); + let on_page_load_handler = pending.on_page_load_handler.take(); + pending + .on_page_load_handler + .replace(Box::new(move |url, event| { + let payload = PageLoadPayload { url: &url, event }; + + if let Some(w) = app_manager_.get_webview(&label) { + if let Some(on_page_load) = &app_manager_.webview.on_page_load { + on_page_load(&w, &payload); + } + + app_manager_ + .plugins + .lock() + .unwrap() + .on_page_load(&w, &payload); + } + + if let Some(handler) = &on_page_load_handler { + handler(url, event); + } + })); + + let permission_request_handler = pending.permission_request_handler.take(); + if permission_request_handler.is_some() || self.on_permission_request.is_some() { + let label_ = pending.label.clone(); + let app_manager_ = manager.manager_owned(); + pending + .permission_request_handler + .replace(Box::new(move |kind| { + if let Some(handler) = &permission_request_handler { + let response = handler(kind); + if response != crate::webview::PermissionResponse::Default { + return response; + } + } + + if let Some(w) = app_manager_.get_webview(&label_) { + if let Some(on_permission_request) = &app_manager_.webview.on_permission_request { + return on_permission_request(w, kind); + } + } + + crate::webview::PermissionResponse::Default + })); + } + + #[cfg(any(target_os = "macos", target_os = "ios"))] + if pending.on_web_content_process_terminate_handler.is_none() { + let app_manager_ = manager.manager_owned(); + if app_manager_ + .webview + .on_web_content_process_terminate + .is_some() + { + let label_ = pending.label.clone(); + pending + .on_web_content_process_terminate_handler + .replace(Box::new(move || { + if let Some(w) = app_manager_.get_webview(&label_) { + if let Some(on_web_content_process_terminate) = + &app_manager_.webview.on_web_content_process_terminate + { + on_web_content_process_terminate(&w); + } + } + })); + } + } + + #[cfg(feature = "protocol-asset")] + if !registered_scheme_protocols.contains(&"asset".into()) { + let asset_scope = app_manager + .state + .get::() + .asset_protocol + .clone(); + let protocol = crate::protocol::asset::get(asset_scope, window_origin.clone()); + pending.register_uri_scheme_protocol("asset", move |webview_id, request, responder| { + protocol(webview_id, request, UriSchemeResponder(responder)) + }); + } + + #[cfg(feature = "isolation")] + if let crate::Pattern::Isolation { + assets, + schema, + key: _, + crypto_keys, + } = &*app_manager.pattern + { + let protocol = crate::protocol::isolation::get( + manager.manager_owned(), + schema, + assets.clone(), + *crypto_keys.aes_gcm().raw(), + window_origin, + use_https_scheme, + ); + pending.register_uri_scheme_protocol(schema, move |webview_id, request, responder| { + protocol(webview_id, request, UriSchemeResponder(responder)) + }); + } + + Ok(pending) + } + + fn initialization_script( + &self, + app_manager: &AppManager, + ipc_script: &str, + pattern_script: &str, + use_https_scheme: bool, + ) -> crate::Result { + #[derive(Template)] + #[default_template("../../scripts/init.js")] + struct InitJavascript<'a> { + #[raw] + pattern_script: &'a str, + #[raw] + ipc_script: &'a str, + #[raw] + core_script: &'a str, + #[raw] + event_initialization_script: &'a str, + #[raw] + freeze_prototype: &'a str, + } + + #[derive(Template)] + #[default_template("../../scripts/core.js")] + struct CoreJavascript<'a> { + os_name: &'a str, + protocol_scheme: &'a str, + invoke_key: &'a str, + } + + let freeze_prototype = if app_manager.config.app.security.freeze_prototype { + include_str!("../../scripts/freeze_prototype.js") + } else { + "" + }; + + InitJavascript { + pattern_script, + ipc_script, + core_script: &CoreJavascript { + os_name: std::env::consts::OS, + protocol_scheme: if use_https_scheme { "https" } else { "http" }, + invoke_key: self.invoke_key(), + } + .render_default(&Default::default())? + .into_string(), + event_initialization_script: &crate::event::event_initialization_script( + app_manager.listeners().function_name(), + app_manager.listeners().listeners_object_name(), + ), + freeze_prototype, + } + .render_default(&Default::default()) + .map(|s| s.into_string()) + .map_err(Into::into) + } + + pub fn prepare_webview>( + &self, + manager: &M, + mut pending: PendingWebview, + window_label: &str, + ) -> crate::Result> { + if self.webviews_lock().contains_key(&pending.label) { + return Err(crate::Error::WebviewLabelAlreadyExists(pending.label)); + } + + let app_manager = manager.manager(); + + #[allow(unused_mut)] // mut url only for the data-url parsing + let mut url = match &pending.webview_attributes.url { + WebviewUrl::App(path) => { + let app_url = app_manager.get_app_url(pending.webview_attributes.use_https_scheme); + let url = if PROXY_DEV_SERVER && is_local_network_url(&app_url) { + Cow::Owned(Url::parse("tauri://localhost").unwrap()) + } else { + app_url + }; + // ignore "index.html" just to simplify the url + if path.to_str() != Some("index.html") { + url + .join(&path.to_string_lossy()) + .map_err(crate::Error::InvalidUrl) + // this will never fail + .unwrap() + } else { + url.into_owned() + } + } + WebviewUrl::External(url) => { + let config_url = app_manager.get_app_url(pending.webview_attributes.use_https_scheme); + let is_app_url = config_url.make_relative(url).is_some(); + let mut url = url.clone(); + if is_app_url && PROXY_DEV_SERVER && is_local_network_url(&url) { + Url::parse("tauri://localhost").unwrap() + } else { + url + } + } + + WebviewUrl::CustomProtocol(url) => url.clone(), + _ => unimplemented!(), + }; + + #[cfg(not(feature = "webview-data-url"))] + if url.scheme() == "data" { + return Err(crate::Error::InvalidWebviewUrl( + "data URLs are not supported without the `webview-data-url` feature.", + )); + } + + #[cfg(feature = "webview-data-url")] + if let Some(csp) = app_manager.csp() { + if url.scheme() == "data" { + if let Ok(data_url) = data_url::DataUrl::process(url.as_str()) { + let (body, _) = data_url.decode_to_vec().unwrap(); + let html = String::from_utf8_lossy(&body).into_owned(); + // naive way to check if it's an html + if html.contains('<') && html.contains('>') { + let document = tauri_utils::html2::parse_doc(html); + tauri_utils::html2::inject_csp(&document, &csp.to_string()); + url.set_path(&format!("{},{}", mime::TEXT_HTML, document.html())); + } + } + } + } + + pending.url = url.to_string(); + + #[cfg(target_os = "android")] + { + pending = pending.on_webview_created(move |ctx| { + let plugin_manager = ctx + .env + .call_method( + ctx.activity, + "getPluginManager", + "()Lapp/tauri/plugin/PluginManager;", + &[], + )? + .l()?; + + // tell the manager the webview is ready + ctx.env.call_method( + plugin_manager, + "onWebViewCreated", + "(Landroid/webkit/WebView;)V", + &[ctx.webview.into()], + )?; + + Ok(()) + }); + } + + let label = pending.label.clone(); + pending = self.prepare_pending_webview(pending, &label, window_label, manager)?; + + pending.ipc_handler = Some(crate::ipc::protocol::message_handler( + manager.manager_owned(), + )); + + // in `windows`, we need to force a data_directory + // but we do respect user-specification + #[cfg(any(target_os = "linux", target_os = "windows"))] + if pending.webview_attributes.data_directory.is_none() { + match manager.path().app_local_data_dir() { + Ok(user_data_dir) => pending.webview_attributes.data_directory = Some(user_data_dir), + Err(e) => log::error!( + "failed to resolve the app local data directory, the webview falls back to its default data directory: {e}" + ), + } + } + + // make sure the directory is created and available to prevent a panic + if let Some(user_data_dir) = &pending.webview_attributes.data_directory { + if !user_data_dir.exists() { + create_dir_all(user_data_dir)?; + } + } + + #[cfg(all(desktop, not(target_os = "windows")))] + if pending.webview_attributes.zoom_hotkeys_enabled { + #[derive(Template)] + #[default_template("../webview/scripts/zoom-hotkey.js")] + struct HotkeyZoom<'a> { + os_name: &'a str, + } + + pending + .webview_attributes + .initialization_scripts + .push(InitializationScript { + script: HotkeyZoom { + os_name: std::env::consts::OS, + } + .render_default(&Default::default())? + .into_string(), + for_main_frame_only: true, + }) + } + + #[cfg(feature = "isolation")] + let pattern = app_manager.pattern.clone(); + let navigation_handler = pending.navigation_handler.take(); + let app_manager = manager.manager_owned(); + let label = pending.label.clone(); + pending.navigation_handler = Some(Box::new(move |url| { + // always allow navigation events for the isolation iframe and do not emit them for consumers + #[cfg(feature = "isolation")] + if let crate::Pattern::Isolation { schema, .. } = &*pattern { + if url.scheme() == schema + && url.domain() == Some(crate::pattern::ISOLATION_IFRAME_SRC_DOMAIN) + { + return true; + } + } + if let Some(handler) = &navigation_handler { + if !handler(url) { + return false; + } + } + let webview = app_manager.webview.webviews_lock().get(&label).cloned(); + if let Some(w) = webview { + app_manager + .plugins + .lock() + .expect("poisoned plugin store") + .on_navigation(&w, url) + } else { + true + } + })); + + Ok(pending) + } + + pub(crate) fn attach_webview( + &self, + window: Window, + webview: DetachedWebview, + use_https_scheme: bool, + ) -> Webview { + let webview = Webview::new(window, webview, use_https_scheme); + + let webview_event_listeners = self.event_listeners.clone(); + let webview_ = webview.clone(); + webview.on_webview_event(move |event| { + let _ = on_webview_event(&webview_, event); + for handler in webview_event_listeners.iter() { + handler(&webview_, event); + } + }); + + // insert the webview into our manager + { + self + .webviews_lock() + .insert(webview.label().to_string(), webview.clone()); + } + + // let plugins know that a new webview has been added to the manager + let manager = webview.manager_owned(); + let webview_ = webview.clone(); + // run on main thread so the plugin store doesn't dead lock with the event loop handler in App + let _ = webview.run_on_main_thread(move || { + manager + .plugins + .lock() + .expect("poisoned plugin store") + .webview_created(webview_); + }); + + #[cfg(all(target_os = "ios", feature = "wry"))] + { + webview + .with_webview(|w| { + unsafe { crate::ios::on_webview_created(w.inner() as _, w.view_controller() as _) }; + }) + .expect("failed to run on_webview_created hook"); + } + + let event = crate::EventName::from_str("tauri://webview-created"); + let payload = Some(crate::webview::CreatedEvent { + label: webview.label().into(), + }); + + let _ = webview + .manager + .emit(event, EmitPayload::Serialize(&payload)); + + webview + } + + pub fn eval_script_all>(&self, script: S) -> crate::Result<()> { + let script = script.into(); + let webviews = self.webviews_lock().values().cloned().collect::>(); + webviews + .iter() + .try_for_each(|webview| webview.eval(&script)) + } + + pub fn labels(&self) -> HashSet { + self.webviews_lock().keys().cloned().collect() + } + + pub(crate) fn invoke_key(&self) -> &str { + &self.invoke_key + } +} + +impl Webview { + /// Emits event to [`EventTarget::Window`] and [`EventTarget::WebviewWindow`] + fn emit_to_webview( + &self, + event: crate::EventName<&str>, + payload: &S, + ) -> crate::Result<()> { + let window_label = self.label(); + let payload = EmitPayload::Serialize(payload); + self + .manager() + .emit_filter(event, payload, |target| match target { + EventTarget::Webview { label } | EventTarget::WebviewWindow { label } => { + label == window_label + } + _ => false, + }) + } +} + +fn on_webview_event(webview: &Webview, event: &WebviewEvent) -> crate::Result<()> { + match event { + WebviewEvent::DragDrop(event) => match event { + DragDropEvent::Enter { paths, position } => { + let payload = DragDropPayload { + paths: Some(paths), + position, + }; + webview.emit_to_webview(DRAG_ENTER_EVENT, &payload)? + } + DragDropEvent::Over { position } => { + let payload = DragDropPayload { + position, + paths: None, + }; + webview.emit_to_webview(DRAG_OVER_EVENT, &payload)? + } + DragDropEvent::Drop { paths, position } => { + let scopes = webview.state::(); + for path in paths { + if path.is_file() { + let _ = scopes.allow_file(path); + } else { + let _ = scopes.allow_directory(path, false); + } + } + let payload = DragDropPayload { + paths: Some(paths), + position, + }; + webview.emit_to_webview(DRAG_DROP_EVENT, &payload)? + } + DragDropEvent::Leave => webview.emit_to_webview(DRAG_LEAVE_EVENT, &())?, + _ => unimplemented!(), + }, + } + + Ok(()) +} + +fn is_local_network_url(url: &url::Url) -> bool { + match url.host() { + Some(url::Host::Domain(s)) => s == "localhost", + Some(url::Host::Ipv4(_)) | Some(url::Host::Ipv6(_)) => true, + None => false, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn local_network_url() { + assert!(is_local_network_url(&"http://localhost".parse().unwrap())); + assert!(is_local_network_url( + &"http://127.0.0.1:8080".parse().unwrap() + )); + assert!(is_local_network_url( + &"https://192.168.3.17".parse().unwrap() + )); + + assert!(!is_local_network_url(&"https://tauri.app".parse().unwrap())); + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/manager/window.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/manager/window.rs new file mode 100644 index 000000000..93f7db6c6 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/manager/window.rs @@ -0,0 +1,292 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + collections::{HashMap, HashSet}, + fmt, + path::PathBuf, + sync::{Arc, Mutex, MutexGuard}, +}; + +use serde::Serialize; +use tauri_runtime::{ + dpi::{PhysicalPosition, PhysicalSize}, + window::WindowBuilder, + window::{DetachedWindow, DragDropEvent, PendingWindow}, +}; + +use crate::{ + AppHandle, EventLoopMessage, EventTarget, Manager, Runtime, Scopes, Window, WindowEvent, + app::GlobalWindowEventListener, event::EventName, image::Image, sealed::ManagerBase, +}; + +use super::EmitPayload; + +const WINDOW_RESIZED_EVENT: EventName<&str> = EventName::from_str("tauri://resize"); +const WINDOW_MOVED_EVENT: EventName<&str> = EventName::from_str("tauri://move"); +const WINDOW_CLOSE_REQUESTED_EVENT: EventName<&str> = + EventName::from_str("tauri://close-requested"); +const WINDOW_DESTROYED_EVENT: EventName<&str> = EventName::from_str("tauri://destroyed"); +const WINDOW_FOCUS_EVENT: EventName<&str> = EventName::from_str("tauri://focus"); +const WINDOW_BLUR_EVENT: EventName<&str> = EventName::from_str("tauri://blur"); +const WINDOW_SCALE_FACTOR_CHANGED_EVENT: EventName<&str> = + EventName::from_str("tauri://scale-change"); +const WINDOW_THEME_CHANGED: EventName<&str> = EventName::from_str("tauri://theme-changed"); +pub(crate) const DRAG_ENTER_EVENT: EventName<&str> = EventName::from_str("tauri://drag-enter"); +pub(crate) const DRAG_OVER_EVENT: EventName<&str> = EventName::from_str("tauri://drag-over"); +pub(crate) const DRAG_DROP_EVENT: EventName<&str> = EventName::from_str("tauri://drag-drop"); +pub(crate) const DRAG_LEAVE_EVENT: EventName<&str> = EventName::from_str("tauri://drag-leave"); +#[cfg(mobile)] +pub(crate) const WINDOW_SUSPENDED_EVENT: EventName<&str> = EventName::from_str("tauri://suspended"); +#[cfg(mobile)] +pub(crate) const WINDOW_RESUMED_EVENT: EventName<&str> = EventName::from_str("tauri://resumed"); + +pub struct WindowManager { + pub windows: Mutex>>, + pub default_icon: Option>, + /// Window event listeners to all windows. + pub event_listeners: Arc>>, +} + +impl fmt::Debug for WindowManager { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("WindowManager") + .field("default_window_icon", &self.default_icon) + .finish() + } +} + +impl WindowManager { + /// Get a locked handle to the windows. + pub(crate) fn windows_lock(&self) -> MutexGuard<'_, HashMap>> { + self.windows.lock().expect("poisoned window manager") + } + + pub fn prepare_window( + &self, + mut pending: PendingWindow, + ) -> crate::Result> { + if self.windows_lock().contains_key(&pending.label) { + return Err(crate::Error::WindowLabelAlreadyExists(pending.label)); + } + + if !pending.window_builder.has_icon() { + if let Some(default_window_icon) = self.default_icon.clone() { + pending.window_builder = pending.window_builder.icon(default_window_icon.into())?; + } + } + + Ok(pending) + } + + pub(crate) fn attach_window( + &self, + app_handle: AppHandle, + window: DetachedWindow, + #[cfg(desktop)] menu: Option>, + ) -> Window { + let window = Window::new( + app_handle.manager.clone(), + window, + app_handle, + #[cfg(desktop)] + menu, + ); + + let window_ = window.clone(); + let window_event_listeners = self.event_listeners.clone(); + window.on_window_event(move |event| { + let _ = on_window_event(&window_, event); + for handler in window_event_listeners.iter() { + handler(&window_, event); + } + // purge the window's Rust listeners only after `tauri://destroyed` was delivered to them + if matches!(event, WindowEvent::Destroyed) { + window_ + .manager() + .listeners() + .remove_window_listeners(window_.label()); + } + }); + + // insert the window into our manager + { + self + .windows_lock() + .insert(window.label().to_string(), window.clone()); + } + + // let plugins know that a new window has been added to the manager + let manager = window.manager.clone(); + let window_ = window.clone(); + // run on main thread so the plugin store doesn't dead lock with the event loop handler in App + let _ = window.run_on_main_thread(move || { + manager + .plugins + .lock() + .expect("poisoned plugin store") + .window_created(window_); + }); + + window + } + + pub fn labels(&self) -> HashSet { + self.windows_lock().keys().cloned().collect() + } +} + +impl Window { + /// Emits event to [`EventTarget::Window`] and [`EventTarget::WebviewWindow`] + fn emit_to_window(&self, event: EventName<&str>, payload: &S) -> crate::Result<()> { + let window_label = self.label(); + let payload = EmitPayload::Serialize(payload); + self + .manager() + .emit_filter(event, payload, |target| match target { + EventTarget::Window { label } | EventTarget::WebviewWindow { label } => { + label == window_label + } + _ => false, + }) + } + + /// Checks whether has js listener for [`EventTarget::Window`] or [`EventTarget::WebviewWindow`] + fn has_js_listener(&self, event: EventName<&str>) -> bool { + let window_label = self.label(); + let listeners = self.manager().listeners(); + listeners.has_js_listener(event, |target| match target { + EventTarget::Window { label } | EventTarget::WebviewWindow { label } => label == window_label, + _ => false, + }) + } +} + +#[derive(Serialize, Clone)] +pub(crate) struct DragDropPayload<'a> { + #[serde(skip_serializing_if = "Option::is_none")] + pub paths: Option<&'a Vec>, + pub position: &'a PhysicalPosition, +} + +fn on_window_event(window: &Window, event: &WindowEvent) -> crate::Result<()> { + match event { + WindowEvent::Resized(size) => window.emit_to_window(WINDOW_RESIZED_EVENT, size)?, + WindowEvent::Moved(position) => window.emit_to_window(WINDOW_MOVED_EVENT, position)?, + WindowEvent::CloseRequested { api } => { + if window.has_js_listener(WINDOW_CLOSE_REQUESTED_EVENT) { + api.prevent_close(); + } + window.emit_to_window(WINDOW_CLOSE_REQUESTED_EVENT, &())?; + } + WindowEvent::Destroyed => { + window.emit_to_window(WINDOW_DESTROYED_EVENT, &())?; + } + WindowEvent::Focused(focused) => window.emit_to_window( + if *focused { + WINDOW_FOCUS_EVENT + } else { + WINDOW_BLUR_EVENT + }, + &(), + )?, + WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size, + .. + } => window.emit_to_window( + WINDOW_SCALE_FACTOR_CHANGED_EVENT, + &ScaleFactorChanged { + scale_factor: *scale_factor, + size: *new_inner_size, + }, + )?, + WindowEvent::DragDrop(event) => match event { + DragDropEvent::Enter { paths, position } => { + let payload = DragDropPayload { + paths: Some(paths), + position, + }; + + if window.is_webview_window() { + // use underlying manager, otherwise have to recheck EventName + window.manager().emit_to( + EventTarget::labeled(window.label()), + DRAG_ENTER_EVENT, + EmitPayload::Serialize(&payload), + )? + } else { + window.emit_to_window(DRAG_ENTER_EVENT, &payload)? + } + } + DragDropEvent::Over { position } => { + let payload = DragDropPayload { + position, + paths: None, + }; + if window.is_webview_window() { + // use underlying manager, otherwise have to recheck EventName + window.manager().emit_to( + EventTarget::labeled(window.label()), + DRAG_OVER_EVENT, + EmitPayload::Serialize(&payload), + )? + } else { + window.emit_to_window(DRAG_OVER_EVENT, &payload)? + } + } + DragDropEvent::Drop { paths, position } => { + let scopes = window.state::(); + for path in paths { + if path.is_file() { + let _ = scopes.allow_file(path); + } else { + let _ = scopes.allow_directory(path, true); + } + } + let payload = DragDropPayload { + paths: Some(paths), + position, + }; + + if window.is_webview_window() { + // use underlying manager, otherwise have to recheck EventName + window.manager().emit_to( + EventTarget::labeled(window.label()), + DRAG_DROP_EVENT, + EmitPayload::Serialize(&payload), + )? + } else { + window.emit_to_window(DRAG_DROP_EVENT, &payload)? + } + } + DragDropEvent::Leave => { + if window.is_webview_window() { + // use underlying manager, otherwise have to recheck EventName + window.manager().emit_to( + EventTarget::labeled(window.label()), + DRAG_LEAVE_EVENT, + EmitPayload::Serialize(&()), + )? + } else { + window.emit_to_window(DRAG_LEAVE_EVENT, &())? + } + } + _ => unimplemented!(), + }, + WindowEvent::ThemeChanged(theme) => window.emit_to_window(WINDOW_THEME_CHANGED, &theme)?, + #[cfg(mobile)] + WindowEvent::Suspended => window.emit_to_window(WINDOW_SUSPENDED_EVENT, &())?, + #[cfg(mobile)] + WindowEvent::Resumed => window.emit_to_window(WINDOW_RESUMED_EVENT, &())?, + } + Ok(()) +} + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +struct ScaleFactorChanged { + scale_factor: f64, + size: PhysicalSize, +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/builders/check.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/builders/check.rs new file mode 100644 index 000000000..15cf837b1 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/builders/check.rs @@ -0,0 +1,90 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{Manager, Runtime, menu::CheckMenuItem, menu::MenuId}; + +/// A builder type for [`CheckMenuItem`] +pub struct CheckMenuItemBuilder { + id: Option, + text: String, + enabled: bool, + checked: bool, + accelerator: Option, +} + +impl CheckMenuItemBuilder { + /// Create a new menu item builder. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn new>(text: S) -> Self { + Self { + id: None, + text: text.as_ref().to_string(), + enabled: true, + checked: true, + accelerator: None, + } + } + + /// Create a new menu item builder with the specified id. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn with_id, S: AsRef>(id: I, text: S) -> Self { + Self { + id: Some(id.into()), + text: text.as_ref().to_string(), + enabled: true, + checked: true, + accelerator: None, + } + } + + /// Set the id for this menu item. + pub fn id>(mut self, id: I) -> Self { + self.id.replace(id.into()); + self + } + + /// Set the enabled state for this menu item. + pub fn enabled(mut self, enabled: bool) -> Self { + self.enabled = enabled; + self + } + + /// Set the checked state for this menu item. + pub fn checked(mut self, checked: bool) -> Self { + self.checked = checked; + self + } + + /// Set the accelerator for this menu item. + pub fn accelerator>(mut self, accelerator: S) -> Self { + self.accelerator.replace(accelerator.as_ref().to_string()); + self + } + + /// Build the menu item + pub fn build>(self, manager: &M) -> crate::Result> { + if let Some(id) = self.id { + CheckMenuItem::with_id( + manager, + id, + self.text, + self.enabled, + self.checked, + self.accelerator, + ) + } else { + CheckMenuItem::new( + manager, + self.text, + self.enabled, + self.checked, + self.accelerator, + ) + } + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/builders/icon.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/builders/icon.rs new file mode 100644 index 000000000..fd377095c --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/builders/icon.rs @@ -0,0 +1,130 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{ + Manager, Runtime, + image::Image, + menu::{IconMenuItem, MenuId, NativeIcon}, +}; + +/// A builder type for [`IconMenuItem`] +pub struct IconMenuItemBuilder<'a> { + id: Option, + text: String, + enabled: bool, + icon: Option>, + native_icon: Option, + accelerator: Option, +} + +impl<'a> IconMenuItemBuilder<'a> { + /// Create a new menu item builder. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn new>(text: S) -> Self { + Self { + id: None, + text: text.as_ref().to_string(), + enabled: true, + icon: None, + native_icon: None, + accelerator: None, + } + } + + /// Create a new menu item builder with the specified id. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn with_id, S: AsRef>(id: I, text: S) -> Self { + Self { + id: Some(id.into()), + text: text.as_ref().to_string(), + enabled: true, + icon: None, + native_icon: None, + accelerator: None, + } + } + + /// Set the id for this menu item. + pub fn id>(mut self, id: I) -> Self { + self.id.replace(id.into()); + self + } + + /// Set the enabled state for this menu item. + pub fn enabled(mut self, enabled: bool) -> Self { + self.enabled = enabled; + self + } + + /// Set the accelerator for this menu item. + pub fn accelerator>(mut self, accelerator: S) -> Self { + self.accelerator.replace(accelerator.as_ref().to_string()); + self + } + + /// Set the icon for this menu item. + /// + /// **Note:** This method conflicts with [`Self::native_icon`] + /// so calling one of them, will reset the other. + pub fn icon(mut self, icon: Image<'a>) -> Self { + self.icon.replace(icon); + self.native_icon = None; + self + } + + /// Set the icon for this menu item. + /// + /// **Note:** This method conflicts with [`Self::icon`] + /// so calling one of them, will reset the other. + pub fn native_icon(mut self, icon: NativeIcon) -> Self { + self.native_icon.replace(icon); + self.icon = None; + self + } + + /// Build the menu item + pub fn build>(self, manager: &M) -> crate::Result> { + if self.icon.is_some() { + if let Some(id) = self.id { + IconMenuItem::with_id( + manager, + id, + self.text, + self.enabled, + self.icon, + self.accelerator, + ) + } else { + IconMenuItem::new( + manager, + self.text, + self.enabled, + self.icon, + self.accelerator, + ) + } + } else if let Some(id) = self.id { + IconMenuItem::with_id_and_native_icon( + manager, + id, + self.text, + self.enabled, + self.native_icon, + self.accelerator, + ) + } else { + IconMenuItem::with_native_icon( + manager, + self.text, + self.enabled, + self.native_icon, + self.accelerator, + ) + } + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/builders/menu.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/builders/menu.rs new file mode 100644 index 000000000..069ca4ddc --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/builders/menu.rs @@ -0,0 +1,675 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{Manager, Runtime, image::Image, menu::*}; + +/// A builder type for [`Menu`] +/// +/// ## Platform-specific: +/// +/// - **macOS**: if using [`MenuBuilder`] for the global menubar, it can only contain [`Submenu`]s +/// +/// # Example +/// +/// ```no_run +/// use tauri::menu::*; +/// tauri::Builder::default() +/// .setup(move |app| { +/// let handle = app.handle(); +/// # let icon1 = tauri::image::Image::new(&[], 0, 0); +/// let menu = MenuBuilder::new(handle) +/// .item(&MenuItem::new(handle, "MenuItem 1", true, None::<&str>)?) +/// .items(&[ +/// &CheckMenuItem::new(handle, "CheckMenuItem 1", true, true, None::<&str>)?, +/// &IconMenuItem::new(handle, "IconMenuItem 1", true, Some(icon1), None::<&str>)?, +/// ]) +/// .separator() +/// .cut() +/// .copy() +/// .paste() +/// .separator() +/// .text("item2", "MenuItem 2") +/// .check("checkitem2", "CheckMenuItem 2") +/// .icon("iconitem2", "IconMenuItem 2", app.default_window_icon().cloned().unwrap()) +/// .build()?; +/// app.set_menu(menu); +/// Ok(()) +/// }); +/// ``` +pub struct MenuBuilder<'m, R: Runtime, M: Manager> { + pub(crate) id: Option, + pub(crate) manager: &'m M, + pub(crate) items: Vec>>, +} + +impl<'m, R: Runtime, M: Manager> MenuBuilder<'m, R, M> { + /// Create a new menu builder. + pub fn new(manager: &'m M) -> Self { + Self { + id: None, + items: Vec::new(), + manager, + } + } + + /// Create a new menu builder with the specified id. + pub fn with_id>(manager: &'m M, id: I) -> Self { + Self { + id: Some(id.into()), + items: Vec::new(), + manager, + } + } + + /// Builds this menu + pub fn build(self) -> crate::Result> { + let menu = if let Some(id) = self.id { + Menu::with_id(self.manager, id)? + } else { + Menu::new(self.manager)? + }; + + for item in self.items { + let item = item?; + menu.append(&item)?; + } + + Ok(menu) + } +} + +/// A builder type for [`Submenu`] +/// +/// # Example +/// +/// ```no_run +/// use tauri::menu::*; +/// tauri::Builder::default() +/// .setup(move |app| { +/// let handle = app.handle(); +/// # let icon1 = tauri::image::Image::new(&[], 0, 0); +/// # let icon2 = icon1.clone(); +/// let menu = Menu::new(handle)?; +/// let submenu = SubmenuBuilder::new(handle, "File") +/// .item(&MenuItem::new(handle, "MenuItem 1", true, None::<&str>)?) +/// .items(&[ +/// &CheckMenuItem::new(handle, "CheckMenuItem 1", true, true, None::<&str>)?, +/// &IconMenuItem::new(handle, "IconMenuItem 1", true, Some(icon1), None::<&str>)?, +/// ]) +/// .separator() +/// .cut() +/// .copy() +/// .paste() +/// .separator() +/// .text("item2", "MenuItem 2") +/// .check("checkitem2", "CheckMenuItem 2") +/// .icon("iconitem2", "IconMenuItem 2", app.default_window_icon().cloned().unwrap()) +/// .build()?; +/// menu.append(&submenu)?; +/// app.set_menu(menu); +/// Ok(()) +/// }); +/// ``` +pub struct SubmenuBuilder<'m, R: Runtime, M: Manager> { + pub(crate) id: Option, + pub(crate) manager: &'m M, + pub(crate) text: String, + pub(crate) enabled: bool, + pub(crate) items: Vec>>, + pub(crate) icon: Option>, + pub(crate) native_icon: Option, +} + +impl<'m, R: Runtime, M: Manager> SubmenuBuilder<'m, R, M> { + /// Create a new submenu builder. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn new>(manager: &'m M, text: S) -> Self { + Self { + id: None, + items: Vec::new(), + text: text.as_ref().to_string(), + enabled: true, + manager, + icon: None, + native_icon: None, + } + } + + /// Create a new submenu builder with the specified id. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn with_id, S: AsRef>(manager: &'m M, id: I, text: S) -> Self { + Self { + id: Some(id.into()), + text: text.as_ref().to_string(), + enabled: true, + items: Vec::new(), + manager, + icon: None, + native_icon: None, + } + } + + /// Set an icon for the submenu. + /// Calling this method resets the native_icon. + pub fn submenu_icon(mut self, icon: crate::image::Image<'m>) -> Self { + self.icon = Some(icon); + self.native_icon = None; + self + } + + /// Set a native icon for the submenu. + /// Calling this method resets the icon. + pub fn submenu_native_icon(mut self, icon: NativeIcon) -> Self { + self.native_icon = Some(icon); + self.icon = None; + self + } + + /// Set the enabled state for the submenu. + pub fn enabled(mut self, enabled: bool) -> Self { + self.enabled = enabled; + self + } + + /// Builds this submenu + pub fn build(self) -> crate::Result> { + let submenu = if let Some(id) = self.id { + if let Some(icon) = self.icon { + Submenu::with_id_and_icon(self.manager, id, self.text, self.enabled, Some(icon))? + } else if let Some(native_icon) = self.native_icon { + Submenu::with_id_and_native_icon( + self.manager, + id, + self.text, + self.enabled, + Some(native_icon), + )? + } else { + Submenu::with_id(self.manager, id, self.text, self.enabled)? + } + } else if let Some(icon) = self.icon { + Submenu::new_with_icon(self.manager, self.text, self.enabled, Some(icon))? + } else if let Some(native_icon) = self.native_icon { + Submenu::new_with_native_icon(self.manager, self.text, self.enabled, Some(native_icon))? + } else { + Submenu::new(self.manager, self.text, self.enabled)? + }; + + for item in self.items { + let item = item?; + submenu.append(&item)?; + } + + Ok(submenu) + } +} + +macro_rules! shared_menu_builder { + ($menu: ty) => { + impl<'m, R: Runtime, M: Manager> $menu { + /// Set the id for this menu. + pub fn id>(mut self, id: I) -> Self { + self.id.replace(id.into()); + self + } + + /// Add this item to the menu. + pub fn item(mut self, item: &dyn IsMenuItem) -> Self { + self.items.push(Ok(item.kind())); + self + } + + /// Add these items to the menu. + pub fn items(mut self, items: &[&dyn IsMenuItem]) -> Self { + for item in items { + self = self.item(*item); + } + self + } + + /// Add a [MenuItem] to the menu. + pub fn text, S: AsRef>(mut self, id: I, text: S) -> Self { + self + .items + .push(MenuItem::with_id(self.manager, id, text, true, None::<&str>).map(|i| i.kind())); + self + } + + /// Add a [CheckMenuItem] to the menu. + pub fn check, S: AsRef>(mut self, id: I, text: S) -> Self { + self.items.push( + CheckMenuItem::with_id(self.manager, id, text, true, true, None::<&str>) + .map(|i| i.kind()), + ); + self + } + + /// Add an [IconMenuItem] to the menu. + pub fn icon, S: AsRef>( + mut self, + id: I, + text: S, + icon: Image<'_>, + ) -> Self { + self.items.push( + IconMenuItem::with_id(self.manager, id, text, true, Some(icon), None::<&str>) + .map(|i| i.kind()), + ); + self + } + + /// Add an [IconMenuItem] with a native icon to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux**: Unsupported. + pub fn native_icon, S: AsRef>( + mut self, + id: I, + text: S, + icon: NativeIcon, + ) -> Self { + self.items.push( + IconMenuItem::with_id_and_native_icon( + self.manager, + id, + text, + true, + Some(icon), + None::<&str>, + ) + .map(|i| i.kind()), + ); + self + } + + /// Add Separator menu item to the menu. + pub fn separator(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::separator(self.manager).map(|i| i.kind())); + self + } + + /// Add Copy menu item to the menu. + pub fn copy(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::copy(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Copy menu item with specified text to the menu. + pub fn copy_with_text>(mut self, text: S) -> Self { + self + .items + .push(PredefinedMenuItem::copy(self.manager, Some(text.as_ref())).map(|i| i.kind())); + self + } + + /// Add Cut menu item to the menu. + pub fn cut(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::cut(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Cut menu item with specified text to the menu. + pub fn cut_with_text>(mut self, text: S) -> Self { + self + .items + .push(PredefinedMenuItem::cut(self.manager, Some(text.as_ref())).map(|i| i.kind())); + self + } + + /// Add Paste menu item to the menu. + pub fn paste(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::paste(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Paste menu item with specified text to the menu. + pub fn paste_with_text>(mut self, text: S) -> Self { + self + .items + .push(PredefinedMenuItem::paste(self.manager, Some(text.as_ref())).map(|i| i.kind())); + self + } + + /// Add SelectAll menu item to the menu. + pub fn select_all(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::select_all(self.manager, None).map(|i| i.kind())); + self + } + + /// Add SelectAll menu item with specified text to the menu. + pub fn select_all_with_text>(mut self, text: S) -> Self { + self.items.push( + PredefinedMenuItem::select_all(self.manager, Some(text.as_ref())).map(|i| i.kind()), + ); + self + } + + /// Add Undo menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn undo(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::undo(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Undo menu item with specified text to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn undo_with_text>(mut self, text: S) -> Self { + self + .items + .push(PredefinedMenuItem::undo(self.manager, Some(text.as_ref())).map(|i| i.kind())); + self + } + /// Add Redo menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn redo(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::redo(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Redo menu item with specified text to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn redo_with_text>(mut self, text: S) -> Self { + self + .items + .push(PredefinedMenuItem::redo(self.manager, Some(text.as_ref())).map(|i| i.kind())); + self + } + + /// Add Minimize window menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn minimize(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::minimize(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Minimize window menu item with specified text to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn minimize_with_text>(mut self, text: S) -> Self { + self + .items + .push(PredefinedMenuItem::minimize(self.manager, Some(text.as_ref())).map(|i| i.kind())); + self + } + + /// Add Maximize window menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn maximize(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::maximize(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Maximize window menu item with specified text to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn maximize_with_text>(mut self, text: S) -> Self { + self + .items + .push(PredefinedMenuItem::maximize(self.manager, Some(text.as_ref())).map(|i| i.kind())); + self + } + + /// Add Fullscreen menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn fullscreen(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::fullscreen(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Fullscreen menu item with specified text to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn fullscreen_with_text>(mut self, text: S) -> Self { + self.items.push( + PredefinedMenuItem::fullscreen(self.manager, Some(text.as_ref())).map(|i| i.kind()), + ); + self + } + + /// Add Hide window menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn hide(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::hide(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Hide window menu item with specified text to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn hide_with_text>(mut self, text: S) -> Self { + self + .items + .push(PredefinedMenuItem::hide(self.manager, Some(text.as_ref())).map(|i| i.kind())); + self + } + + /// Add Hide other windows menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn hide_others(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::hide_others(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Hide other windows menu item with specified text to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn hide_others_with_text>(mut self, text: S) -> Self { + self.items.push( + PredefinedMenuItem::hide_others(self.manager, Some(text.as_ref())).map(|i| i.kind()), + ); + self + } + + /// Add Show all app windows menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn show_all(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::show_all(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Show all app windows menu item with specified text to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn show_all_with_text>(mut self, text: S) -> Self { + self + .items + .push(PredefinedMenuItem::show_all(self.manager, Some(text.as_ref())).map(|i| i.kind())); + self + } + + /// Add Close window menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn close_window(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::close_window(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Close window menu item with specified text to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn close_window_with_text>(mut self, text: S) -> Self { + self.items.push( + PredefinedMenuItem::close_window(self.manager, Some(text.as_ref())).map(|i| i.kind()), + ); + self + } + + /// Add Quit app menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn quit(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::quit(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Quit app menu item with specified text to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn quit_with_text>(mut self, text: S) -> Self { + self + .items + .push(PredefinedMenuItem::quit(self.manager, Some(text.as_ref())).map(|i| i.kind())); + self + } + + /// Add About app menu item to the menu. + pub fn about(mut self, metadata: Option>) -> Self { + self + .items + .push(PredefinedMenuItem::about(self.manager, None, metadata).map(|i| i.kind())); + self + } + + /// Add About app menu item with specified text to the menu. + pub fn about_with_text>( + mut self, + text: S, + metadata: Option>, + ) -> Self { + self.items.push( + PredefinedMenuItem::about(self.manager, Some(text.as_ref()), metadata).map(|i| i.kind()), + ); + self + } + + /// Add Services menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn services(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::services(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Services menu item with specified text to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn services_with_text>(mut self, text: S) -> Self { + self + .items + .push(PredefinedMenuItem::services(self.manager, Some(text.as_ref())).map(|i| i.kind())); + self + } + + /// Add Bring All to Front menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn bring_all_to_front(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::bring_all_to_front(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Bring All to Front menu item with specified text to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn bring_all_to_front_with_text>(mut self, text: S) -> Self { + self.items.push( + PredefinedMenuItem::bring_all_to_front(self.manager, Some(text.as_ref())) + .map(|i| i.kind()), + ); + self + } + } + }; +} + +shared_menu_builder!(MenuBuilder<'m, R, M>); +shared_menu_builder!(SubmenuBuilder<'m, R, M>); diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/builders/mod.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/builders/mod.rs new file mode 100644 index 000000000..ce816fbd6 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/builders/mod.rs @@ -0,0 +1,17 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![cfg(desktop)] + +//! A module containing menu builder types + +mod menu; +pub use menu::MenuBuilder; +pub use menu::SubmenuBuilder; +mod normal; +pub use normal::MenuItemBuilder; +mod check; +pub use check::CheckMenuItemBuilder; +mod icon; +pub use icon::IconMenuItemBuilder; diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/builders/normal.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/builders/normal.rs new file mode 100644 index 000000000..e5cb5143b --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/builders/normal.rs @@ -0,0 +1,68 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{Manager, Runtime, menu::MenuId, menu::MenuItem}; + +/// A builder type for [`MenuItem`] +pub struct MenuItemBuilder { + id: Option, + text: String, + enabled: bool, + accelerator: Option, +} + +impl MenuItemBuilder { + /// Create a new menu item builder. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn new>(text: S) -> Self { + Self { + id: None, + text: text.as_ref().to_string(), + enabled: true, + accelerator: None, + } + } + + /// Create a new menu item builder with the specified id. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn with_id, S: AsRef>(id: I, text: S) -> Self { + Self { + id: Some(id.into()), + text: text.as_ref().to_string(), + enabled: true, + accelerator: None, + } + } + + /// Set the id for this menu item. + pub fn id>(mut self, id: I) -> Self { + self.id.replace(id.into()); + self + } + + /// Set the enabled state for this menu item. + pub fn enabled(mut self, enabled: bool) -> Self { + self.enabled = enabled; + self + } + + /// Set the accelerator for this menu item. + pub fn accelerator>(mut self, accelerator: S) -> Self { + self.accelerator.replace(accelerator.as_ref().to_string()); + self + } + + /// Build the menu item + pub fn build>(self, manager: &M) -> crate::Result> { + if let Some(id) = self.id { + MenuItem::with_id(manager, id, self.text, self.enabled, self.accelerator) + } else { + MenuItem::new(manager, self.text, self.enabled, self.accelerator) + } + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/check.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/check.rs new file mode 100644 index 000000000..cf7d3c99b --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/check.rs @@ -0,0 +1,129 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::sync::Arc; + +use super::run_item_main_thread; +use crate::menu::CheckMenuItemInner; +use crate::run_main_thread; +use crate::{AppHandle, Manager, Runtime, menu::MenuId}; + +use super::CheckMenuItem; + +impl CheckMenuItem { + /// Create a new menu item. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn new( + manager: &M, + text: T, + enabled: bool, + checked: bool, + accelerator: Option, + ) -> crate::Result + where + M: Manager, + T: AsRef, + A: AsRef, + { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.as_ref().to_owned(); + let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok()); + + let item = run_main_thread!(handle, || { + let item = muda::CheckMenuItem::new(text, enabled, checked, accelerator); + CheckMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Create a new menu item with the specified id. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn with_id( + manager: &M, + id: I, + text: T, + enabled: bool, + checked: bool, + accelerator: Option, + ) -> crate::Result + where + M: Manager, + I: Into, + T: AsRef, + A: AsRef, + { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let id = id.into(); + let text = text.as_ref().to_owned(); + let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok()); + + let item = run_main_thread!(handle, move || { + let item = muda::CheckMenuItem::with_id(id, text, enabled, checked, accelerator); + CheckMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// The application handle associated with this type. + pub fn app_handle(&self) -> &AppHandle { + &self.0.app_handle + } + + /// Returns a unique identifier associated with this menu item. + pub fn id(&self) -> &MenuId { + self.0.inner.id() + } + + /// Get the text for this menu item. + pub fn text(&self) -> crate::Result { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().text()) + } + + /// Set the text for this menu item. `text` could optionally contain + /// an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn set_text>(&self, text: S) -> crate::Result<()> { + let text = text.as_ref().to_string(); + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_text(text)) + } + + /// Get whether this menu item is enabled or not. + pub fn is_enabled(&self) -> crate::Result { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().is_enabled()) + } + + /// Enable or disable this menu item. + pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_enabled(enabled)) + } + + /// Set this menu item accelerator. + pub fn set_accelerator>(&self, accelerator: Option) -> crate::Result<()> { + let accel = accelerator.and_then(|s| s.as_ref().parse().ok()); + run_item_main_thread!(self, |self_: Self| { + (*self_.0).as_ref().set_accelerator(accel) + })? + .map_err(Into::into) + } + + /// Get whether this check menu item is checked or not. + pub fn is_checked(&self) -> crate::Result { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().is_checked()) + } + + /// Check or Uncheck this check menu item. + pub fn set_checked(&self, checked: bool) -> crate::Result<()> { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_checked(checked)) + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/icon.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/icon.rs new file mode 100644 index 000000000..d74cff6bf --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/icon.rs @@ -0,0 +1,220 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::sync::Arc; + +use super::run_item_main_thread; +use super::{IconMenuItem, NativeIcon}; +use crate::menu::IconMenuItemInner; +use crate::run_main_thread; +use crate::{AppHandle, Manager, Runtime, image::Image, menu::MenuId}; + +impl IconMenuItem { + /// Create a new menu item. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn new( + manager: &M, + text: T, + enabled: bool, + icon: Option>, + accelerator: Option, + ) -> crate::Result + where + M: Manager, + T: AsRef, + A: AsRef, + { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.as_ref().to_owned(); + let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok()); + let icon = match icon { + Some(i) => Some(i.try_into()?), + None => None, + }; + + let item = run_main_thread!(handle, || { + let item = muda::IconMenuItem::new(text, enabled, icon, accelerator); + IconMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Create a new menu item with the specified id. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn with_id( + manager: &M, + id: I, + text: T, + enabled: bool, + icon: Option>, + accelerator: Option, + ) -> crate::Result + where + M: Manager, + I: Into, + T: AsRef, + A: AsRef, + { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let id = id.into(); + let text = text.as_ref().to_owned(); + let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok()); + let icon = match icon { + Some(i) => Some(i.try_into()?), + None => None, + }; + + let item = run_main_thread!(handle, move || { + let item = muda::IconMenuItem::with_id(id, text, enabled, icon, accelerator); + IconMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Create a new icon menu item but with a native icon. + /// + /// See [`IconMenuItem::new`] for more info. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux**: Unsupported. + pub fn with_native_icon( + manager: &M, + text: T, + enabled: bool, + native_icon: Option, + accelerator: Option, + ) -> crate::Result + where + M: Manager, + T: AsRef, + A: AsRef, + { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.as_ref().to_owned(); + let icon = native_icon.map(Into::into); + let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok()); + + let item = run_main_thread!(handle, || { + let item = muda::IconMenuItem::with_native_icon(text, enabled, icon, accelerator); + IconMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Create a new icon menu item with the specified id but with a native icon. + /// + /// See [`IconMenuItem::new`] for more info. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux**: Unsupported. + pub fn with_id_and_native_icon( + manager: &M, + id: I, + text: T, + enabled: bool, + native_icon: Option, + accelerator: Option, + ) -> crate::Result + where + M: Manager, + I: Into, + T: AsRef, + A: AsRef, + { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let id = id.into(); + let text = text.as_ref().to_owned(); + let icon = native_icon.map(Into::into); + let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok()); + + let item = run_main_thread!(handle, move || { + let item = muda::IconMenuItem::with_id_and_native_icon(id, text, enabled, icon, accelerator); + IconMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// The application handle associated with this type. + pub fn app_handle(&self) -> &AppHandle { + &self.0.app_handle + } + + /// Returns a unique identifier associated with this menu item. + pub fn id(&self) -> &MenuId { + self.0.inner.id() + } + + /// Get the text for this menu item. + pub fn text(&self) -> crate::Result { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().text()) + } + + /// Set the text for this menu item. `text` could optionally contain + /// an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn set_text>(&self, text: S) -> crate::Result<()> { + let text = text.as_ref().to_string(); + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_text(text)) + } + + /// Get whether this menu item is enabled or not. + pub fn is_enabled(&self) -> crate::Result { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().is_enabled()) + } + + /// Enable or disable this menu item. + pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_enabled(enabled)) + } + + /// Set this menu item accelerator. + pub fn set_accelerator>(&self, accelerator: Option) -> crate::Result<()> { + let accel = accelerator.and_then(|s| s.as_ref().parse().ok()); + run_item_main_thread!(self, |self_: Self| { + (*self_.0).as_ref().set_accelerator(accel) + })? + .map_err(Into::into) + } + + /// Change this menu item icon or remove it. + pub fn set_icon(&self, icon: Option>) -> crate::Result<()> { + let icon = match icon { + Some(i) => Some(i.try_into()?), + None => None, + }; + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_icon(icon)) + } + + /// Change this menu item icon to a native image or remove it. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux**: Unsupported. + pub fn set_native_icon(&self, _icon: Option) -> crate::Result<()> { + #[cfg(target_os = "macos")] + return run_item_main_thread!(self, |self_: Self| { + (*self_.0).as_ref().set_native_icon(_icon.map(Into::into)) + }); + #[allow(unreachable_code)] + Ok(()) + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/menu.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/menu.rs new file mode 100644 index 000000000..ba99e47f5 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/menu.rs @@ -0,0 +1,400 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::sync::Arc; + +use super::run_item_main_thread; +use super::sealed::ContextMenuBase; +use super::{ + AboutMetadata, IsMenuItem, Menu, MenuInner, MenuItemKind, PredefinedMenuItem, Submenu, +}; +use crate::Window; +use crate::run_main_thread; +use crate::{AppHandle, Manager, Position, Runtime}; +use muda::ContextMenu; +use muda::MenuId; + +/// Expected submenu id of the Window menu for macOS. +pub const WINDOW_SUBMENU_ID: &str = "__tauri_window_menu__"; +/// Expected submenu id of the Help menu for macOS. +pub const HELP_SUBMENU_ID: &str = "__tauri_help_menu__"; + +impl super::ContextMenu for Menu { + #[cfg(target_os = "windows")] + fn hpopupmenu(&self) -> crate::Result { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().hpopupmenu()) + } + + fn popup(&self, window: Window) -> crate::Result<()> { + self.popup_inner(window, None::) + } + + fn popup_at>( + &self, + window: Window, + position: P, + ) -> crate::Result<()> { + self.popup_inner(window, Some(position)) + } +} + +impl ContextMenuBase for Menu { + fn popup_inner>( + &self, + window: crate::Window, + position: Option

, + ) -> crate::Result<()> { + let position = position.map(Into::into); + run_item_main_thread!(self, move |self_: Self| { + #[cfg(target_os = "macos")] + if let Ok(view) = window.ns_view() { + unsafe { + self_ + .inner() + .show_context_menu_for_nsview(view as _, position); + } + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + if let Ok(w) = window.gtk_window() { + self_ + .inner() + .show_context_menu_for_gtk_window(w.as_ref(), position); + } + + #[cfg(windows)] + if let Ok(hwnd) = window.hwnd() { + unsafe { + self_ + .inner() + .show_context_menu_for_hwnd(hwnd.0 as _, position); + } + } + }) + } + fn inner_context(&self) -> &dyn muda::ContextMenu { + (*self.0).as_ref() + } + + fn inner_context_owned(&self) -> Box { + Box::new((*self.0).as_ref().clone()) + } +} + +impl Menu { + /// Creates a new menu. + pub fn new>(manager: &M) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let menu = run_main_thread!(handle, || { + let menu = muda::Menu::new(); + MenuInner::new(app_handle, menu) + })?; + + Ok(Self(Arc::new(menu))) + } + + /// Creates a new menu with the specified id. + pub fn with_id, I: Into>(manager: &M, id: I) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let id = id.into(); + let menu = run_main_thread!(handle, move || { + let menu = muda::Menu::with_id(id); + MenuInner::new(app_handle, menu) + })?; + + Ok(Self(Arc::new(menu))) + } + + /// Creates a new menu with given `items`. It calls [`Menu::new`] and [`Menu::append_items`] internally. + pub fn with_items>( + manager: &M, + items: &[&dyn IsMenuItem], + ) -> crate::Result { + let menu = Self::new(manager)?; + menu.append_items(items)?; + Ok(menu) + } + + /// Creates a new menu with the specified id and given `items`. + /// It calls [`Menu::new`] and [`Menu::append_items`] internally. + pub fn with_id_and_items, I: Into>( + manager: &M, + id: I, + items: &[&dyn IsMenuItem], + ) -> crate::Result { + let menu = Self::with_id(manager, id)?; + menu.append_items(items)?; + Ok(menu) + } + + /// Creates a menu filled with default menu items and submenus. + pub fn default(app_handle: &AppHandle) -> crate::Result { + let pkg_info = app_handle.package_info(); + let config = app_handle.config(); + let about_metadata = AboutMetadata { + name: Some(pkg_info.name.clone()), + version: Some(pkg_info.version.to_string()), + copyright: config.bundle.copyright.clone(), + authors: config.bundle.publisher.clone().map(|p| vec![p]), + ..Default::default() + }; + + let window_menu = Submenu::with_id_and_items( + app_handle, + WINDOW_SUBMENU_ID, + "Window", + true, + &[ + &PredefinedMenuItem::minimize(app_handle, None)?, + &PredefinedMenuItem::maximize(app_handle, None)?, + #[cfg(target_os = "macos")] + &PredefinedMenuItem::separator(app_handle)?, + &PredefinedMenuItem::close_window(app_handle, None)?, + ], + )?; + + let help_menu = Submenu::with_id_and_items( + app_handle, + HELP_SUBMENU_ID, + "Help", + true, + &[ + #[cfg(not(target_os = "macos"))] + &PredefinedMenuItem::about(app_handle, None, Some(about_metadata))?, + ], + )?; + + let menu = Menu::with_items( + app_handle, + &[ + #[cfg(target_os = "macos")] + &Submenu::with_items( + app_handle, + pkg_info.name.clone(), + true, + &[ + &PredefinedMenuItem::about(app_handle, None, Some(about_metadata))?, + &PredefinedMenuItem::separator(app_handle)?, + &PredefinedMenuItem::services(app_handle, None)?, + &PredefinedMenuItem::separator(app_handle)?, + &PredefinedMenuItem::hide(app_handle, None)?, + &PredefinedMenuItem::hide_others(app_handle, None)?, + &PredefinedMenuItem::separator(app_handle)?, + &PredefinedMenuItem::quit(app_handle, None)?, + ], + )?, + #[cfg(not(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + )))] + &Submenu::with_items( + app_handle, + "File", + true, + &[ + &PredefinedMenuItem::close_window(app_handle, None)?, + #[cfg(not(target_os = "macos"))] + &PredefinedMenuItem::quit(app_handle, None)?, + ], + )?, + &Submenu::with_items( + app_handle, + "Edit", + true, + &[ + &PredefinedMenuItem::undo(app_handle, None)?, + &PredefinedMenuItem::redo(app_handle, None)?, + &PredefinedMenuItem::separator(app_handle)?, + &PredefinedMenuItem::cut(app_handle, None)?, + &PredefinedMenuItem::copy(app_handle, None)?, + &PredefinedMenuItem::paste(app_handle, None)?, + &PredefinedMenuItem::select_all(app_handle, None)?, + ], + )?, + #[cfg(target_os = "macos")] + &Submenu::with_items( + app_handle, + "View", + true, + &[&PredefinedMenuItem::fullscreen(app_handle, None)?], + )?, + &window_menu, + &help_menu, + ], + )?; + + Ok(menu) + } + + pub(crate) fn inner(&self) -> &muda::Menu { + (*self.0).as_ref() + } + + /// The application handle associated with this type. + pub fn app_handle(&self) -> &AppHandle { + &self.0.app_handle + } + + /// Returns a unique identifier associated with this menu. + pub fn id(&self) -> &MenuId { + self.0.inner.id() + } + + /// Add a menu item to the end of this menu. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Only [`Submenu`] can be added to the menu. + /// + /// [`Submenu`]: super::Submenu + pub fn append(&self, item: &dyn IsMenuItem) -> crate::Result<()> { + let kind = item.kind(); + run_item_main_thread!(self, |self_: Self| { + (*self_.0).as_ref().append(kind.inner().inner_muda()) + })? + .map_err(Into::into) + } + + /// Add menu items to the end of this menu. It calls [`Menu::append`] in a loop internally. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Only [`Submenu`] can be added to the menu + /// + /// [`Submenu`]: super::Submenu + pub fn append_items(&self, items: &[&dyn IsMenuItem]) -> crate::Result<()> { + for item in items { + self.append(*item)? + } + + Ok(()) + } + + /// Add a menu item to the beginning of this menu. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Only [`Submenu`] can be added to the menu + /// + /// [`Submenu`]: super::Submenu + pub fn prepend(&self, item: &dyn IsMenuItem) -> crate::Result<()> { + let kind = item.kind(); + run_item_main_thread!(self, |self_: Self| { + (*self_.0).as_ref().prepend(kind.inner().inner_muda()) + })? + .map_err(Into::into) + } + + /// Add menu items to the beginning of this menu. It calls [`Menu::insert_items`] with position of `0` internally. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Only [`Submenu`] can be added to the menu + /// + /// [`Submenu`]: super::Submenu + pub fn prepend_items(&self, items: &[&dyn IsMenuItem]) -> crate::Result<()> { + self.insert_items(items, 0) + } + + /// Insert a menu item at the specified `position` in the menu. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Only [`Submenu`] can be added to the menu + /// + /// [`Submenu`]: super::Submenu + pub fn insert(&self, item: &dyn IsMenuItem, position: usize) -> crate::Result<()> { + let kind = item.kind(); + run_item_main_thread!(self, |self_: Self| (*self_.0) + .as_ref() + .insert(kind.inner().inner_muda(), position))? + .map_err(Into::into) + } + + /// Insert menu items at the specified `position` in the menu. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Only [`Submenu`] can be added to the menu + /// + /// [`Submenu`]: super::Submenu + pub fn insert_items(&self, items: &[&dyn IsMenuItem], position: usize) -> crate::Result<()> { + for (i, item) in items.iter().enumerate() { + self.insert(*item, position + i)? + } + + Ok(()) + } + + /// Remove a menu item from this menu. + pub fn remove(&self, item: &dyn IsMenuItem) -> crate::Result<()> { + let kind = item.kind(); + run_item_main_thread!(self, |self_: Self| { + (*self_.0).as_ref().remove(kind.inner().inner_muda()) + })? + .map_err(Into::into) + } + + /// Remove the menu item at the specified position from this menu and returns it. + pub fn remove_at(&self, position: usize) -> crate::Result>> { + run_item_main_thread!(self, |self_: Self| { + (*self_.0) + .as_ref() + .remove_at(position) + .map(|i| MenuItemKind::from_muda(self_.0.app_handle.clone(), i)) + }) + } + + /// Retrieves the menu item matching the given identifier. + pub fn get<'a, I>(&self, id: &'a I) -> Option> + where + I: ?Sized, + MenuId: PartialEq<&'a I>, + { + self + .items() + .unwrap_or_default() + .into_iter() + .find(|i| i.id() == &id) + } + + /// Returns a list of menu items that has been added to this menu. + pub fn items(&self) -> crate::Result>> { + run_item_main_thread!(self, |self_: Self| { + (*self_.0) + .as_ref() + .items() + .into_iter() + .map(|i| MenuItemKind::from_muda(self_.0.app_handle.clone(), i)) + .collect::>() + }) + } + + /// Set this menu as the application menu. + /// + /// This is an alias for [`AppHandle::set_menu`]. + pub fn set_as_app_menu(&self) -> crate::Result>> { + self.0.app_handle.set_menu(self.clone()) + } + + /// Set this menu as the window menu. + /// + /// This is an alias for [`Window::set_menu`]. + pub fn set_as_window_menu(&self, window: &Window) -> crate::Result>> { + window.set_menu(self.clone()) + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/mod.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/mod.rs new file mode 100644 index 000000000..ab4359203 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/mod.rs @@ -0,0 +1,771 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Menu types and utilities. + +mod builders; +mod check; +mod icon; +#[allow(clippy::module_inception)] +mod menu; +mod normal; +pub(crate) mod plugin; +mod predefined; +mod submenu; +use std::{mem::ManuallyDrop, sync::Arc}; + +pub use builders::*; +pub use menu::{HELP_SUBMENU_ID, WINDOW_SUBMENU_ID}; +use serde::{Deserialize, Serialize}; + +use crate::menu::plugin::remove_menu_channel; +use crate::{AppHandle, Runtime, image::Image}; +pub use muda::MenuId; + +macro_rules! run_item_main_thread { + ($self:ident, $ex:expr) => {{ + use std::sync::mpsc::channel; + let (tx, rx) = channel(); + let self_ = $self.clone(); + let task = move || { + let f = $ex; + let _ = tx.send(f(self_)); + }; + $self + .app_handle() + .run_on_main_thread(task) + .and_then(|_| rx.recv().map_err(|_| crate::Error::FailedToReceiveMessage)) + }}; +} + +pub(crate) use run_item_main_thread; + +/// Describes a menu event emitted when a menu item is activated +#[derive(Debug, Clone, Serialize)] +pub struct MenuEvent { + /// Id of the menu item which triggered this event + pub id: MenuId, +} + +impl MenuEvent { + /// Returns the id of the menu item which triggered this event + pub fn id(&self) -> &MenuId { + &self.id + } +} + +impl From for MenuEvent { + fn from(value: muda::MenuEvent) -> Self { + Self { id: value.id } + } +} + +macro_rules! gen_wrappers { + ( + $( + $(#[$attr:meta])* + $type:ident($inner:ident$(, $kind:ident)?) + ),* + ) => { + $( + #[tauri_macros::default_runtime(crate::Wry, wry)] + pub(crate) struct $inner { + // This [`ManuallyDrop`] is used to [`ManuallyDrop::take`] in [`Self::drop`] to drop it on main thread + inner: ManuallyDrop<::muda::$type>, + app_handle: $crate::AppHandle, + } + + impl $inner { + fn new(app_handle: $crate::AppHandle, menu: ::muda::$type) -> Self { + Self { + inner: ManuallyDrop::new(menu), + app_handle, + } + } + } + + /// # Safety + /// + /// We make sure it always runs on the main thread. + unsafe impl Sync for $inner {} + unsafe impl Send for $inner {} + + impl $crate::Resource for $type {} + + impl Drop for $inner { + fn drop(&mut self) { + remove_menu_channel(&self.app_handle, self.inner.id()); + // SAFETY: we will not access `self.inner` after this + let inner = unsafe { ManuallyDrop::take(&mut self.inner) }; + // SAFETY: inner was created on main thread and is being dropped on main thread + let inner = $crate::UnsafeSend(inner); + let _ = self.app_handle.run_on_main_thread(move || { + drop(inner.take()); + }); + } + } + + impl AsRef<::muda::$type> for $inner { + fn as_ref(&self) -> &::muda::$type { + &self.inner + } + } + + $(#[$attr])* + pub struct $type(::std::sync::Arc<$inner>); + + impl Clone for $type { + fn clone(&self) -> Self { + Self(self.0.clone()) + } + } + + $( + impl $crate::menu::sealed::IsMenuItemBase for $type { + fn inner_muda(&self) -> &dyn muda::IsMenuItem { + (*self.0).as_ref() + } + } + + impl $crate::menu::IsMenuItem for $type { + fn kind(&self) -> MenuItemKind { + MenuItemKind::$kind(self.clone()) + } + + fn id(&self) -> &MenuId { + self.0.inner.id() + } + } + )* + )* + }; +} + +gen_wrappers!( + /// A type that is either a menu bar on the window + /// on Windows and Linux or as a global menu in the menubar on macOS. + /// + /// ## Platform-specific: + /// + /// - **macOS**: if using [`Menu`] for the global menubar, it can only contain [`Submenu`]s + Menu(MenuInner), + /// A menu item inside a [`Menu`] or [`Submenu`] and contains only text. + MenuItem(MenuItemInner, MenuItem), + /// A type that is a submenu inside a [`Menu`] or [`Submenu`] + Submenu(SubmenuInner, Submenu), + /// A predefined (native) menu item which has a predefined behavior by the OS or by this crate. + PredefinedMenuItem(PredefinedMenuItemInner, Predefined), + /// A menu item inside a [`Menu`] or [`Submenu`] + /// and usually contains a text and a check mark or a similar toggle + /// that corresponds to a checked and unchecked states. + CheckMenuItem(CheckMenuItemInner, Check), + /// A menu item inside a [`Menu`] or [`Submenu`] + /// and usually contains an icon and a text. + IconMenuItem(IconMenuItemInner, Icon) +); + +/// Application metadata for the [`PredefinedMenuItem::about`]. +#[derive(Debug, Clone, Default)] +pub struct AboutMetadata<'a> { + /// Sets the application name. + pub name: Option, + /// The application version. + pub version: Option, + /// The short version, e.g. "1.0". + /// + /// ## Platform-specific + /// + /// - **Windows / Linux:** Appended to the end of `version` in parentheses. + pub short_version: Option, + /// The authors of the application. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub authors: Option>, + /// Application comments. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub comments: Option, + /// The copyright of the application. + pub copyright: Option, + /// The license of the application. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub license: Option, + /// The application website. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub website: Option, + /// The website label. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub website_label: Option, + /// The credits. + /// + /// ## Platform-specific + /// + /// - **Windows / Linux:** Unsupported. + pub credits: Option, + /// The application icon. + /// + /// ## Platform-specific + /// + /// - **Windows:** Unsupported. + pub icon: Option>, +} + +/// A builder type for [`AboutMetadata`]. +#[derive(Clone, Debug, Default)] +pub struct AboutMetadataBuilder<'a>(AboutMetadata<'a>); + +impl<'a> AboutMetadataBuilder<'a> { + /// Create a new about metadata builder. + pub fn new() -> Self { + Default::default() + } + + /// Sets the application name. + pub fn name>(mut self, name: Option) -> Self { + self.0.name = name.map(|s| s.into()); + self + } + /// Sets the application version. + pub fn version>(mut self, version: Option) -> Self { + self.0.version = version.map(|s| s.into()); + self + } + /// Sets the short version, e.g. "1.0". + /// + /// ## Platform-specific + /// + /// - **Windows / Linux:** Appended to the end of `version` in parentheses. + pub fn short_version>(mut self, short_version: Option) -> Self { + self.0.short_version = short_version.map(|s| s.into()); + self + } + /// Sets the authors of the application. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub fn authors(mut self, authors: Option>) -> Self { + self.0.authors = authors; + self + } + /// Application comments. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub fn comments>(mut self, comments: Option) -> Self { + self.0.comments = comments.map(|s| s.into()); + self + } + /// Sets the copyright of the application. + pub fn copyright>(mut self, copyright: Option) -> Self { + self.0.copyright = copyright.map(|s| s.into()); + self + } + /// Sets the license of the application. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub fn license>(mut self, license: Option) -> Self { + self.0.license = license.map(|s| s.into()); + self + } + /// Sets the application website. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub fn website>(mut self, website: Option) -> Self { + self.0.website = website.map(|s| s.into()); + self + } + /// Sets the website label. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub fn website_label>(mut self, website_label: Option) -> Self { + self.0.website_label = website_label.map(|s| s.into()); + self + } + /// Sets the credits. + /// + /// ## Platform-specific + /// + /// - **Windows / Linux:** Unsupported. + pub fn credits>(mut self, credits: Option) -> Self { + self.0.credits = credits.map(|s| s.into()); + self + } + /// Sets the application icon. + /// + /// ## Platform-specific + /// + /// - **Windows:** Unsupported. + pub fn icon(mut self, icon: Option>) -> Self { + self.0.icon = icon; + self + } + + /// Construct the final [`AboutMetadata`] + pub fn build(self) -> AboutMetadata<'a> { + self.0 + } +} + +impl TryFrom> for muda::AboutMetadata { + type Error = crate::Error; + + fn try_from(value: AboutMetadata<'_>) -> Result { + let icon = match value.icon { + Some(i) => Some(i.try_into()?), + None => None, + }; + + Ok(Self { + authors: value.authors, + name: value.name, + version: value.version, + short_version: value.short_version, + comments: value.comments, + copyright: value.copyright, + license: value.license, + website: value.website, + website_label: value.website_label, + credits: value.credits, + icon, + }) + } +} + +/// A native Icon to be used for the menu item +/// +/// ## Platform-specific: +/// +/// - **Windows / Linux**: Unsupported. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +pub enum NativeIcon { + /// An add item template image. + Add, + /// Advanced preferences toolbar icon for the preferences window. + Advanced, + /// A Bluetooth template image. + Bluetooth, + /// Bookmarks image suitable for a template. + Bookmarks, + /// A caution image. + Caution, + /// A color panel toolbar icon. + ColorPanel, + /// A column view mode template image. + ColumnView, + /// A computer icon. + Computer, + /// An enter full-screen mode template image. + EnterFullScreen, + /// Permissions for all users. + Everyone, + /// An exit full-screen mode template image. + ExitFullScreen, + /// A cover flow view mode template image. + FlowView, + /// A folder image. + Folder, + /// A burnable folder icon. + FolderBurnable, + /// A smart folder icon. + FolderSmart, + /// A link template image. + FollowLinkFreestanding, + /// A font panel toolbar icon. + FontPanel, + /// A `go back` template image. + GoLeft, + /// A `go forward` template image. + GoRight, + /// Home image suitable for a template. + Home, + /// An iChat Theater template image. + IChatTheater, + /// An icon view mode template image. + IconView, + /// An information toolbar icon. + Info, + /// A template image used to denote invalid data. + InvalidDataFreestanding, + /// A generic left-facing triangle template image. + LeftFacingTriangle, + /// A list view mode template image. + ListView, + /// A locked padlock template image. + LockLocked, + /// An unlocked padlock template image. + LockUnlocked, + /// A horizontal dash, for use in menus. + MenuMixedState, + /// A check mark template image, for use in menus. + MenuOnState, + /// A MobileMe icon. + MobileMe, + /// A drag image for multiple items. + MultipleDocuments, + /// A network icon. + Network, + /// A path button template image. + Path, + /// General preferences toolbar icon for the preferences window. + PreferencesGeneral, + /// A Quick Look template image. + QuickLook, + /// A refresh template image. + RefreshFreestanding, + /// A refresh template image. + Refresh, + /// A remove item template image. + Remove, + /// A reveal contents template image. + RevealFreestanding, + /// A generic right-facing triangle template image. + RightFacingTriangle, + /// A share view template image. + Share, + /// A slideshow template image. + Slideshow, + /// A badge for a `smart` item. + SmartBadge, + /// Small green indicator, similar to iChat's available image. + StatusAvailable, + /// Small clear indicator. + StatusNone, + /// Small yellow indicator, similar to iChat's idle image. + StatusPartiallyAvailable, + /// Small red indicator, similar to iChat's unavailable image. + StatusUnavailable, + /// A stop progress template image. + StopProgressFreestanding, + /// A stop progress button template image. + StopProgress, + /// An image of the empty trash can. + TrashEmpty, + /// An image of the full trash can. + TrashFull, + /// Permissions for a single user. + User, + /// User account toolbar icon for the preferences window. + UserAccounts, + /// Permissions for a group of users. + UserGroup, + /// Permissions for guests. + UserGuest, +} + +impl From for muda::NativeIcon { + fn from(value: NativeIcon) -> Self { + match value { + NativeIcon::Add => muda::NativeIcon::Add, + NativeIcon::Advanced => muda::NativeIcon::Advanced, + NativeIcon::Bluetooth => muda::NativeIcon::Bluetooth, + NativeIcon::Bookmarks => muda::NativeIcon::Bookmarks, + NativeIcon::Caution => muda::NativeIcon::Caution, + NativeIcon::ColorPanel => muda::NativeIcon::ColorPanel, + NativeIcon::ColumnView => muda::NativeIcon::ColumnView, + NativeIcon::Computer => muda::NativeIcon::Computer, + NativeIcon::EnterFullScreen => muda::NativeIcon::EnterFullScreen, + NativeIcon::Everyone => muda::NativeIcon::Everyone, + NativeIcon::ExitFullScreen => muda::NativeIcon::ExitFullScreen, + NativeIcon::FlowView => muda::NativeIcon::FlowView, + NativeIcon::Folder => muda::NativeIcon::Folder, + NativeIcon::FolderBurnable => muda::NativeIcon::FolderBurnable, + NativeIcon::FolderSmart => muda::NativeIcon::FolderSmart, + NativeIcon::FollowLinkFreestanding => muda::NativeIcon::FollowLinkFreestanding, + NativeIcon::FontPanel => muda::NativeIcon::FontPanel, + NativeIcon::GoLeft => muda::NativeIcon::GoLeft, + NativeIcon::GoRight => muda::NativeIcon::GoRight, + NativeIcon::Home => muda::NativeIcon::Home, + NativeIcon::IChatTheater => muda::NativeIcon::IChatTheater, + NativeIcon::IconView => muda::NativeIcon::IconView, + NativeIcon::Info => muda::NativeIcon::Info, + NativeIcon::InvalidDataFreestanding => muda::NativeIcon::InvalidDataFreestanding, + NativeIcon::LeftFacingTriangle => muda::NativeIcon::LeftFacingTriangle, + NativeIcon::ListView => muda::NativeIcon::ListView, + NativeIcon::LockLocked => muda::NativeIcon::LockLocked, + NativeIcon::LockUnlocked => muda::NativeIcon::LockUnlocked, + NativeIcon::MenuMixedState => muda::NativeIcon::MenuMixedState, + NativeIcon::MenuOnState => muda::NativeIcon::MenuOnState, + NativeIcon::MobileMe => muda::NativeIcon::MobileMe, + NativeIcon::MultipleDocuments => muda::NativeIcon::MultipleDocuments, + NativeIcon::Network => muda::NativeIcon::Network, + NativeIcon::Path => muda::NativeIcon::Path, + NativeIcon::PreferencesGeneral => muda::NativeIcon::PreferencesGeneral, + NativeIcon::QuickLook => muda::NativeIcon::QuickLook, + NativeIcon::RefreshFreestanding => muda::NativeIcon::RefreshFreestanding, + NativeIcon::Refresh => muda::NativeIcon::Refresh, + NativeIcon::Remove => muda::NativeIcon::Remove, + NativeIcon::RevealFreestanding => muda::NativeIcon::RevealFreestanding, + NativeIcon::RightFacingTriangle => muda::NativeIcon::RightFacingTriangle, + NativeIcon::Share => muda::NativeIcon::Share, + NativeIcon::Slideshow => muda::NativeIcon::Slideshow, + NativeIcon::SmartBadge => muda::NativeIcon::SmartBadge, + NativeIcon::StatusAvailable => muda::NativeIcon::StatusAvailable, + NativeIcon::StatusNone => muda::NativeIcon::StatusNone, + NativeIcon::StatusPartiallyAvailable => muda::NativeIcon::StatusPartiallyAvailable, + NativeIcon::StatusUnavailable => muda::NativeIcon::StatusUnavailable, + NativeIcon::StopProgressFreestanding => muda::NativeIcon::StopProgressFreestanding, + NativeIcon::StopProgress => muda::NativeIcon::StopProgress, + NativeIcon::TrashEmpty => muda::NativeIcon::TrashEmpty, + NativeIcon::TrashFull => muda::NativeIcon::TrashFull, + NativeIcon::User => muda::NativeIcon::User, + NativeIcon::UserAccounts => muda::NativeIcon::UserAccounts, + NativeIcon::UserGroup => muda::NativeIcon::UserGroup, + NativeIcon::UserGuest => muda::NativeIcon::UserGuest, + } + } +} + +/// An enumeration of all menu item kinds that could be added to +/// a [`Menu`] or [`Submenu`] +pub enum MenuItemKind { + /// Normal menu item + MenuItem(MenuItem), + /// Submenu menu item + Submenu(Submenu), + /// Predefined menu item + Predefined(PredefinedMenuItem), + /// Check menu item + Check(CheckMenuItem), + /// Icon menu item + Icon(IconMenuItem), +} + +impl MenuItemKind { + /// Returns a unique identifier associated with this menu item. + pub fn id(&self) -> &MenuId { + match self { + MenuItemKind::MenuItem(i) => i.id(), + MenuItemKind::Submenu(i) => i.id(), + MenuItemKind::Predefined(i) => i.id(), + MenuItemKind::Check(i) => i.id(), + MenuItemKind::Icon(i) => i.id(), + } + } + + pub(crate) fn inner(&self) -> &dyn IsMenuItem { + match self { + MenuItemKind::MenuItem(i) => i, + MenuItemKind::Submenu(i) => i, + MenuItemKind::Predefined(i) => i, + MenuItemKind::Check(i) => i, + MenuItemKind::Icon(i) => i, + } + } + + pub(crate) fn from_muda(app_handle: AppHandle, i: muda::MenuItemKind) -> Self { + match i { + muda::MenuItemKind::MenuItem(i) => { + Self::MenuItem(MenuItem(Arc::new(MenuItemInner::new(app_handle, i)))) + } + muda::MenuItemKind::Submenu(i) => { + Self::Submenu(Submenu(Arc::new(SubmenuInner::new(app_handle, i)))) + } + muda::MenuItemKind::Predefined(i) => Self::Predefined(PredefinedMenuItem(Arc::new( + PredefinedMenuItemInner::new(app_handle, i), + ))), + muda::MenuItemKind::Check(i) => Self::Check(CheckMenuItem(Arc::new( + CheckMenuItemInner::new(app_handle, i), + ))), + muda::MenuItemKind::Icon(i) => Self::Icon(IconMenuItem(Arc::new(IconMenuItemInner::new( + app_handle, i, + )))), + } + } + + /// Casts this item to a [`MenuItem`], and returns `None` if it wasn't. + pub fn as_menuitem(&self) -> Option<&MenuItem> { + match self { + MenuItemKind::MenuItem(i) => Some(i), + _ => None, + } + } + + /// Casts this item to a [`MenuItem`], and panics if it wasn't. + pub fn as_menuitem_unchecked(&self) -> &MenuItem { + match self { + MenuItemKind::MenuItem(i) => i, + _ => panic!("Not a MenuItem"), + } + } + + /// Casts this item to a [`Submenu`], and returns `None` if it wasn't. + pub fn as_submenu(&self) -> Option<&Submenu> { + match self { + MenuItemKind::Submenu(i) => Some(i), + _ => None, + } + } + + /// Casts this item to a [`Submenu`], and panics if it wasn't. + pub fn as_submenu_unchecked(&self) -> &Submenu { + match self { + MenuItemKind::Submenu(i) => i, + _ => panic!("Not a Submenu"), + } + } + + /// Casts this item to a [`PredefinedMenuItem`], and returns `None` if it wasn't. + pub fn as_predefined_menuitem(&self) -> Option<&PredefinedMenuItem> { + match self { + MenuItemKind::Predefined(i) => Some(i), + _ => None, + } + } + + /// Casts this item to a [`PredefinedMenuItem`], and panics if it wasn't. + pub fn as_predefined_menuitem_unchecked(&self) -> &PredefinedMenuItem { + match self { + MenuItemKind::Predefined(i) => i, + _ => panic!("Not a PredefinedMenuItem"), + } + } + + /// Casts this item to a [`CheckMenuItem`], and returns `None` if it wasn't. + pub fn as_check_menuitem(&self) -> Option<&CheckMenuItem> { + match self { + MenuItemKind::Check(i) => Some(i), + _ => None, + } + } + + /// Casts this item to a [`CheckMenuItem`], and panics if it wasn't. + pub fn as_check_menuitem_unchecked(&self) -> &CheckMenuItem { + match self { + MenuItemKind::Check(i) => i, + _ => panic!("Not a CheckMenuItem"), + } + } + + /// Casts this item to a [`IconMenuItem`], and returns `None` if it wasn't. + pub fn as_icon_menuitem(&self) -> Option<&IconMenuItem> { + match self { + MenuItemKind::Icon(i) => Some(i), + _ => None, + } + } + + /// Casts this item to a [`IconMenuItem`], and panics if it wasn't. + pub fn as_icon_menuitem_unchecked(&self) -> &IconMenuItem { + match self { + MenuItemKind::Icon(i) => i, + _ => panic!("Not an IconMenuItem"), + } + } +} + +impl Clone for MenuItemKind { + fn clone(&self) -> Self { + match self { + Self::MenuItem(i) => Self::MenuItem(i.clone()), + Self::Submenu(i) => Self::Submenu(i.clone()), + Self::Predefined(i) => Self::Predefined(i.clone()), + Self::Check(i) => Self::Check(i.clone()), + Self::Icon(i) => Self::Icon(i.clone()), + } + } +} + +impl sealed::IsMenuItemBase for MenuItemKind { + fn inner_muda(&self) -> &dyn muda::IsMenuItem { + self.inner().inner_muda() + } +} + +impl IsMenuItem for MenuItemKind { + fn kind(&self) -> MenuItemKind { + self.clone() + } + + fn id(&self) -> &MenuId { + self.id() + } +} + +/// A trait that defines a generic item in a menu, which may be one of [`MenuItemKind`] +/// +/// # Safety +/// +/// This trait is ONLY meant to be implemented internally by the crate. +pub trait IsMenuItem: sealed::IsMenuItemBase { + /// Returns the kind of this menu item. + fn kind(&self) -> MenuItemKind; + + /// Returns a unique identifier associated with this menu. + fn id(&self) -> &MenuId; +} + +/// A helper trait with methods to help creating a context menu. +/// +/// # Safety +/// +/// This trait is ONLY meant to be implemented internally by the crate. +pub trait ContextMenu: sealed::ContextMenuBase + Send + Sync { + /// Get the popup [`HMENU`] for this menu. + /// + /// The returned [`HMENU`] is valid as long as the [`ContextMenu`] is. + /// + /// [`HMENU`]: https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types#HMENU + #[cfg(windows)] + #[cfg_attr(docsrs, doc(cfg(windows)))] + fn hpopupmenu(&self) -> crate::Result; + + /// Popup this menu as a context menu on the specified window at the cursor position. + fn popup(&self, window: crate::Window) -> crate::Result<()>; + + /// Popup this menu as a context menu on the specified window at the specified position. + /// + /// The position is relative to the window's top-left corner. + fn popup_at>( + &self, + window: crate::Window, + position: P, + ) -> crate::Result<()>; +} + +pub(crate) mod sealed { + + pub trait IsMenuItemBase { + fn inner_muda(&self) -> &dyn muda::IsMenuItem; + } + + pub trait ContextMenuBase { + fn inner_context(&self) -> &dyn muda::ContextMenu; + fn inner_context_owned(&self) -> Box; + fn popup_inner>( + &self, + window: crate::Window, + position: Option

, + ) -> crate::Result<()>; + } +} + +#[cfg(windows)] +pub(crate) fn map_to_menu_theme(theme: tauri_utils::Theme) -> muda::MenuTheme { + match theme { + tauri_utils::Theme::Light => muda::MenuTheme::Light, + tauri_utils::Theme::Dark => muda::MenuTheme::Dark, + _ => muda::MenuTheme::Auto, + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/normal.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/normal.rs new file mode 100644 index 000000000..c6aa015a4 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/normal.rs @@ -0,0 +1,117 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::sync::Arc; + +use super::run_item_main_thread; +use crate::menu::MenuItemInner; +use crate::run_main_thread; +use crate::{AppHandle, Manager, Runtime, menu::MenuId}; + +use super::MenuItem; + +impl MenuItem { + /// Create a new menu item. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn new( + manager: &M, + text: T, + enabled: bool, + accelerator: Option, + ) -> crate::Result + where + M: Manager, + T: AsRef, + A: AsRef, + { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.as_ref().to_owned(); + let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok()); + + let item = run_main_thread!(handle, || { + let item = muda::MenuItem::new(text, enabled, accelerator); + MenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Create a new menu item with the specified id. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn with_id( + manager: &M, + id: I, + text: T, + enabled: bool, + accelerator: Option, + ) -> crate::Result + where + M: Manager, + I: Into, + T: AsRef, + A: AsRef, + { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let id = id.into(); + let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok()); + let text = text.as_ref().to_owned(); + + let item = run_main_thread!(handle, move || { + let item = muda::MenuItem::with_id(id, text, enabled, accelerator); + MenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// The application handle associated with this type. + pub fn app_handle(&self) -> &AppHandle { + &self.0.app_handle + } + + /// Returns a unique identifier associated with this menu item. + pub fn id(&self) -> &MenuId { + self.0.inner.id() + } + + /// Get the text for this menu item. + pub fn text(&self) -> crate::Result { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().text()) + } + + /// Set the text for this menu item. `text` could optionally contain + /// an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn set_text>(&self, text: S) -> crate::Result<()> { + let text = text.as_ref().to_string(); + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_text(text)) + } + + /// Get whether this menu item is enabled or not. + pub fn is_enabled(&self) -> crate::Result { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().is_enabled()) + } + + /// Enable or disable this menu item. + pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_enabled(enabled)) + } + + /// Set this menu item accelerator. + pub fn set_accelerator>(&self, accelerator: Option) -> crate::Result<()> { + let accel = accelerator.and_then(|s| s.as_ref().parse().ok()); + run_item_main_thread!(self, |self_: Self| { + (*self_.0).as_ref().set_accelerator(accel) + })? + .map_err(Into::into) + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/plugin.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/plugin.rs new file mode 100644 index 000000000..0019ce317 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/plugin.rs @@ -0,0 +1,998 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{collections::HashMap, sync::Mutex}; + +use serde::{Deserialize, Serialize}; +use tauri_runtime::dpi::Position; + +use super::{sealed::ContextMenuBase, *}; +use crate::{ + Manager, ResourceTable, RunEvent, Runtime, State, Webview, Window, command, + image::JsImage, + ipc::{Channel, channel::JavaScriptChannelId}, + plugin::{Builder, TauriPlugin}, + resources::ResourceId, + sealed::ManagerBase, +}; +use tauri_macros::do_menu_item; + +#[derive(Deserialize, Serialize)] +pub(crate) enum ItemKind { + Menu, + MenuItem, + Predefined, + Submenu, + Check, + Icon, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AboutMetadata { + pub name: Option, + pub version: Option, + pub short_version: Option, + pub authors: Option>, + pub comments: Option, + pub copyright: Option, + pub license: Option, + pub website: Option, + pub website_label: Option, + pub credits: Option, + pub icon: Option, +} + +impl AboutMetadata { + pub fn into_metadata( + self, + resources_table: &ResourceTable, + ) -> crate::Result> { + let icon = match self.icon { + Some(i) => Some(i.into_img(resources_table)?.as_ref().clone()), + None => None, + }; + + Ok(super::AboutMetadata { + name: self.name, + version: self.version, + short_version: self.short_version, + authors: self.authors, + comments: self.comments, + copyright: self.copyright, + license: self.license, + website: self.website, + website_label: self.website_label, + credits: self.credits, + icon, + }) + } +} + +#[allow(clippy::large_enum_variant)] +#[derive(Deserialize)] +enum Predefined { + Separator, + Copy, + Cut, + Paste, + SelectAll, + Undo, + Redo, + Minimize, + Maximize, + Fullscreen, + Hide, + HideOthers, + ShowAll, + CloseWindow, + Quit, + About(Option), + Services, + BringAllToFront, +} + +#[derive(Deserialize)] +struct SubmenuPayload { + id: Option, + text: String, + enabled: Option, + items: Vec, + icon: Option, +} + +impl SubmenuPayload { + pub fn create_item( + self, + webview: &Webview, + resources_table: &ResourceTable, + ) -> crate::Result> { + let mut builder = if let Some(id) = self.id { + SubmenuBuilder::with_id(webview, id, self.text) + } else { + SubmenuBuilder::new(webview, self.text) + }; + if let Some(enabled) = self.enabled { + builder = builder.enabled(enabled); + } + if let Some(icon) = self.icon { + builder = match icon { + Icon::Native(native_icon) => builder.submenu_native_icon(native_icon), + Icon::Icon(js_icon) => { + builder.submenu_icon(js_icon.into_img(resources_table)?.as_ref().clone()) + } + }; + } + for item in self.items { + builder = item.with_item(webview, resources_table, |i| Ok(builder.item(i)))?; + } + + builder.build() + } +} + +#[derive(Deserialize)] +struct CheckMenuItemPayload { + handler: Option, + id: Option, + text: String, + checked: bool, + enabled: Option, + accelerator: Option, +} + +impl CheckMenuItemPayload { + pub fn create_item(self, webview: &Webview) -> crate::Result> { + let mut builder = if let Some(id) = self.id { + CheckMenuItemBuilder::with_id(id, self.text) + } else { + CheckMenuItemBuilder::new(self.text) + }; + if let Some(accelerator) = self.accelerator { + builder = builder.accelerator(accelerator); + } + if let Some(enabled) = self.enabled { + builder = builder.enabled(enabled); + } + + let item = builder.checked(self.checked).build(webview)?; + + if let Some(handler) = self.handler { + let handler = handler.channel_on(webview.clone()); + webview + .state::() + .0 + .lock() + .unwrap() + .insert(item.id().clone(), handler); + } + + Ok(item) + } +} + +#[derive(Deserialize)] +#[serde(untagged)] +enum Icon { + Native(NativeIcon), + Icon(JsImage), +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct IconMenuItemPayload { + handler: Option, + id: Option, + text: String, + icon: Icon, + enabled: Option, + accelerator: Option, +} + +impl IconMenuItemPayload { + pub fn create_item( + self, + webview: &Webview, + resources_table: &ResourceTable, + ) -> crate::Result> { + let mut builder = if let Some(id) = self.id { + IconMenuItemBuilder::with_id(id, self.text) + } else { + IconMenuItemBuilder::new(self.text) + }; + if let Some(accelerator) = self.accelerator { + builder = builder.accelerator(accelerator); + } + if let Some(enabled) = self.enabled { + builder = builder.enabled(enabled); + } + builder = match self.icon { + Icon::Native(native_icon) => builder.native_icon(native_icon), + Icon::Icon(icon) => builder.icon(icon.into_img(resources_table)?.as_ref().clone()), + }; + + let item = builder.build(webview)?; + + if let Some(handler) = self.handler { + let handler = handler.channel_on(webview.clone()); + webview + .state::() + .0 + .lock() + .unwrap() + .insert(item.id().clone(), handler); + } + + Ok(item) + } +} + +#[derive(Deserialize)] +struct MenuItemPayload { + handler: Option, + id: Option, + text: String, + enabled: Option, + accelerator: Option, +} + +impl MenuItemPayload { + pub fn create_item(self, webview: &Webview) -> crate::Result> { + let mut builder = if let Some(id) = self.id { + MenuItemBuilder::with_id(id, self.text) + } else { + MenuItemBuilder::new(self.text) + }; + if let Some(accelerator) = self.accelerator { + builder = builder.accelerator(accelerator); + } + if let Some(enabled) = self.enabled { + builder = builder.enabled(enabled); + } + + let item = builder.build(webview)?; + + if let Some(handler) = self.handler { + let handler = handler.channel_on(webview.clone()); + webview + .state::() + .0 + .lock() + .unwrap() + .insert(item.id().clone(), handler); + } + + Ok(item) + } +} + +#[derive(Deserialize)] +struct PredefinedMenuItemPayload { + item: Predefined, + text: Option, +} + +impl PredefinedMenuItemPayload { + pub fn create_item( + self, + webview: &Webview, + resources_table: &ResourceTable, + ) -> crate::Result> { + match self.item { + Predefined::Separator => PredefinedMenuItem::separator(webview), + Predefined::Copy => PredefinedMenuItem::copy(webview, self.text.as_deref()), + Predefined::Cut => PredefinedMenuItem::cut(webview, self.text.as_deref()), + Predefined::Paste => PredefinedMenuItem::paste(webview, self.text.as_deref()), + Predefined::SelectAll => PredefinedMenuItem::select_all(webview, self.text.as_deref()), + Predefined::Undo => PredefinedMenuItem::undo(webview, self.text.as_deref()), + Predefined::Redo => PredefinedMenuItem::redo(webview, self.text.as_deref()), + Predefined::Minimize => PredefinedMenuItem::minimize(webview, self.text.as_deref()), + Predefined::Maximize => PredefinedMenuItem::maximize(webview, self.text.as_deref()), + Predefined::Fullscreen => PredefinedMenuItem::fullscreen(webview, self.text.as_deref()), + Predefined::Hide => PredefinedMenuItem::hide(webview, self.text.as_deref()), + Predefined::HideOthers => PredefinedMenuItem::hide_others(webview, self.text.as_deref()), + Predefined::ShowAll => PredefinedMenuItem::show_all(webview, self.text.as_deref()), + Predefined::CloseWindow => PredefinedMenuItem::close_window(webview, self.text.as_deref()), + Predefined::Quit => PredefinedMenuItem::quit(webview, self.text.as_deref()), + Predefined::About(metadata) => { + let metadata = match metadata { + Some(m) => Some(m.into_metadata(resources_table)?), + None => None, + }; + PredefinedMenuItem::about(webview, self.text.as_deref(), metadata) + } + Predefined::Services => PredefinedMenuItem::services(webview, self.text.as_deref()), + Predefined::BringAllToFront => { + PredefinedMenuItem::bring_all_to_front(webview, self.text.as_deref()) + } + } + } +} + +#[derive(Deserialize)] +#[serde(untagged)] +// Note, order matters for untagged enum deserialization +enum MenuItemPayloadKind { + ExistingItem((ResourceId, ItemKind)), + Predefined(PredefinedMenuItemPayload), + Check(CheckMenuItemPayload), + Icon(IconMenuItemPayload), + Submenu(SubmenuPayload), + MenuItem(MenuItemPayload), +} + +impl MenuItemPayloadKind { + pub fn with_item) -> crate::Result>( + self, + webview: &Webview, + resources_table: &ResourceTable, + f: F, + ) -> crate::Result { + match self { + Self::ExistingItem((rid, kind)) => { + do_menu_item!(resources_table, rid, kind, |i| f(&*i)) + } + Self::Submenu(i) => f(&i.create_item(webview, resources_table)?), + Self::Predefined(i) => f(&i.create_item(webview, resources_table)?), + Self::Check(i) => f(&i.create_item(webview)?), + Self::Icon(i) => f(&i.create_item(webview, resources_table)?), + Self::MenuItem(i) => f(&i.create_item(webview)?), + } + } +} + +#[derive(Deserialize, Default)] +#[serde(rename_all = "camelCase")] +struct NewOptions { + id: Option, + text: Option, + enabled: Option, + checked: Option, + accelerator: Option, + #[serde(rename = "item")] + predefined_item: Option, + icon: Option, + items: Option>, +} + +#[command(root = "crate")] +fn new( + webview: Webview, + kind: ItemKind, + options: Option, + channels: State<'_, MenuChannels>, + handler: Channel, +) -> crate::Result<(ResourceId, MenuId)> { + let options = options.unwrap_or_default(); + let mut resources_table = webview.resources_table(); + + let (rid, id) = match kind { + ItemKind::Menu => { + let mut builder = MenuBuilder::new(&webview); + if let Some(id) = options.id { + builder = builder.id(id); + } + if let Some(items) = options.items { + for item in items { + builder = item.with_item(&webview, &resources_table, |i| Ok(builder.item(i)))?; + } + } + let menu = builder.build()?; + let id = menu.id().clone(); + let rid = resources_table.add(menu); + + (rid, id) + } + + ItemKind::Submenu => { + let submenu = SubmenuPayload { + id: options.id, + text: options.text.unwrap_or_default(), + enabled: options.enabled, + items: options.items.unwrap_or_default(), + icon: options.icon, + } + .create_item(&webview, &resources_table)?; + let id = submenu.id().clone(); + let rid = resources_table.add(submenu); + + (rid, id) + } + + ItemKind::MenuItem => { + let item = MenuItemPayload { + // handler managed in this function instead + handler: None, + id: options.id, + text: options.text.unwrap_or_default(), + enabled: options.enabled, + accelerator: options.accelerator, + } + .create_item(&webview)?; + let id = item.id().clone(); + let rid = resources_table.add(item); + (rid, id) + } + + ItemKind::Predefined => { + let item = PredefinedMenuItemPayload { + item: options.predefined_item.ok_or_else(|| { + anyhow::anyhow!("the `item` option is required for a `Predefined` menu item") + })?, + text: options.text, + } + .create_item(&webview, &resources_table)?; + let id = item.id().clone(); + let rid = resources_table.add(item); + (rid, id) + } + + ItemKind::Check => { + let item = CheckMenuItemPayload { + // handler managed in this function instead + handler: None, + id: options.id, + text: options.text.unwrap_or_default(), + checked: options.checked.unwrap_or_default(), + enabled: options.enabled, + accelerator: options.accelerator, + } + .create_item(&webview)?; + let id = item.id().clone(); + let rid = resources_table.add(item); + (rid, id) + } + + ItemKind::Icon => { + let item = IconMenuItemPayload { + // handler managed in this function instead + handler: None, + id: options.id, + text: options.text.unwrap_or_default(), + icon: options.icon.unwrap_or(Icon::Native(NativeIcon::User)), + enabled: options.enabled, + accelerator: options.accelerator, + } + .create_item(&webview, &resources_table)?; + let id = item.id().clone(); + let rid = resources_table.add(item); + (rid, id) + } + }; + + channels.0.lock().unwrap().insert(id.clone(), handler); + + Ok((rid, id)) +} + +#[command(root = "crate")] +fn append( + webview: Webview, + rid: ResourceId, + kind: ItemKind, + items: Vec, +) -> crate::Result<()> { + let resources_table = webview.resources_table(); + match kind { + ItemKind::Menu => { + let menu = resources_table.get::>(rid)?; + for item in items { + item.with_item(&webview, &resources_table, |i| menu.append(i))?; + } + } + ItemKind::Submenu => { + let submenu = resources_table.get::>(rid)?; + for item in items { + item.with_item(&webview, &resources_table, |i| submenu.append(i))?; + } + } + _ => return Err(crate::Error::UnexpectedMenuKind), + }; + + Ok(()) +} + +#[command(root = "crate")] +fn prepend( + webview: Webview, + rid: ResourceId, + kind: ItemKind, + items: Vec, +) -> crate::Result<()> { + let resources_table = webview.resources_table(); + match kind { + ItemKind::Menu => { + let menu = resources_table.get::>(rid)?; + for item in items { + item.with_item(&webview, &resources_table, |i| menu.prepend(i))?; + } + } + ItemKind::Submenu => { + let submenu = resources_table.get::>(rid)?; + for item in items { + item.with_item(&webview, &resources_table, |i| submenu.prepend(i))?; + } + } + _ => return Err(crate::Error::UnexpectedMenuKind), + }; + + Ok(()) +} + +#[command(root = "crate")] +fn insert( + webview: Webview, + rid: ResourceId, + kind: ItemKind, + items: Vec, + mut position: usize, +) -> crate::Result<()> { + let resources_table = webview.resources_table(); + match kind { + ItemKind::Menu => { + let menu = resources_table.get::>(rid)?; + for item in items { + item.with_item(&webview, &resources_table, |i| menu.insert(i, position))?; + position += 1 + } + } + ItemKind::Submenu => { + let submenu = resources_table.get::>(rid)?; + for item in items { + item.with_item(&webview, &resources_table, |i| submenu.insert(i, position))?; + position += 1 + } + } + _ => return Err(crate::Error::UnexpectedMenuKind), + }; + + Ok(()) +} + +#[command(root = "crate")] +fn remove( + webview: Webview, + rid: ResourceId, + kind: ItemKind, + item: (ResourceId, ItemKind), +) -> crate::Result<()> { + let resources_table = webview.resources_table(); + let (item_rid, item_kind) = item; + match kind { + ItemKind::Menu => { + let menu = resources_table.get::>(rid)?; + do_menu_item!(resources_table, item_rid, item_kind, |i| menu.remove(&*i))?; + } + ItemKind::Submenu => { + let submenu = resources_table.get::>(rid)?; + do_menu_item!(resources_table, item_rid, item_kind, |i| submenu + .remove(&*i))?; + } + _ => return Err(crate::Error::UnexpectedMenuKind), + }; + + Ok(()) +} + +macro_rules! make_item_resource { + ($resources_table:ident, $item:ident) => {{ + let id = $item.id().clone(); + let (rid, kind) = match $item { + MenuItemKind::MenuItem(i) => ($resources_table.add(i), ItemKind::MenuItem), + MenuItemKind::Submenu(i) => ($resources_table.add(i), ItemKind::Submenu), + MenuItemKind::Predefined(i) => ($resources_table.add(i), ItemKind::Predefined), + MenuItemKind::Check(i) => ($resources_table.add(i), ItemKind::Check), + MenuItemKind::Icon(i) => ($resources_table.add(i), ItemKind::Icon), + }; + (rid, id, kind) + }}; +} + +#[command(root = "crate")] +fn remove_at( + webview: Webview, + rid: ResourceId, + kind: ItemKind, + position: usize, +) -> crate::Result> { + let mut resources_table = webview.resources_table(); + match kind { + ItemKind::Menu => { + let menu = resources_table.get::>(rid)?; + if let Some(item) = menu.remove_at(position)? { + return Ok(Some(make_item_resource!(resources_table, item))); + } + } + ItemKind::Submenu => { + let submenu = resources_table.get::>(rid)?; + if let Some(item) = submenu.remove_at(position)? { + return Ok(Some(make_item_resource!(resources_table, item))); + } + } + _ => return Err(crate::Error::UnexpectedMenuKind), + }; + + Ok(None) +} + +#[command(root = "crate")] +fn items( + webview: Webview, + rid: ResourceId, + kind: ItemKind, +) -> crate::Result> { + let mut resources_table = webview.resources_table(); + let items = match kind { + ItemKind::Menu => resources_table.get::>(rid)?.items()?, + ItemKind::Submenu => resources_table.get::>(rid)?.items()?, + _ => return Err(crate::Error::UnexpectedMenuKind), + }; + + Ok( + items + .into_iter() + .map(|i| make_item_resource!(resources_table, i)) + .collect::>(), + ) +} + +#[command(root = "crate")] +fn get( + webview: Webview, + rid: ResourceId, + kind: ItemKind, + id: MenuId, +) -> crate::Result> { + let mut resources_table = webview.resources_table(); + match kind { + ItemKind::Menu => { + let menu = resources_table.get::>(rid)?; + if let Some(item) = menu.get(&id) { + return Ok(Some(make_item_resource!(resources_table, item))); + } + } + ItemKind::Submenu => { + let submenu = resources_table.get::>(rid)?; + if let Some(item) = submenu.get(&id) { + return Ok(Some(make_item_resource!(resources_table, item))); + } + } + _ => return Err(crate::Error::UnexpectedMenuKind), + }; + + Ok(None) +} + +#[command(root = "crate")] +async fn popup( + webview: Webview, + current_window: Window, + rid: ResourceId, + kind: ItemKind, + window: Option, + at: Option, +) -> crate::Result<()> { + let window = window + .map(|w| webview.manager().get_window(&w)) + .unwrap_or(Some(current_window)); + + if let Some(window) = window { + let resources_table = webview.resources_table(); + match kind { + ItemKind::Menu => { + let menu = resources_table.get::>(rid)?; + menu.popup_inner(window, at)?; + } + ItemKind::Submenu => { + let submenu = resources_table.get::>(rid)?; + submenu.popup_inner(window, at)?; + } + _ => return Err(crate::Error::UnexpectedMenuKind), + }; + } + + Ok(()) +} + +#[command(root = "crate")] +fn create_default( + app: AppHandle, + webview: Webview, +) -> crate::Result<(ResourceId, MenuId)> { + let mut resources_table = webview.resources_table(); + let menu = Menu::default(&app)?; + let id = menu.id().clone(); + let rid = resources_table.add(menu); + Ok((rid, id)) +} + +#[command(root = "crate")] +async fn set_as_app_menu( + webview: Webview, + rid: ResourceId, +) -> crate::Result> { + let mut resources_table = webview.resources_table(); + let menu = resources_table.get::>(rid)?; + if let Some(menu) = menu.set_as_app_menu()? { + let id = menu.id().clone(); + let rid = resources_table.add(menu); + return Ok(Some((rid, id))); + } + Ok(None) +} + +#[command(root = "crate")] +async fn set_as_window_menu( + webview: Webview, + current_window: Window, + rid: ResourceId, + window: Option, +) -> crate::Result> { + let window = window + .map(|w| webview.manager().get_window(&w)) + .unwrap_or(Some(current_window)); + + if let Some(window) = window { + let mut resources_table = webview.resources_table(); + let menu = resources_table.get::>(rid)?; + if let Some(menu) = menu.set_as_window_menu(&window)? { + let id = menu.id().clone(); + let rid = resources_table.add(menu); + return Ok(Some((rid, id))); + } + } + Ok(None) +} + +#[command(root = "crate")] +fn text(webview: Webview, rid: ResourceId, kind: ItemKind) -> crate::Result { + let resources_table = webview.resources_table(); + do_menu_item!(resources_table, rid, kind, |i| i.text()) +} + +#[command(root = "crate")] +fn set_text( + webview: Webview, + rid: ResourceId, + kind: ItemKind, + text: String, +) -> crate::Result<()> { + let resources_table = webview.resources_table(); + do_menu_item!(resources_table, rid, kind, |i| i.set_text(text)) +} + +#[command(root = "crate")] +fn is_enabled( + webview: Webview, + rid: ResourceId, + kind: ItemKind, +) -> crate::Result { + let resources_table = webview.resources_table(); + do_menu_item!(resources_table, rid, kind, |i| i.is_enabled(), !Predefined) +} + +#[command(root = "crate")] +fn set_enabled( + webview: Webview, + rid: ResourceId, + kind: ItemKind, + enabled: bool, +) -> crate::Result<()> { + let resources_table = webview.resources_table(); + do_menu_item!( + resources_table, + rid, + kind, + |i| i.set_enabled(enabled), + !Predefined + ) +} + +#[command(root = "crate")] +fn set_accelerator( + webview: Webview, + rid: ResourceId, + kind: ItemKind, + accelerator: Option, +) -> crate::Result<()> { + let resources_table = webview.resources_table(); + do_menu_item!( + resources_table, + rid, + kind, + |i| i.set_accelerator(accelerator), + !Predefined | !Submenu + ) +} + +#[command(root = "crate")] +fn set_as_windows_menu_for_nsapp( + webview: Webview, + rid: ResourceId, +) -> crate::Result<()> { + #[cfg(target_os = "macos")] + { + let resources_table = webview.resources_table(); + let submenu = resources_table.get::>(rid)?; + submenu.set_as_windows_menu_for_nsapp()?; + } + + let _ = rid; + let _ = webview; + Ok(()) +} + +#[command(root = "crate")] +fn set_as_help_menu_for_nsapp( + webview: Webview, + rid: ResourceId, +) -> crate::Result<()> { + #[cfg(target_os = "macos")] + { + let resources_table = webview.resources_table(); + let submenu = resources_table.get::>(rid)?; + submenu.set_as_help_menu_for_nsapp()?; + } + + let _ = rid; + let _ = webview; + + Ok(()) +} + +#[command(root = "crate")] +fn is_checked(webview: Webview, rid: ResourceId) -> crate::Result { + let resources_table = webview.resources_table(); + let check_item = resources_table.get::>(rid)?; + check_item.is_checked() +} + +#[command(root = "crate")] +fn set_checked( + webview: Webview, + rid: ResourceId, + checked: bool, +) -> crate::Result<()> { + let resources_table = webview.resources_table(); + let check_item = resources_table.get::>(rid)?; + check_item.set_checked(checked) +} + +#[command(root = "crate")] +fn set_icon( + webview: Webview, + rid: ResourceId, + kind: ItemKind, + icon: Option, +) -> crate::Result<()> { + let resources_table = webview.resources_table(); + do_menu_item!( + resources_table, + rid, + kind, + |icon_item| match icon { + Some(Icon::Native(icon)) => icon_item.set_native_icon(Some(icon)), + Some(Icon::Icon(icon)) => { + icon_item.set_icon(Some(icon.into_img(&resources_table)?.as_ref().clone())) + } + None => { + icon_item.set_icon(None)?; + icon_item.set_native_icon(None)?; + Ok(()) + } + }, + Icon | Submenu + ) +} + +struct MenuChannels(Mutex>>); + +// Called in `Menu`'s `Drop` to clean up the event handlers +pub(crate) fn remove_menu_channel(app: &AppHandle, id: &MenuId) { + app.state::().0.lock().unwrap().remove(id); +} + +pub(crate) fn init() -> TauriPlugin { + Builder::new("menu") + .setup(|app, _api| { + app.manage(MenuChannels(Mutex::default())); + Ok(()) + }) + .on_event(|app, e| { + if let RunEvent::MenuEvent(e) = e { + // Cloning the channel out in case the menu gets dropped during the channel send through `channel_interceptor` + let channel = app + .state::() + .0 + .lock() + .unwrap() + .get(&e.id) + .cloned(); + if let Some(channel) = channel { + let _ = channel.send(e.id.clone()); + } + } + }) + .invoke_handler(crate::generate_handler![ + #![plugin(menu)] + new, + append, + prepend, + insert, + remove, + remove_at, + items, + get, + popup, + create_default, + set_as_app_menu, + set_as_window_menu, + text, + set_text, + is_enabled, + set_enabled, + set_accelerator, + set_as_windows_menu_for_nsapp, + set_as_help_menu_for_nsapp, + is_checked, + set_checked, + set_icon, + ]) + .build() +} + +#[cfg(test)] +mod tests { + use super::{ItemKind, NewOptions}; + use crate::{Manager, ipc::Channel, test::mock_app}; + + fn new_item( + webview: &crate::WebviewWindow, + kind: ItemKind, + options: serde_json::Value, + ) -> crate::Result<()> { + let options: NewOptions = serde_json::from_value(options).unwrap(); + super::new( + webview.as_ref().clone(), + kind, + Some(options), + webview.state(), + Channel::new(|_| Ok(())), + ) + .map(|_| ()) + } + + #[test] + fn new_rejects_invalid_input_without_panicking() { + let app = mock_app(); + let webview = crate::WebviewWindowBuilder::new(&app, "main", Default::default()) + .build() + .unwrap(); + + // predefined item without the `item` option + let err = new_item(&webview, ItemKind::Predefined, serde_json::json!({})) + .unwrap_err() + .to_string(); + assert!(err.contains("`item` option"), "unexpected error: {err}"); + + // submenu icon whose buffer does not match its dimensions + let res = new_item( + &webview, + ItemKind::Submenu, + serde_json::json!({ "icon": { "rgba": [1, 2, 3], "width": 100, "height": 100 } }), + ); + assert!(res.is_err(), "invalid submenu icon should return an error"); + + // icon menu item with the same invalid icon + let res = new_item( + &webview, + ItemKind::Icon, + serde_json::json!({ "icon": { "rgba": [1, 2, 3], "width": 100, "height": 100 } }), + ); + assert!( + res.is_err(), + "invalid menu item icon should return an error" + ); + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/predefined.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/predefined.rs new file mode 100644 index 000000000..059183b3c --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/predefined.rs @@ -0,0 +1,360 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::sync::Arc; + +use super::run_item_main_thread; +use super::{AboutMetadata, PredefinedMenuItem}; +use crate::menu::PredefinedMenuItemInner; +use crate::run_main_thread; +use crate::{AppHandle, Manager, Runtime, menu::MenuId}; + +impl PredefinedMenuItem { + /// Separator menu item + pub fn separator>(manager: &M) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let item = run_main_thread!(handle, || { + let item = muda::PredefinedMenuItem::separator(); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Copy menu item + pub fn copy>(manager: &M, text: Option<&str>) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.map(|t| t.to_owned()); + + let item = run_main_thread!(handle, || { + let item = muda::PredefinedMenuItem::copy(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Cut menu item + pub fn cut>(manager: &M, text: Option<&str>) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.map(|t| t.to_owned()); + + let item = run_main_thread!(handle, || { + let item = muda::PredefinedMenuItem::cut(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Paste menu item + pub fn paste>(manager: &M, text: Option<&str>) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.map(|t| t.to_owned()); + + let item = run_main_thread!(handle, || { + let item = muda::PredefinedMenuItem::paste(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// SelectAll menu item + pub fn select_all>(manager: &M, text: Option<&str>) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.map(|t| t.to_owned()); + + let item = run_main_thread!(handle, || { + let item = muda::PredefinedMenuItem::select_all(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Undo menu item + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn undo>(manager: &M, text: Option<&str>) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.map(|t| t.to_owned()); + + let item = run_main_thread!(handle, || { + let item = muda::PredefinedMenuItem::undo(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + /// Redo menu item + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn redo>(manager: &M, text: Option<&str>) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.map(|t| t.to_owned()); + + let item = run_main_thread!(handle, || { + let item = muda::PredefinedMenuItem::redo(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Minimize window menu item + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn minimize>(manager: &M, text: Option<&str>) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.map(|t| t.to_owned()); + + let item = run_main_thread!(handle, || { + let item = muda::PredefinedMenuItem::minimize(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Maximize window menu item + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn maximize>(manager: &M, text: Option<&str>) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.map(|t| t.to_owned()); + + let item = run_main_thread!(handle, || { + let item = muda::PredefinedMenuItem::maximize(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Fullscreen menu item + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn fullscreen>(manager: &M, text: Option<&str>) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.map(|t| t.to_owned()); + + let item = run_main_thread!(handle, || { + let item = muda::PredefinedMenuItem::fullscreen(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Hide window menu item + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn hide>(manager: &M, text: Option<&str>) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.map(|t| t.to_owned()); + + let item = run_main_thread!(handle, || { + let item = muda::PredefinedMenuItem::hide(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Hide other windows menu item + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn hide_others>(manager: &M, text: Option<&str>) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.map(|t| t.to_owned()); + + let item = run_main_thread!(handle, || { + let item = muda::PredefinedMenuItem::hide_others(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Show all app windows menu item + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn show_all>(manager: &M, text: Option<&str>) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.map(|t| t.to_owned()); + + let item = run_main_thread!(handle, || { + let item = muda::PredefinedMenuItem::show_all(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Close window menu item + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn close_window>(manager: &M, text: Option<&str>) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.map(|t| t.to_owned()); + + let item = run_main_thread!(handle, || { + let item = muda::PredefinedMenuItem::close_window(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Quit app menu item + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn quit>(manager: &M, text: Option<&str>) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.map(|t| t.to_owned()); + + let item = run_main_thread!(handle, || { + let item = muda::PredefinedMenuItem::quit(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// About app menu item + pub fn about>( + manager: &M, + text: Option<&str>, + metadata: Option>, + ) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.map(|t| t.to_owned()); + + let metadata = match metadata { + Some(m) => Some(m.try_into()?), + None => None, + }; + + let item = run_main_thread!(handle, || { + let item = muda::PredefinedMenuItem::about(text.as_deref(), metadata); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Services menu item + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn services>(manager: &M, text: Option<&str>) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.map(|t| t.to_owned()); + + let item = run_main_thread!(handle, || { + let item = muda::PredefinedMenuItem::services(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Bring All to Front menu item + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn bring_all_to_front>(manager: &M, text: Option<&str>) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.map(|t| t.to_owned()); + + let item = run_main_thread!(handle, || { + let item = muda::PredefinedMenuItem::bring_all_to_front(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Returns a unique identifier associated with this menu item. + pub fn id(&self) -> &MenuId { + self.0.inner.id() + } + + /// Get the text for this menu item. + pub fn text(&self) -> crate::Result { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().text()) + } + + /// Set the text for this menu item. `text` could optionally contain + /// an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn set_text>(&self, text: S) -> crate::Result<()> { + let text = text.as_ref().to_string(); + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_text(text)) + } + + /// The application handle associated with this type. + pub fn app_handle(&self) -> &AppHandle { + &self.0.app_handle + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/submenu.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/submenu.rs new file mode 100644 index 000000000..c7b7bf8cb --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/menu/submenu.rs @@ -0,0 +1,425 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::sync::Arc; + +use super::Submenu; +use super::run_item_main_thread; +use super::{IsMenuItem, MenuItemKind, sealed::ContextMenuBase}; +use crate::menu::NativeIcon; +use crate::menu::SubmenuInner; +use crate::run_main_thread; +use crate::{AppHandle, Manager, Position, Runtime, Window}; +use muda::{ContextMenu, Icon as MudaIcon, MenuId}; + +impl super::ContextMenu for Submenu { + #[cfg(target_os = "windows")] + fn hpopupmenu(&self) -> crate::Result { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().hpopupmenu()) + } + + fn popup(&self, window: Window) -> crate::Result<()> { + self.popup_inner(window, None::) + } + + fn popup_at>( + &self, + window: Window, + position: P, + ) -> crate::Result<()> { + self.popup_inner(window, Some(position)) + } +} + +impl ContextMenuBase for Submenu { + fn popup_inner>( + &self, + window: crate::Window, + position: Option

, + ) -> crate::Result<()> { + let position = position.map(Into::into); + run_item_main_thread!(self, move |self_: Self| { + #[cfg(target_os = "macos")] + if let Ok(view) = window.ns_view() { + unsafe { + self_ + .inner() + .show_context_menu_for_nsview(view as _, position); + } + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + if let Ok(w) = window.gtk_window() { + self_ + .inner() + .show_context_menu_for_gtk_window(w.as_ref(), position); + } + + #[cfg(windows)] + if let Ok(hwnd) = window.hwnd() { + unsafe { + self_ + .inner() + .show_context_menu_for_hwnd(hwnd.0 as _, position); + } + } + }) + } + + fn inner_context(&self) -> &dyn muda::ContextMenu { + (*self.0).as_ref() + } + + fn inner_context_owned(&self) -> Box { + Box::new((*self.0).as_ref().clone()) + } +} + +impl Submenu { + /// Creates a new submenu. + pub fn new, S: AsRef>( + manager: &M, + text: S, + enabled: bool, + ) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.as_ref().to_owned(); + + let submenu = run_main_thread!(handle, || { + let submenu = muda::Submenu::new(text, enabled); + SubmenuInner::new(app_handle, submenu) + })?; + + Ok(Self(Arc::new(submenu))) + } + + /// Create a new submenu with an icon. + pub fn new_with_icon, S: AsRef>( + manager: &M, + text: S, + enabled: bool, + icon: Option>, + ) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + let text = text.as_ref().to_owned(); + let icon: Option = match icon { + Some(i) => Some(i.try_into()?), + None => None, + }; + let submenu = run_main_thread!(handle, move || { + let submenu = muda::Submenu::new(text, enabled); + if icon.is_some() { + submenu.set_icon(icon); + } + SubmenuInner::new(app_handle, submenu) + })?; + Ok(Self(Arc::new(submenu))) + } + + /// Create a new submenu with a native icon. + pub fn new_with_native_icon, S: AsRef>( + manager: &M, + text: S, + enabled: bool, + icon: Option, + ) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + let text = text.as_ref().to_owned(); + let submenu = run_main_thread!(handle, || { + let submenu = muda::Submenu::new(text, enabled); + if let Some(icon) = icon { + submenu.set_native_icon(Some(icon.into())); + } + SubmenuInner::new(app_handle, submenu) + })?; + Ok(Self(Arc::new(submenu))) + } + + /// Creates a new submenu with the specified id. + pub fn with_id, I: Into, S: AsRef>( + manager: &M, + id: I, + text: S, + enabled: bool, + ) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let id = id.into(); + let text = text.as_ref().to_owned(); + + let submenu = run_main_thread!(handle, move || { + let submenu = muda::Submenu::with_id(id, text, enabled); + SubmenuInner::new(app_handle, submenu) + })?; + + Ok(Self(Arc::new(submenu))) + } + + /// Create a new submenu with an id and an icon. + pub fn with_id_and_icon, I: Into, S: AsRef>( + manager: &M, + id: I, + text: S, + enabled: bool, + icon: Option>, + ) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + let id = id.into(); + let text = text.as_ref().to_owned(); + let icon: Option = match icon { + Some(i) => Some(i.try_into()?), + None => None, + }; + let submenu = run_main_thread!(handle, move || { + let submenu = muda::Submenu::with_id(id, text, enabled); + if icon.is_some() { + submenu.set_icon(icon); + } + SubmenuInner::new(app_handle, submenu) + })?; + Ok(Self(Arc::new(submenu))) + } + + /// Create a new submenu with an id and a native icon. + pub fn with_id_and_native_icon, I: Into, S: AsRef>( + manager: &M, + id: I, + text: S, + enabled: bool, + icon: Option, + ) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + let id = id.into(); + let text = text.as_ref().to_owned(); + let submenu = run_main_thread!(handle, move || { + let submenu = muda::Submenu::with_id(id, text, enabled); + if let Some(icon) = icon { + submenu.set_native_icon(Some(icon.into())); + } + SubmenuInner::new(app_handle, submenu) + })?; + Ok(Self(Arc::new(submenu))) + } + + /// Creates a new menu with given `items`. It calls [`Submenu::new`] and [`Submenu::append_items`] internally. + pub fn with_items, S: AsRef>( + manager: &M, + text: S, + enabled: bool, + items: &[&dyn IsMenuItem], + ) -> crate::Result { + let menu = Self::new(manager, text, enabled)?; + menu.append_items(items)?; + Ok(menu) + } + + /// Creates a new menu with the specified id and given `items`. + /// It calls [`Submenu::new`] and [`Submenu::append_items`] internally. + pub fn with_id_and_items, I: Into, S: AsRef>( + manager: &M, + id: I, + text: S, + enabled: bool, + items: &[&dyn IsMenuItem], + ) -> crate::Result { + let menu = Self::with_id(manager, id, text, enabled)?; + menu.append_items(items)?; + Ok(menu) + } + + pub(crate) fn inner(&self) -> &muda::Submenu { + (*self.0).as_ref() + } + + /// The application handle associated with this type. + pub fn app_handle(&self) -> &AppHandle { + &self.0.app_handle + } + + /// Returns a unique identifier associated with this submenu. + pub fn id(&self) -> &MenuId { + self.0.inner.id() + } + + /// Add a menu item to the end of this submenu. + pub fn append(&self, item: &dyn IsMenuItem) -> crate::Result<()> { + let kind = item.kind(); + run_item_main_thread!(self, |self_: Self| { + (*self_.0).as_ref().append(kind.inner().inner_muda()) + })? + .map_err(Into::into) + } + + /// Add menu items to the end of this submenu. It calls [`Submenu::append`] in a loop internally. + pub fn append_items(&self, items: &[&dyn IsMenuItem]) -> crate::Result<()> { + for item in items { + self.append(*item)? + } + + Ok(()) + } + + /// Add a menu item to the beginning of this submenu. + pub fn prepend(&self, item: &dyn IsMenuItem) -> crate::Result<()> { + let kind = item.kind(); + run_item_main_thread!(self, |self_: Self| { + (*self_.0).as_ref().prepend(kind.inner().inner_muda()) + })? + .map_err(Into::into) + } + + /// Add menu items to the beginning of this submenu. It calls [`Submenu::insert_items`] with position of `0` internally. + pub fn prepend_items(&self, items: &[&dyn IsMenuItem]) -> crate::Result<()> { + self.insert_items(items, 0) + } + + /// Insert a menu item at the specified `position` in this submenu. + pub fn insert(&self, item: &dyn IsMenuItem, position: usize) -> crate::Result<()> { + let kind = item.kind(); + run_item_main_thread!(self, |self_: Self| { + (*self_.0) + .as_ref() + .insert(kind.inner().inner_muda(), position) + })? + .map_err(Into::into) + } + + /// Insert menu items at the specified `position` in this submenu. + pub fn insert_items(&self, items: &[&dyn IsMenuItem], position: usize) -> crate::Result<()> { + for (i, item) in items.iter().enumerate() { + self.insert(*item, position + i)? + } + + Ok(()) + } + + /// Remove a menu item from this submenu. + pub fn remove(&self, item: &dyn IsMenuItem) -> crate::Result<()> { + let kind = item.kind(); + run_item_main_thread!(self, |self_: Self| { + (*self_.0).as_ref().remove(kind.inner().inner_muda()) + })? + .map_err(Into::into) + } + + /// Remove the menu item at the specified position from this submenu and returns it. + pub fn remove_at(&self, position: usize) -> crate::Result>> { + run_item_main_thread!(self, |self_: Self| { + (*self_.0) + .as_ref() + .remove_at(position) + .map(|i| MenuItemKind::from_muda(self_.0.app_handle.clone(), i)) + }) + } + + /// Retrieves the menu item matching the given identifier. + pub fn get<'a, I>(&self, id: &'a I) -> Option> + where + I: ?Sized, + MenuId: PartialEq<&'a I>, + { + self + .items() + .unwrap_or_default() + .into_iter() + .find(|i| i.id() == &id) + } + + /// Returns a list of menu items that has been added to this submenu. + pub fn items(&self) -> crate::Result>> { + run_item_main_thread!(self, |self_: Self| { + (*self_.0) + .as_ref() + .items() + .into_iter() + .map(|i| MenuItemKind::from_muda(self_.0.app_handle.clone(), i)) + .collect::>() + }) + } + + /// Get the text for this submenu. + pub fn text(&self) -> crate::Result { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().text()) + } + + /// Set the text for this submenu. `text` could optionally contain + /// an `&` before a character to assign this character as the mnemonic + /// for this submenu. To display a `&` without assigning a mnemonic, use `&&`. + pub fn set_text>(&self, text: S) -> crate::Result<()> { + let text = text.as_ref().to_string(); + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_text(text)) + } + + /// Get whether this submenu is enabled or not. + pub fn is_enabled(&self) -> crate::Result { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().is_enabled()) + } + + /// Enable or disable this submenu. + pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_enabled(enabled)) + } + + /// Set this submenu as the Window menu for the application on macOS. + /// + /// This will cause macOS to automatically add window-switching items and + /// certain other items to the menu. + #[cfg(target_os = "macos")] + pub fn set_as_windows_menu_for_nsapp(&self) -> crate::Result<()> { + run_item_main_thread!(self, |self_: Self| { + (*self_.0).as_ref().set_as_windows_menu_for_nsapp() + })?; + Ok(()) + } + + /// Set this submenu as the Help menu for the application on macOS. + /// + /// This will cause macOS to automatically add a search box to the menu. + /// + /// If no menu is set as the Help menu, macOS will automatically use any menu + /// which has a title matching the localized word "Help". + #[cfg(target_os = "macos")] + pub fn set_as_help_menu_for_nsapp(&self) -> crate::Result<()> { + run_item_main_thread!(self, |self_: Self| { + (*self_.0).as_ref().set_as_help_menu_for_nsapp() + })?; + Ok(()) + } + + /// Change this submenu icon or remove it. + pub fn set_icon(&self, icon: Option>) -> crate::Result<()> { + let icon = match icon { + Some(i) => Some(i.try_into()?), + None => None, + }; + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_icon(icon)) + } + + /// Change this submenu icon to a native image or remove it. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux**: Unsupported. + pub fn set_native_icon(&self, _icon: Option) -> crate::Result<()> { + #[cfg(target_os = "macos")] + return run_item_main_thread!(self, |self_: Self| { + (*self_.0).as_ref().set_native_icon(_icon.map(Into::into)) + }); + #[allow(unreachable_code)] + Ok(()) + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/path/android.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/path/android.rs new file mode 100644 index 000000000..4180232bf --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/path/android.rs @@ -0,0 +1,202 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use super::{AppDirectory, Result}; +use crate::{AppHandle, Runtime, plugin::PluginHandle}; +use std::path::{Path, PathBuf}; + +/// A helper class to access the mobile path APIs. +pub struct PathResolver(pub(crate) PluginHandle); + +impl Clone for PathResolver { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +#[derive(serde::Deserialize)] +struct PathResponse { + path: PathBuf, +} + +#[derive(serde::Serialize)] +struct GetFileNameFromUriRequest<'a> { + uri: &'a str, +} + +#[derive(serde::Deserialize)] +struct GetFileNameFromUriResponse { + name: Option, +} + +impl PathResolver { + /// Returns the final component of the `Path`, if there is one. + /// + /// If the path is a normal file, this is the file name. If it's the path of a directory, this + /// is the directory name. + /// + /// Returns [`None`] if the path terminates in `..`. + /// + /// On Android this also supports checking the file name of content URIs, such as the values returned by the dialog plugin. + /// + /// If you are dealing with plain file system paths or not worried about Android content URIs, prefer [`Path::file_name`]. + pub fn file_name(&self, path: &str) -> Option { + if path.starts_with("content://") || path.starts_with("file://") { + self + .0 + .run_mobile_plugin::( + "getFileNameFromUri", + GetFileNameFromUriRequest { uri: path }, + ) + .map(|r| r.name) + .unwrap_or_else(|e| { + log::error!("failed to get file name from URI: {e}"); + None + }) + } else { + Path::new(path) + .file_name() + .map(|name| name.to_string_lossy().into_owned()) + } + } + + pub(super) fn app_handle(&self) -> &AppHandle { + self.0.app() + } + + fn call_resolve(&self, dir: &str) -> Result { + self + .0 + .run_mobile_plugin::(dir, ()) + .map(|r| r.path) + .map_err(Into::into) + } + + /// Returns the path to the user's audio directory. + pub fn audio_dir(&self) -> Result { + self.call_resolve("getAudioDir") + } + + /// Returns the path to the user's cache directory. + pub fn cache_dir(&self) -> Result { + self.call_resolve("getExternalCacheDir") + } + + /// Returns the path to the user's config directory. + pub fn config_dir(&self) -> Result { + self.call_resolve("getConfigDir") + } + + /// Returns the path to the user's data directory. + pub fn data_dir(&self) -> Result { + self.call_resolve("getDataDir") + } + + /// Returns the path to the user's local data directory. + pub fn local_data_dir(&self) -> Result { + self.call_resolve("getDataDir") + } + + /// Returns the path to the user's document directory. + pub fn document_dir(&self) -> Result { + self.call_resolve("getDocumentDir") + } + + /// Returns the path to the user's download directory. + pub fn download_dir(&self) -> Result { + self.call_resolve("getDownloadDir") + } + + /// Returns the path to the user's picture directory. + pub fn picture_dir(&self) -> Result { + self.call_resolve("getPictureDir") + } + + /// Returns the path to the user's public directory. + pub fn public_dir(&self) -> Result { + self.call_resolve("getPublicDir") + } + + /// Returns the path to the user's video directory. + /// + /// Resolves to the app-specific Movies directory (`getExternalFilesDir(DIRECTORY_MOVIES)`), + /// typically `.../files/Movies`. + /// + /// ## Migration + /// + /// Previously this resolved to external cache storage ([`cache_dir`], typically `.../cache`). + /// Files written to the old location will not be discovered at the new path and must be migrated + /// or paths updated accordingly. + pub fn video_dir(&self) -> Result { + self.call_resolve("getVideoDir") + } + + /// Returns the path to the resource directory of this app. + pub fn resource_dir(&self) -> Result { + self.call_resolve("getResourcesDir") + } + + /// Returns the path to the suggested directory for your app's config files. + /// + /// Resolves to [`config_dir`]`/${bundle_identifier}`, + /// unless overridden with the [`app > appDirectoriesOverride`](crate::utils::config::AppConfig::app_directories_override) config. + pub fn app_config_dir(&self) -> Result { + self.app_dir(AppDirectory::Config, || self.call_resolve("getConfigDir")) + } + + /// Returns the path to the suggested directory for your app's data files. + /// + /// Resolves to [`data_dir`]`/${bundle_identifier}`, + /// unless overridden with the [`app > appDirectoriesOverride`](crate::utils::config::AppConfig::app_directories_override) config. + pub fn app_data_dir(&self) -> Result { + self.app_dir(AppDirectory::Data, || self.call_resolve("getDataDir")) + } + + /// Returns the path to the suggested directory for your app's local data files. + /// + /// Resolves to [`local_data_dir`]`/${bundle_identifier}`, + /// unless overridden with the [`app > appDirectoriesOverride`](crate::utils::config::AppConfig::app_directories_override) config. + pub fn app_local_data_dir(&self) -> Result { + self.app_dir(AppDirectory::LocalData, || self.call_resolve("getDataDir")) + } + + /// Returns the path to the suggested directory for your app's cache files. + /// + /// Resolves to [`cache_dir`]`/${bundle_identifier}`, + /// unless overridden with the [`app > appDirectoriesOverride`](crate::utils::config::AppConfig::app_directories_override) config + /// (a single root override resolves to `/caches`). + pub fn app_cache_dir(&self) -> Result { + self.app_dir(AppDirectory::Cache, || self.call_resolve("getCacheDir")) + } + + /// Returns the path to the suggested directory for your app's log files. + /// + /// Resolves to [`config_dir`]`/${bundle_identifier}/logs`, + /// unless overridden with the [`app > appDirectoriesOverride`](crate::utils::config::AppConfig::app_directories_override) config + /// (a single root override resolves to `/logs`). + pub fn app_log_dir(&self) -> Result { + self.app_dir(AppDirectory::Log, || { + self + .call_resolve("getConfigDir") + .map(|dir| dir.join("logs")) + }) + } + + /// A temporary directory. Resolves to [`std::env::temp_dir`]. + pub fn temp_dir(&self) -> Result { + Ok(std::env::temp_dir()) + } + + /// Returns the path to the user's home directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to `$HOME`. + /// - **macOS:** Resolves to `$HOME`. + /// - **Windows:** Resolves to `{FOLDERID_Profile}`. + /// - **iOS**: Cannot be written to directly, use one of the app paths instead. + pub fn home_dir(&self) -> Result { + self.call_resolve("getHomeDir") + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/path/desktop.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/path/desktop.rs new file mode 100644 index 000000000..109f0f923 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/path/desktop.rs @@ -0,0 +1,574 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use super::{AppDirectory, Error, Result}; +use crate::{AppHandle, Manager, Runtime}; +use std::path::{Path, PathBuf}; + +/// The path resolver is a helper class for general and application-specific path APIs. +pub struct PathResolver(pub(crate) AppHandle); + +impl Clone for PathResolver { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl PathResolver { + /// Returns the final component of the `Path`, if there is one. + /// + /// If the path is a normal file, this is the file name. If it's the path of a directory, this + /// is the directory name. + /// + /// Returns [`None`] if the path terminates in `..`. + /// + /// On Android this also supports checking the file name of content URIs, such as the values returned by the dialog plugin. + /// + /// If you are dealing with plain file system paths or not worried about Android content URIs, prefer [`Path::file_name`]. + pub fn file_name(&self, path: &str) -> Option { + Path::new(path) + .file_name() + .map(|name| name.to_string_lossy().into_owned()) + } + + /// Returns the path to the user's audio directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_MUSIC_DIR`. + /// - **macOS:** Resolves to `$HOME/Music`. + /// - **Windows:** Resolves to `{FOLDERID_Music}`. + pub fn audio_dir(&self) -> Result { + dirs::audio_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's cache directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to `$XDG_CACHE_HOME` or `$HOME/.cache`. + /// - **macOS:** Resolves to `$HOME/Library/Caches`. + /// - **Windows:** Resolves to `{FOLDERID_LocalAppData}`. + pub fn cache_dir(&self) -> Result { + dirs::cache_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's config directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to `$XDG_CONFIG_HOME` or `$HOME/.config`. + /// - **macOS:** Resolves to `$HOME/Library/Application Support`. + /// - **Windows:** Resolves to `{FOLDERID_RoamingAppData}`. + pub fn config_dir(&self) -> Result { + dirs::config_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's data directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to `$XDG_DATA_HOME` or `$HOME/.local/share`. + /// - **macOS:** Resolves to `$HOME/Library/Application Support`. + /// - **Windows:** Resolves to `{FOLDERID_RoamingAppData}`. + pub fn data_dir(&self) -> Result { + dirs::data_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's local data directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to `$XDG_DATA_HOME` or `$HOME/.local/share`. + /// - **macOS:** Resolves to `$HOME/Library/Application Support`. + /// - **Windows:** Resolves to `{FOLDERID_LocalAppData}`. + pub fn local_data_dir(&self) -> Result { + dirs::data_local_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's desktop directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DESKTOP_DIR`. + /// - **macOS:** Resolves to `$HOME/Desktop`. + /// - **Windows:** Resolves to `{FOLDERID_Desktop}`. + pub fn desktop_dir(&self) -> Result { + dirs::desktop_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's document directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DOCUMENTS_DIR`. + /// - **macOS:** Resolves to `$HOME/Documents`. + /// - **Windows:** Resolves to `{FOLDERID_Documents}`. + pub fn document_dir(&self) -> Result { + dirs::document_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's download directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DOWNLOAD_DIR`. + /// - **macOS:** Resolves to `$HOME/Downloads`. + /// - **Windows:** Resolves to `{FOLDERID_Downloads}`. + pub fn download_dir(&self) -> Result { + dirs::download_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's executable directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to `$XDG_BIN_HOME/../bin` or `$XDG_DATA_HOME/../bin` or `$HOME/.local/bin`. + /// - **macOS:** Not supported. + /// - **Windows:** Not supported. + pub fn executable_dir(&self) -> Result { + dirs::executable_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's font directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to `$XDG_DATA_HOME/fonts` or `$HOME/.local/share/fonts`. + /// - **macOS:** Resolves to `$HOME/Library/Fonts`. + /// - **Windows:** Not supported. + pub fn font_dir(&self) -> Result { + dirs::font_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's home directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to `$HOME`. + /// - **macOS:** Resolves to `$HOME`. + /// - **Windows:** Resolves to `{FOLDERID_Profile}`. + /// - **iOS**: Cannot be written to directly, use one of the app paths instead. + pub fn home_dir(&self) -> Result { + dirs::home_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's picture directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_PICTURES_DIR`. + /// - **macOS:** Resolves to `$HOME/Pictures`. + /// - **Windows:** Resolves to `{FOLDERID_Pictures}`. + pub fn picture_dir(&self) -> Result { + dirs::picture_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's public directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_PUBLICSHARE_DIR`. + /// - **macOS:** Resolves to `$HOME/Public`. + /// - **Windows:** Resolves to `{FOLDERID_Public}`. + pub fn public_dir(&self) -> Result { + dirs::public_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's runtime directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to `$XDG_RUNTIME_DIR`. + /// - **macOS:** Not supported. + /// - **Windows:** Not supported. + pub fn runtime_dir(&self) -> Result { + dirs::runtime_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's template directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_TEMPLATES_DIR`. + /// - **macOS:** Not supported. + /// - **Windows:** Resolves to `{FOLDERID_Templates}`. + pub fn template_dir(&self) -> Result { + dirs::template_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's video dir + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_VIDEOS_DIR`. + /// - **macOS:** Resolves to `$HOME/Movies`. + /// - **Windows:** Resolves to `{FOLDERID_Videos}`. + pub fn video_dir(&self) -> Result { + dirs::video_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the resource directory of this app. + /// + /// ## Platform-specific + /// + /// Although we provide the exact path where this function resolves to, + /// this is not a contract and things might change in the future + /// + /// - **Windows:** Resolves to the directory that contains the main executable. + /// - **Linux:** When running in an AppImage, the `APPDIR` variable will be set to + /// the mounted location of the app, and the resource dir will be `${APPDIR}/usr/lib/${exe_name}`. + /// If not running in an AppImage, the path is `/usr/lib/${exe_name}`. + /// When running the app from `src-tauri/target/(debug|release)/`, the path is `${exe_dir}/../lib/${exe_name}`. + /// - **macOS:** Resolves to `${exe_dir}/../Resources` (inside .app). + /// - **iOS:** Resolves to `${exe_dir}/assets`. + /// - **Android:** Currently the resources are stored in the APK as assets so it's not a normal file system path, + /// we return a special URI prefix `asset://localhost/` here that can be used with the [file system plugin](https://tauri.app/plugin/file-system/), + /// with that, you can read the files through [`FsExt::fs`](https://docs.rs/tauri-plugin-fs/latest/tauri_plugin_fs/trait.FsExt.html#tymethod.fs) + /// like this: `app.fs().read_to_string(app.path().resource_dir().unwrap().join("resource"));` + pub fn resource_dir(&self) -> Result { + crate::utils::platform::resource_dir(self.0.package_info(), &self.0.env()) + .map_err(|_| Error::UnknownPath) + } + + /// Returns the path to the suggested directory for your app's config files. + /// + /// Resolves to [`config_dir`](Self::config_dir)`/${bundle_identifier}`, + /// unless overridden with the [`app > appDirectoriesOverride`](crate::utils::config::AppConfig::app_directories_override) config. + pub fn app_config_dir(&self) -> Result { + self.app_dir(AppDirectory::Config, || { + dirs::config_dir() + .ok_or(Error::UnknownPath) + .map(|dir| dir.join(&self.0.config().identifier)) + }) + } + + /// Returns the path to the suggested directory for your app's data files. + /// + /// Resolves to [`data_dir`](Self::data_dir)`/${bundle_identifier}`, + /// unless overridden with the [`app > appDirectoriesOverride`](crate::utils::config::AppConfig::app_directories_override) config. + pub fn app_data_dir(&self) -> Result { + self.app_dir(AppDirectory::Data, || { + dirs::data_dir() + .ok_or(Error::UnknownPath) + .map(|dir| dir.join(&self.0.config().identifier)) + }) + } + + /// Returns the path to the suggested directory for your app's local data files. + /// + /// Resolves to [`local_data_dir`](Self::local_data_dir)`/${bundle_identifier}`, + /// unless overridden with the [`app > appDirectoriesOverride`](crate::utils::config::AppConfig::app_directories_override) config. + /// + /// On Windows and Linux this is also the default data directory of the webviews. + pub fn app_local_data_dir(&self) -> Result { + self.app_dir(AppDirectory::LocalData, || { + dirs::data_local_dir() + .ok_or(Error::UnknownPath) + .map(|dir| dir.join(&self.0.config().identifier)) + }) + } + + /// Returns the path to the suggested directory for your app's cache files. + /// + /// Resolves to [`cache_dir`](Self::cache_dir)`/${bundle_identifier}`, + /// unless overridden with the [`app > appDirectoriesOverride`](crate::utils::config::AppConfig::app_directories_override) config + /// (a single root override resolves to `/caches`). + pub fn app_cache_dir(&self) -> Result { + self.app_dir(AppDirectory::Cache, || { + dirs::cache_dir() + .ok_or(Error::UnknownPath) + .map(|dir| dir.join(&self.0.config().identifier)) + }) + } + + /// Returns the path to the suggested directory for your app's log files. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to [`local_data_dir`](Self::local_data_dir)`/${bundle_identifier}/logs`. + /// - **macOS:** Resolves to [`home_dir`](Self::home_dir)`/Library/Logs/${bundle_identifier}` + /// - **Windows:** Resolves to [`local_data_dir`](Self::local_data_dir)`/${bundle_identifier}/logs`. + /// + /// All of them can be overridden with the [`app > appDirectoriesOverride`](crate::utils::config::AppConfig::app_directories_override) config + /// (a single root override resolves to `/logs`). + pub fn app_log_dir(&self) -> Result { + self.app_dir(AppDirectory::Log, || { + #[cfg(target_os = "macos")] + let path = dirs::home_dir() + .ok_or(Error::UnknownPath) + .map(|dir| dir.join("Library/Logs").join(&self.0.config().identifier)); + + #[cfg(not(target_os = "macos"))] + let path = dirs::data_local_dir() + .ok_or(Error::UnknownPath) + .map(|dir| dir.join(&self.0.config().identifier).join("logs")); + + path + }) + } + + /// A temporary directory. Resolves to [`std::env::temp_dir`]. + pub fn temp_dir(&self) -> Result { + Ok(std::env::temp_dir()) + } + + pub(super) fn app_handle(&self) -> &AppHandle { + &self.0 + } + + /// The directory relative app directory overrides are resolved against: the directory containing the app binary. + /// + /// When running from an AppImage on Linux, this is the directory containing the AppImage file, + /// and when running from a `.app` bundle on macOS, the directory containing the bundle. + #[cfg(desktop)] + pub(super) fn app_binary_dir(&self) -> Result { + let binary = crate::process::current_binary(&self.0.env())?; + let dir = binary.parent().ok_or(Error::NoParent)?; + + #[cfg(target_os = "macos")] + if let Some(bundle_parent) = macos_bundle_parent(dir) { + return Ok(bundle_parent); + } + + Ok(dir.to_path_buf()) + } +} + +/// For a `

/.app/Contents/MacOS` directory, returns ``. +#[cfg(any(target_os = "macos", test))] +fn macos_bundle_parent(macos_dir: &Path) -> Option { + use std::ffi::OsStr; + + if macos_dir.file_name() != Some(OsStr::new("MacOS")) { + return None; + } + + let contents_dir = macos_dir.parent()?; + if contents_dir.file_name() != Some(OsStr::new("Contents")) { + return None; + } + + let bundle = contents_dir.parent()?; + if bundle.extension() != Some(OsStr::new("app")) { + return None; + } + + bundle.parent().map(Path::to_path_buf) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::path::normalize; + use crate::{ + App, + test::{MockRuntime, mock_builder, mock_context, noop_assets}, + }; + use std::path::Component; + use tauri_utils::config::{AppDirectoriesOverride, AppDirectoryOverrides}; + + const IDENTIFIER: &str = "com.tauri.test"; + + fn app_with(app_directories_override: Option) -> App { + let mut context = mock_context(noop_assets()); + context.config_mut().identifier = IDENTIFIER.into(); + context.config_mut().app.app_directories_override = app_directories_override; + mock_builder().build(context).unwrap() + } + + /// The directory containing the test binary, which is never inside an app bundle. + fn binary_dir() -> PathBuf { + crate::utils::platform::current_exe() + .unwrap() + .parent() + .unwrap() + .to_path_buf() + } + + #[test] + fn default_directories() { + let app = app_with(None); + let path = app.path(); + assert_eq!( + path.app_config_dir().unwrap(), + dirs::config_dir().unwrap().join(IDENTIFIER) + ); + assert_eq!( + path.app_data_dir().unwrap(), + dirs::data_dir().unwrap().join(IDENTIFIER) + ); + assert_eq!( + path.app_local_data_dir().unwrap(), + dirs::data_local_dir().unwrap().join(IDENTIFIER) + ); + assert_eq!( + path.app_cache_dir().unwrap(), + dirs::cache_dir().unwrap().join(IDENTIFIER) + ); + } + + #[test] + fn root_override_relative_to_binary() { + let binary_dir = binary_dir(); + + let app = app_with(Some(AppDirectoriesOverride::Root("./".into()))); + let path = app.path(); + assert_eq!(path.app_config_dir().unwrap(), binary_dir); + assert_eq!(path.app_data_dir().unwrap(), binary_dir); + assert_eq!(path.app_local_data_dir().unwrap(), binary_dir); + assert_eq!(path.app_cache_dir().unwrap(), binary_dir.join("caches")); + assert_eq!(path.app_log_dir().unwrap(), binary_dir.join("logs")); + // the `.` component must not leak into the resolved path + assert!( + !path + .app_cache_dir() + .unwrap() + .components() + .any(|c| c == Component::CurDir) + ); + + let app = app_with(Some(AppDirectoriesOverride::Root("data".into()))); + assert_eq!(app.path().app_data_dir().unwrap(), binary_dir.join("data")); + + // `..` components are kept + let app = app_with(Some(AppDirectoriesOverride::Root("../data".into()))); + assert_eq!( + app.path().app_data_dir().unwrap(), + binary_dir.join("../data") + ); + } + + #[test] + fn root_override_absolute() { + let root = normalize(std::env::temp_dir().join("tauri-app-directories-override")); + let app = app_with(Some(AppDirectoriesOverride::Root(root.clone()))); + let path = app.path(); + assert_eq!(path.app_config_dir().unwrap(), root); + assert_eq!(path.app_data_dir().unwrap(), root); + assert_eq!(path.app_local_data_dir().unwrap(), root); + assert_eq!(path.app_cache_dir().unwrap(), root.join("caches")); + assert_eq!(path.app_log_dir().unwrap(), root.join("logs")); + } + + #[test] + fn root_override_variable() { + let data_dir = dirs::data_dir().unwrap(); + + let app = app_with(Some(AppDirectoriesOverride::Root("$DATA/my-app".into()))); + let path = app.path(); + assert_eq!(path.app_config_dir().unwrap(), data_dir.join("my-app")); + assert_eq!( + path.app_cache_dir().unwrap(), + data_dir.join("my-app/caches") + ); + assert_eq!(path.app_log_dir().unwrap(), data_dir.join("my-app/logs")); + + let app = app_with(Some(AppDirectoriesOverride::Root("$DATA".into()))); + assert_eq!(app.path().app_data_dir().unwrap(), data_dir); + + // `..` components are kept, unlike `PathResolver::parse` + let app = app_with(Some(AppDirectoriesOverride::Root("$DATA/../my-app".into()))); + assert_eq!( + app.path().app_data_dir().unwrap(), + data_dir.join("../my-app") + ); + } + + #[test] + fn directories_override() { + let app = app_with(Some(AppDirectoriesOverride::Directories( + AppDirectoryOverrides { + log: Some("$CACHE/my-app/logs".into()), + cache: Some("custom-cache".into()), + ..Default::default() + }, + ))); + let path = app.path(); + + // overridden directories resolve to exactly the configured path + assert_eq!( + path.app_log_dir().unwrap(), + dirs::cache_dir().unwrap().join("my-app/logs") + ); + assert_eq!( + path.app_cache_dir().unwrap(), + binary_dir().join("custom-cache") + ); + + // the other directories keep their default location + assert_eq!( + path.app_config_dir().unwrap(), + dirs::config_dir().unwrap().join(IDENTIFIER) + ); + assert_eq!( + path.app_data_dir().unwrap(), + dirs::data_dir().unwrap().join(IDENTIFIER) + ); + assert_eq!( + path.app_local_data_dir().unwrap(), + dirs::data_local_dir().unwrap().join(IDENTIFIER) + ); + } + + #[test] + fn rejects_app_directory_variables() { + for variable in [ + "$APPCONFIG", + "$APPDATA", + "$APPLOCALDATA", + "$APPCACHE", + "$APPLOG", + ] { + let app = app_with(Some(AppDirectoriesOverride::Root( + format!("{variable}/my-app").into(), + ))); + let err = app.path().app_data_dir().unwrap_err(); + assert!( + matches!(err, Error::InvalidAppDirectoriesOverride(..)), + "{variable}: {err}" + ); + } + } + + #[test] + fn rejects_unknown_variables() { + let app = app_with(Some(AppDirectoriesOverride::Root("$UNKNOWN/my-app".into()))); + let err = app.path().app_data_dir().unwrap_err(); + assert!( + matches!(err, Error::InvalidAppDirectoriesOverride(..)), + "{err}" + ); + } + + #[cfg(windows)] + #[test] + fn rejects_root_relative_paths() { + for root in [r"\my-app", "C:my-app"] { + let app = app_with(Some(AppDirectoriesOverride::Root(root.into()))); + let err = app.path().app_data_dir().unwrap_err(); + assert!( + matches!(err, Error::InvalidAppDirectoriesOverride(..)), + "{root}: {err}" + ); + } + } + + #[test] + fn macos_bundle_parent_dir() { + assert_eq!( + macos_bundle_parent(Path::new("/Applications/My App.app/Contents/MacOS")), + Some(PathBuf::from("/Applications")) + ); + assert_eq!( + macos_bundle_parent(Path::new("/Applications/My App.app/Contents")), + None + ); + assert_eq!( + macos_bundle_parent(Path::new("/Applications/MyApp/Contents/MacOS")), + None + ); + assert_eq!(macos_bundle_parent(Path::new("/opt/my-app/bin")), None); + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/path/init.js b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/path/init.js new file mode 100644 index 000000000..a402f61e9 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/path/init.js @@ -0,0 +1,10 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +Object.defineProperty(window.__TAURI_INTERNALS__.plugins, 'path', { + value: { + sep: __TEMPLATE_sep__, + delimiter: __TEMPLATE_delimiter__ + } +}) diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/path/mod.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/path/mod.rs new file mode 100644 index 000000000..453437fdd --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/path/mod.rs @@ -0,0 +1,527 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + path::{Component, Display, Path, PathBuf}, + str::FromStr, +}; + +use crate::Runtime; +use tauri_utils::config::AppDirectoriesOverride; + +use serde::{Deserialize, Deserializer, Serialize, de::Error as DeError}; +use serde_repr::{Deserialize_repr, Serialize_repr}; + +pub(crate) mod plugin; + +use crate::error::*; + +#[cfg(target_os = "android")] +mod android; +#[cfg(not(target_os = "android"))] +mod desktop; + +#[cfg(target_os = "android")] +pub use android::PathResolver; +#[cfg(not(target_os = "android"))] +pub use desktop::PathResolver; + +/// A wrapper for [`PathBuf`] that prevents path traversal. +/// +/// # Examples +/// +/// ``` +/// # use tauri::path::SafePathBuf; +/// assert!(SafePathBuf::new("../secret.txt".into()).is_err()); +/// assert!(SafePathBuf::new("/home/user/stuff/../secret.txt".into()).is_err()); +/// +/// assert!(SafePathBuf::new("./file.txt".into()).is_ok()); +/// assert!(SafePathBuf::new("/home/user/secret.txt".into()).is_ok()); +/// ``` +#[derive(Clone, Debug, Serialize)] +pub struct SafePathBuf(PathBuf); + +impl SafePathBuf { + /// Validates the path for directory traversal vulnerabilities and returns a new [`SafePathBuf`] instance if it is safe. + pub fn new(path: PathBuf) -> std::result::Result { + if path.components().any(|x| matches!(x, Component::ParentDir)) { + return Err("cannot traverse directory, rewrite the path without the use of `../`"); + } + Ok(Self(path)) + } + + /// Returns an object that implements [`std::fmt::Display`] for safely printing paths. + /// + /// See [`PathBuf#method.display`] for more information. + pub fn display(&self) -> Display<'_> { + self.0.display() + } +} + +impl AsRef for SafePathBuf { + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} + +impl FromStr for SafePathBuf { + type Err = &'static str; + + fn from_str(s: &str) -> std::result::Result { + Self::new(s.into()) + } +} + +impl From for PathBuf { + fn from(path: SafePathBuf) -> Self { + path.0 + } +} + +impl<'de> Deserialize<'de> for SafePathBuf { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let path = PathBuf::deserialize(deserializer)?; + SafePathBuf::new(path).map_err(DeError::custom) + } +} + +/// A base directory for a path. +/// +/// The base directory is the optional root of a file system operation. +/// If informed by the API call, all paths will be relative to the path of the given directory. +/// +/// For more information, check the [`dirs` documentation](https://docs.rs/dirs/). +#[derive(Serialize_repr, Deserialize_repr, Clone, Copy, Debug)] +#[repr(u16)] +#[non_exhaustive] +pub enum BaseDirectory { + /// The Audio directory. + /// Resolves to [`crate::path::PathResolver::audio_dir`]. + Audio = 1, + /// The Cache directory. + /// Resolves to [`crate::path::PathResolver::cache_dir`]. + Cache = 2, + /// The Config directory. + /// Resolves to [`crate::path::PathResolver::config_dir`]. + Config = 3, + /// The Data directory. + /// Resolves to [`crate::path::PathResolver::data_dir`]. + Data = 4, + /// The LocalData directory. + /// Resolves to [`crate::path::PathResolver::local_data_dir`]. + LocalData = 5, + /// The Document directory. + /// Resolves to [`crate::path::PathResolver::document_dir`]. + Document = 6, + /// The Download directory. + /// Resolves to [`crate::path::PathResolver::download_dir`]. + Download = 7, + /// The Picture directory. + /// Resolves to [`crate::path::PathResolver::picture_dir`]. + Picture = 8, + /// The Public directory. + /// Resolves to [`crate::path::PathResolver::public_dir`]. + Public = 9, + /// The Video directory. + /// Resolves to [`crate::path::PathResolver::video_dir`]. + Video = 10, + /// The Resource directory. + /// Resolves to [`crate::path::PathResolver::resource_dir`]. + Resource = 11, + /// A temporary directory. + /// Resolves to [`std::env::temp_dir`]. + Temp = 12, + /// The default app config directory. + /// Resolves to [`crate::path::PathResolver::app_config_dir`], + /// which can be overridden with the `app > appDirectoriesOverride` config. + AppConfig = 13, + /// The default app data directory. + /// Resolves to [`crate::path::PathResolver::app_data_dir`], + /// which can be overridden with the `app > appDirectoriesOverride` config. + AppData = 14, + /// The default app local data directory. + /// Resolves to [`crate::path::PathResolver::app_local_data_dir`], + /// which can be overridden with the `app > appDirectoriesOverride` config. + AppLocalData = 15, + /// The default app cache directory. + /// Resolves to [`crate::path::PathResolver::app_cache_dir`], + /// which can be overridden with the `app > appDirectoriesOverride` config. + AppCache = 16, + /// The default app log directory. + /// Resolves to [`crate::path::PathResolver::app_log_dir`], + /// which can be overridden with the `app > appDirectoriesOverride` config. + AppLog = 17, + /// The Desktop directory. + /// Resolves to [`crate::path::PathResolver::desktop_dir`]. + #[cfg(not(target_os = "android"))] + Desktop = 18, + /// The Executable directory. + /// Resolves to [`crate::path::PathResolver::executable_dir`]. + #[cfg(not(target_os = "android"))] + Executable = 19, + /// The Font directory. + /// Resolves to [`crate::path::PathResolver::font_dir`]. + #[cfg(not(target_os = "android"))] + Font = 20, + /// The Home directory. + /// Resolves to [`crate::path::PathResolver::home_dir`]. + Home = 21, + /// The Runtime directory. + /// Resolves to [`crate::path::PathResolver::runtime_dir`]. + #[cfg(not(target_os = "android"))] + Runtime = 22, + /// The Template directory. + /// Resolves to [`crate::path::PathResolver::template_dir`]. + #[cfg(not(target_os = "android"))] + Template = 23, +} + +impl BaseDirectory { + /// Gets the variable that represents this [`BaseDirectory`] for string paths. + pub fn variable(self) -> &'static str { + match self { + Self::Audio => "$AUDIO", + Self::Cache => "$CACHE", + Self::Config => "$CONFIG", + Self::Data => "$DATA", + Self::LocalData => "$LOCALDATA", + Self::Document => "$DOCUMENT", + Self::Download => "$DOWNLOAD", + Self::Picture => "$PICTURE", + Self::Public => "$PUBLIC", + Self::Video => "$VIDEO", + Self::Resource => "$RESOURCE", + Self::Temp => "$TEMP", + Self::AppConfig => "$APPCONFIG", + Self::AppData => "$APPDATA", + Self::AppLocalData => "$APPLOCALDATA", + Self::AppCache => "$APPCACHE", + Self::AppLog => "$APPLOG", + Self::Home => "$HOME", + + #[cfg(not(target_os = "android"))] + Self::Desktop => "$DESKTOP", + #[cfg(not(target_os = "android"))] + Self::Executable => "$EXE", + #[cfg(not(target_os = "android"))] + Self::Font => "$FONT", + #[cfg(not(target_os = "android"))] + Self::Runtime => "$RUNTIME", + #[cfg(not(target_os = "android"))] + Self::Template => "$TEMPLATE", + } + } + + /// Gets the [`BaseDirectory`] associated with the given variable, or [`None`] if the variable doesn't match any. + pub fn from_variable(variable: &str) -> Option { + let res = match variable { + "$AUDIO" => Self::Audio, + "$CACHE" => Self::Cache, + "$CONFIG" => Self::Config, + "$DATA" => Self::Data, + "$LOCALDATA" => Self::LocalData, + "$DOCUMENT" => Self::Document, + "$DOWNLOAD" => Self::Download, + + "$PICTURE" => Self::Picture, + "$PUBLIC" => Self::Public, + "$VIDEO" => Self::Video, + "$RESOURCE" => Self::Resource, + "$TEMP" => Self::Temp, + "$APPCONFIG" => Self::AppConfig, + "$APPDATA" => Self::AppData, + "$APPLOCALDATA" => Self::AppLocalData, + "$APPCACHE" => Self::AppCache, + "$APPLOG" => Self::AppLog, + "$HOME" => Self::Home, + + #[cfg(not(target_os = "android"))] + "$DESKTOP" => Self::Desktop, + #[cfg(not(target_os = "android"))] + "$EXE" => Self::Executable, + #[cfg(not(target_os = "android"))] + "$FONT" => Self::Font, + #[cfg(not(target_os = "android"))] + "$RUNTIME" => Self::Runtime, + #[cfg(not(target_os = "android"))] + "$TEMPLATE" => Self::Template, + + _ => return None, + }; + Some(res) + } +} + +impl PathResolver { + /// Resolves the path with the base directory. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri::{path::BaseDirectory, Manager}; + /// tauri::Builder::default() + /// .setup(|app| { + /// let path = app.path().resolve("path/to/something", BaseDirectory::Config)?; + /// assert_eq!(path.to_str().unwrap(), "/home/${whoami}/.config/path/to/something"); + /// Ok(()) + /// }); + /// ``` + pub fn resolve>(&self, path: P, base_directory: BaseDirectory) -> Result { + resolve_path::(self, base_directory, Some(path.as_ref().to_path_buf())) + } + + /// Parse the given path, resolving a [`BaseDirectory`] variable if the path starts with one. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri::Manager; + /// tauri::Builder::default() + /// .setup(|app| { + /// let path = app.path().parse("$HOME/.bashrc")?; + /// assert_eq!(path.to_str().unwrap(), "/home/${whoami}/.bashrc"); + /// Ok(()) + /// }); + /// ``` + pub fn parse>(&self, path: P) -> Result { + let mut p = PathBuf::new(); + let mut components = path.as_ref().components(); + match components.next() { + Some(Component::Normal(str)) => { + if let Some(base_directory) = BaseDirectory::from_variable(&str.to_string_lossy()) { + p.push(resolve_path::(self, base_directory, None)?); + } else { + p.push(str); + } + } + Some(component) => p.push(component), + None => (), + } + + for component in components { + if let Component::ParentDir = component { + continue; + } + p.push(component); + } + + Ok(p) + } +} + +fn resolve_path( + resolver: &PathResolver, + directory: BaseDirectory, + path: Option, +) -> Result { + let resolve_resource = matches!(directory, BaseDirectory::Resource); + let mut base_dir_path = match directory { + BaseDirectory::Audio => resolver.audio_dir(), + BaseDirectory::Cache => resolver.cache_dir(), + BaseDirectory::Config => resolver.config_dir(), + BaseDirectory::Data => resolver.data_dir(), + BaseDirectory::LocalData => resolver.local_data_dir(), + BaseDirectory::Document => resolver.document_dir(), + BaseDirectory::Download => resolver.download_dir(), + BaseDirectory::Picture => resolver.picture_dir(), + BaseDirectory::Public => resolver.public_dir(), + BaseDirectory::Video => resolver.video_dir(), + BaseDirectory::Resource => resolver.resource_dir(), + BaseDirectory::Temp => resolver.temp_dir(), + BaseDirectory::AppConfig => resolver.app_config_dir(), + BaseDirectory::AppData => resolver.app_data_dir(), + BaseDirectory::AppLocalData => resolver.app_local_data_dir(), + BaseDirectory::AppCache => resolver.app_cache_dir(), + BaseDirectory::AppLog => resolver.app_log_dir(), + BaseDirectory::Home => resolver.home_dir(), + #[cfg(not(target_os = "android"))] + BaseDirectory::Desktop => resolver.desktop_dir(), + #[cfg(not(target_os = "android"))] + BaseDirectory::Executable => resolver.executable_dir(), + #[cfg(not(target_os = "android"))] + BaseDirectory::Font => resolver.font_dir(), + #[cfg(not(target_os = "android"))] + BaseDirectory::Runtime => resolver.runtime_dir(), + #[cfg(not(target_os = "android"))] + BaseDirectory::Template => resolver.template_dir(), + }?; + + if let Some(path) = path { + // use the same path resolution mechanism as the bundler's resource injection algorithm + if resolve_resource { + let mut resource_path = PathBuf::new(); + for component in path.components() { + match component { + Component::Prefix(_) => {} + Component::RootDir => resource_path.push("_root_"), + Component::CurDir => {} + Component::ParentDir => resource_path.push("_up_"), + Component::Normal(p) => resource_path.push(p), + } + } + base_dir_path.push(resource_path); + } else { + base_dir_path.push(path); + } + } + + Ok(base_dir_path) +} + +/// An app-specific directory that can be overridden with the `app > appDirectoriesOverride` config. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum AppDirectory { + Config, + Data, + LocalData, + Cache, + Log, +} + +impl AppDirectory { + /// The subdirectory this directory resolves to when a single root overrides all app directories. + fn root_override_subdirectory(self) -> Option<&'static str> { + match self { + Self::Cache => Some("caches"), + Self::Log => Some("logs"), + Self::Config | Self::Data | Self::LocalData => None, + } + } +} + +impl PathResolver { + /// Resolves an app directory, honoring the `app > appDirectoriesOverride` config. + pub(crate) fn app_dir( + &self, + dir: AppDirectory, + default: impl FnOnce() -> Result, + ) -> Result { + match self.app_directory_override(dir)? { + Some(path) => Ok(path), + None => default(), + } + } + + /// Resolves the override configured for the given app directory, if any. + fn app_directory_override(&self, dir: AppDirectory) -> Result> { + let Some(config) = &self.app_handle().config().app.app_directories_override else { + return Ok(None); + }; + + let (path, subdirectory) = match config { + AppDirectoriesOverride::Root(root) => (root, dir.root_override_subdirectory()), + AppDirectoriesOverride::Directories(directories) => { + let path = match dir { + AppDirectory::Config => &directories.config, + AppDirectory::Data => &directories.data, + AppDirectory::LocalData => &directories.local_data, + AppDirectory::Cache => &directories.cache, + AppDirectory::Log => &directories.log, + }; + match path { + Some(path) => (path, None), + None => return Ok(None), + } + } + }; + + let mut path = self.resolve_override_path(path)?; + if let Some(subdirectory) = subdirectory { + path.push(subdirectory); + } + + Ok(Some(path)) + } + + /// Resolves a path from the `app > appDirectoriesOverride` config: + /// + /// - a path starting with a base directory variable (e.g. `$DATA/my-app`) is resolved against that directory, + /// - an absolute path is used as is, + /// - any other path is resolved relative to the app binary directory on desktop, + /// and rejected on mobile where the app bundle is read-only. + fn resolve_override_path(&self, path: &Path) -> Result { + let mut components = path.components(); + let first = components.next(); + + if let Some(Component::Normal(first)) = first { + if let Some(variable) = first.to_str().filter(|s| s.starts_with('$')) { + let base_directory = BaseDirectory::from_variable(variable).ok_or_else(|| { + Error::InvalidAppDirectoriesOverride( + path.to_path_buf(), + format!("unknown base directory variable `{variable}`"), + ) + })?; + + if matches!( + base_directory, + BaseDirectory::AppConfig + | BaseDirectory::AppData + | BaseDirectory::AppLocalData + | BaseDirectory::AppCache + | BaseDirectory::AppLog + ) { + return Err(Error::InvalidAppDirectoriesOverride( + path.to_path_buf(), + format!("`{variable}` refers to an app directory, which is what is being overridden"), + )); + } + + // unlike `parse`, `resolve` keeps `..` components + return self + .resolve(components.as_path(), base_directory) + .map(normalize); + } + } + + if path.is_absolute() { + return Ok(normalize(path)); + } + + // Windows root-relative (`\foo`) and drive-relative (`C:foo`) paths would replace the base directory on join + if path.has_root() || matches!(first, Some(Component::Prefix(_))) { + return Err(Error::InvalidAppDirectoriesOverride( + path.to_path_buf(), + "root-relative and drive-relative paths are not supported".into(), + )); + } + + #[cfg(desktop)] + { + Ok(normalize(self.app_binary_dir()?.join(path))) + } + #[cfg(mobile)] + { + Err(Error::InvalidAppDirectoriesOverride( + path.to_path_buf(), + "relative paths are not supported on Android and iOS, use a base directory variable or an absolute path".into(), + )) + } + } +} + +/// Removes `.` components and trailing separators from a path, keeping `..` components. +fn normalize(path: impl AsRef) -> PathBuf { + path.as_ref().components().collect() +} + +#[cfg(test)] +mod test { + use super::SafePathBuf; + use quickcheck::{Arbitrary, Gen}; + + use std::path::PathBuf; + + impl Arbitrary for SafePathBuf { + fn arbitrary(g: &mut Gen) -> Self { + Self(PathBuf::arbitrary(g)) + } + + fn shrink(&self) -> Box> { + Box::new(self.0.shrink().map(SafePathBuf)) + } + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/path/plugin.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/path/plugin.rs new file mode 100644 index 000000000..c00ff6887 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/path/plugin.rs @@ -0,0 +1,315 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::path::{Component, MAIN_SEPARATOR, Path, PathBuf}; + +use serialize_to_javascript::{DefaultTemplate, Template, default_template}; + +use super::{BaseDirectory, Error, PathResolver, Result}; +use crate::{ + AppHandle, Manager, Runtime, State, command, + plugin::{Builder, TauriPlugin}, +}; + +/// Normalize a path, removing things like `.` and `..`, this snippet is taken from cargo's paths util. +/// +fn normalize_path(path: &Path) -> PathBuf { + let mut components = path.components().peekable(); + let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().copied() { + components.next(); + PathBuf::from(c.as_os_str()) + } else { + PathBuf::new() + }; + + for component in components { + match component { + Component::Prefix(..) => unreachable!(), + Component::RootDir => { + ret.push(component.as_os_str()); + } + Component::CurDir => {} + Component::ParentDir => { + ret.pop(); + } + Component::Normal(c) => { + ret.push(c); + } + } + } + ret +} + +/// Normalize a path, removing things like `.` and `..`, this snippet is taken from cargo's paths util but +/// slightly modified to not resolve absolute paths. +/// +fn normalize_path_no_absolute(path: &Path) -> PathBuf { + let mut components = path.components().peekable(); + let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().copied() { + components.next(); + PathBuf::from(c.as_os_str()) + } else { + PathBuf::new() + }; + + for component in components { + match component { + Component::Prefix(..) => unreachable!(), + Component::RootDir => { + ret.push(component.as_os_str()); + } + Component::CurDir => {} + Component::ParentDir => { + ret.pop(); + } + Component::Normal(c) => { + // Using PathBuf::push here will replace the whole path if an absolute path is encountered + // which is not the intended behavior, so instead of that, convert the current resolved path + // to a string and do simple string concatenation with the current component then convert it + // back to a PathBuf + let mut p = ret.to_string_lossy().to_string(); + // Only add a separator if it doesn't have one already or if current normalized path is empty, + // this ensures it won't have an unwanted leading separator + if !p.is_empty() && !p.ends_with('/') && !p.ends_with('\\') { + p.push(MAIN_SEPARATOR); + } + if let Some(c) = c.to_str() { + p.push_str(c); + } + ret = PathBuf::from(p); + } + } + } + ret +} + +#[command(root = "crate")] +pub fn resolve_directory( + _app: AppHandle, + resolver: State<'_, PathResolver>, + directory: BaseDirectory, + path: Option, +) -> Result { + super::resolve_path(&resolver, directory, path).map(|p| dunce::simplified(&p).to_path_buf()) +} + +#[command(root = "crate")] +pub fn resolve(paths: Vec) -> Result { + // Start with current directory then start adding paths from the vector one by one using `PathBuf.push()` which + // will ensure that if an absolute path is encountered in the iteration, it will be used as the current full path. + // + // examples: + // 1. `vec!["."]` or `vec![]` will be equal to `std::env::current_dir()` + // 2. `vec!["/foo/bar", "/tmp/file", "baz"]` will be equal to `PathBuf::from("/tmp/file/baz")` + let mut path = std::env::current_dir().map_err(Error::CurrentDir)?; + for p in paths { + path.push(p); + } + Ok(dunce::simplified(&normalize_path(&path)).to_path_buf()) +} + +#[command(root = "crate")] +pub fn normalize(path: String) -> String { + let mut p = dunce::simplified(&normalize_path_no_absolute(Path::new(&path))) + .to_string_lossy() + .to_string(); + + // Node.js behavior is to return `".."` for `normalize("..")` + // and `"."` for `normalize("")` or `normalize(".")` + if p.is_empty() && path == ".." { + "..".into() + } else if p.is_empty() && (path.is_empty() || path == ".") { + ".".into() + } else { + // Add a trailing separator if the path passed to this functions had a trailing separator. That's how Node.js behaves. + if (path.ends_with('/') || path.ends_with('\\')) && (!p.ends_with('/') || !p.ends_with('\\')) { + p.push(MAIN_SEPARATOR); + } + p + } +} + +#[command(root = "crate")] +pub fn join(paths: Vec) -> String { + let path = PathBuf::from( + paths + .into_iter() + .map(|mut p| { + // Add a `MAIN_SEPARATOR` if it doesn't already have one and is not an empty string. + // Doing this to ensure that the vector elements are separated in + // the resulting string so path.components() can work correctly when called + // in `normalize_path_no_absolute()` later on. + if !p.is_empty() && !p.ends_with('/') && !p.ends_with('\\') { + p.push(MAIN_SEPARATOR); + } + p + }) + .collect::(), + ); + + let p = dunce::simplified(&normalize_path_no_absolute(&path)) + .to_string_lossy() + .to_string(); + + if p.is_empty() { ".".into() } else { p } +} + +#[command(root = "crate")] +pub fn dirname(path: String) -> Result { + match Path::new(&path).parent() { + Some(p) => Ok(dunce::simplified(p).to_path_buf()), + None => Err(Error::NoParent), + } +} + +#[command(root = "crate")] +pub fn extname(app: AppHandle, path: String) -> Result { + let file_name = app.path().file_name(&path).ok_or(Error::NoExtension)?; + match Path::new(&file_name) + .extension() + .and_then(std::ffi::OsStr::to_str) + { + Some(p) => Ok(p.to_string()), + None => Err(Error::NoExtension), + } +} + +#[command(root = "crate")] +pub fn basename(app: AppHandle, path: &str, ext: Option<&str>) -> Result { + let file_name = app.path().file_name(path); + match file_name { + Some(p) => { + let maybe_stripped = if let Some(ext) = ext { + p.strip_suffix(ext).unwrap_or(&p).to_string() + } else { + p + }; + Ok(maybe_stripped) + } + None => Err(Error::NoBasename), + } +} + +#[command(root = "crate")] +pub fn is_absolute(path: String) -> bool { + Path::new(&path).is_absolute() +} + +#[derive(Template)] +#[default_template("./init.js")] +struct InitJavascript { + sep: &'static str, + delimiter: &'static str, +} + +/// Initializes the plugin. +pub(crate) fn init() -> TauriPlugin { + #[cfg(windows)] + let (sep, delimiter) = ("\\", ";"); + #[cfg(not(windows))] + let (sep, delimiter) = ("/", ":"); + + let init_js = InitJavascript { sep, delimiter } + .render_default(&Default::default()) + // this will never fail with the above sep and delimiter values + .unwrap(); + + Builder::new("path") + .invoke_handler(crate::generate_handler![ + #![plugin(path)] + resolve_directory, + resolve, + normalize, + join, + dirname, + extname, + basename, + is_absolute + ]) + .js_init_script(init_js.to_string()) + .setup(|app, _api| { + #[cfg(target_os = "android")] + { + let handle = _api.register_android_plugin("app.tauri", "PathPlugin")?; + app.manage(PathResolver(handle)); + } + + #[cfg(not(target_os = "android"))] + { + app.manage(PathResolver(app.clone())); + } + + Ok(()) + }) + .build() +} + +#[cfg(test)] +mod tests { + use crate::test::mock_app; + + #[test] + fn basename() { + let app = mock_app(); + let path = "/path/to/some-json-file.json"; + assert_eq!( + super::basename(app.handle().clone(), path, Some(".json")).unwrap(), + "some-json-file" + ); + + let path = "/path/to/some-json-file.json"; + assert_eq!( + super::basename(app.handle().clone(), path, Some("json")).unwrap(), + "some-json-file." + ); + + let path = "/path/to/some-json-file.html.json"; + assert_eq!( + super::basename(app.handle().clone(), path, Some(".json")).unwrap(), + "some-json-file.html" + ); + + let path = "/path/to/some-json-file.json.json"; + assert_eq!( + super::basename(app.handle().clone(), path, Some(".json")).unwrap(), + "some-json-file.json" + ); + + let path = "/path/to/some-json-file.json.html"; + assert_eq!( + super::basename(app.handle().clone(), path, Some(".json")).unwrap(), + "some-json-file.json.html" + ); + } + + #[test] + fn join() { + fn check(paths: Vec<&str>, expected_unix: &str, expected_windows: &str) { + let expected = if cfg!(windows) { + expected_windows + } else { + expected_unix + }; + let paths = paths.into_iter().map(String::from).collect(); + assert_eq!(super::join(paths), expected); + } + + check(vec![""], ".", "."); + check(vec!["", ""], ".", "."); + check(vec!["a"], "a", "a"); + check(vec!["", "a"], "a", "a"); + check(vec!["a", "b"], "a/b", r"a\b"); + check(vec!["a", "", "b"], "a/b", r"a\b"); + check(vec!["a", "/b", "c"], "a/b/c", r"a\b\c"); + check(vec!["a", "b/c", "d"], "a/b/c/d", r"a\b\c\d"); + check(vec!["a/", "b"], "a/b", r"a\b"); + } + + #[test] + fn normalize() { + assert_eq!(super::normalize("".into()), "."); + assert_eq!(super::normalize(".".into()), "."); + assert_eq!(super::normalize("..".into()), ".."); + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/pattern.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/pattern.rs new file mode 100644 index 000000000..c4f73f12d --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/pattern.rs @@ -0,0 +1,87 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#[cfg(feature = "isolation")] +use std::sync::Arc; + +use serde::Serialize; +use serialize_to_javascript::{Template, default_template}; + +/// The domain of the isolation iframe source. +#[cfg(feature = "isolation")] +pub const ISOLATION_IFRAME_SRC_DOMAIN: &str = "localhost"; + +/// An application pattern. +#[derive(Debug)] +pub enum Pattern { + /// The brownfield pattern. + Brownfield, + /// Isolation pattern. Recommended for security purposes. + #[cfg(feature = "isolation")] + Isolation { + /// The HTML served on `isolation://index.html`. + assets: Arc, + + /// The schema used for the isolation frames. + schema: String, + + /// A random string used to ensure that the message went through the isolation frame. + /// + /// This should be regenerated at runtime. + key: String, + + /// Cryptographically secure keys + crypto_keys: Box, + }, +} + +/// The shape of the JavaScript Pattern config +#[derive(Debug, Serialize)] +#[serde(rename_all = "lowercase", tag = "pattern")] +pub(crate) enum PatternObject { + /// Brownfield pattern. + Brownfield, + /// Isolation pattern. Recommended for security purposes. + #[cfg(feature = "isolation")] + Isolation { + /// Which `IsolationSide` this `PatternObject` is getting injected into + side: IsolationSide, + }, +} + +impl From<&Pattern> for PatternObject { + fn from(pattern: &Pattern) -> Self { + match pattern { + Pattern::Brownfield => Self::Brownfield, + #[cfg(feature = "isolation")] + Pattern::Isolation { .. } => Self::Isolation { + side: IsolationSide::default(), + }, + } + } +} + +/// Where the JavaScript is injected to +#[cfg(feature = "isolation")] +#[derive(Default, Debug, Serialize)] +#[serde(rename_all = "lowercase")] +pub(crate) enum IsolationSide { + /// Original frame, the Brownfield application + #[default] + Original, + /// Secure frame, the isolation security application + #[allow(dead_code)] + Secure, +} + +#[derive(Template)] +#[default_template("../scripts/pattern.js")] +pub(crate) struct PatternJavascript { + pub(crate) pattern: PatternObject, +} + +#[cfg(feature = "isolation")] +pub(crate) fn format_real_schema(schema: &str, https: bool) -> String { + format!("{}/", crate::protocol::origin(schema, https)) +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/plugin.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/plugin.rs new file mode 100644 index 000000000..680192863 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/plugin.rs @@ -0,0 +1,1062 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! The Tauri plugin extension to expand Tauri functionality. + +use crate::{ + AppHandle, Error, RunEvent, Runtime, UriSchemeContext, Webview, Window, + app::UriSchemeResponder, + ipc::{Invoke, InvokeHandler, ScopeObject, ScopeValue}, + manager::webview::UriSchemeProtocol, + utils::config::PluginConfig, + webview::PageLoadPayload, +}; +use serde::{ + Serialize, Serializer, + de::{Deserialize, DeserializeOwned, Deserializer, Error as DeError}, +}; +use serde_json::Value as JsonValue; +use tauri_macros::default_runtime; +use tauri_runtime::webview::InitializationScript; +use thiserror::Error; +use url::Url; + +use std::{ + borrow::Cow, + collections::HashMap, + fmt::{self, Debug}, + sync::Arc, +}; + +/// Mobile APIs. +#[cfg(mobile)] +pub mod mobile; + +/// The plugin interface. +pub trait Plugin: Send { + /// The plugin name. Used as key on the plugin config object. + fn name(&self) -> &'static str; + + /// Initializes the plugin. + #[allow(unused_variables)] + fn initialize( + &mut self, + app: &AppHandle, + config: JsonValue, + ) -> Result<(), Box> { + Ok(()) + } + + /// Add the provided JavaScript to a list of scripts that should be run after the global object has been created, + /// but before the HTML document has been parsed and before any other script included by the HTML document is run. + /// + /// The script is wrapped into its own context with `(function () { /* your script here */ })();`, + /// so global variables must be assigned to `window` instead of implicitly declared. + /// + /// This is executed only on the main frame. + /// If you only want to run it in all frames, use [`Plugin::initialization_script_2`] to set that to false. + /// + /// ## Platform-specific + /// + /// - **Windows:** scripts are always added to subframes. + /// - **Android:** When [addDocumentStartJavaScript] is not supported, + /// we prepend initialization scripts to each HTML head (implementation only supported on custom protocol URLs). + /// For remote URLs, we use [onPageStarted] which is not guaranteed to run before other scripts. + /// + /// [addDocumentStartJavaScript]: https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E) + /// [onPageStarted]: https://developer.android.com/reference/android/webkit/WebViewClient#onPageStarted(android.webkit.WebView,%20java.lang.String,%20android.graphics.Bitmap) + fn initialization_script(&self) -> Option { + None + } + + // TODO: Change `initialization_script` to this in v3 + /// Same as [`Plugin::initialization_script`] but returns an [`InitializationScript`] instead + /// We plan to replace [`Plugin::initialization_script`] with this signature in v3 + fn initialization_script_2(&self) -> Option { + self + .initialization_script() + .map(|script| InitializationScript { + script, + for_main_frame_only: true, + }) + } + + /// Callback invoked when the window is created. + #[allow(unused_variables)] + fn window_created(&mut self, window: Window) {} + + /// Callback invoked when the webview is created. + #[allow(unused_variables)] + fn webview_created(&mut self, webview: Webview) {} + + /// Callback invoked when webview tries to navigate to the given Url. Returning false cancels navigation. + #[allow(unused_variables)] + fn on_navigation(&mut self, webview: &Webview, url: &Url) -> bool { + true + } + + /// Callback invoked when the webview performs a navigation to a page. + #[allow(unused_variables)] + fn on_page_load(&mut self, webview: &Webview, payload: &PageLoadPayload<'_>) {} + + /// Callback invoked when the event loop receives a new event. + #[allow(unused_variables)] + fn on_event(&mut self, app: &AppHandle, event: &RunEvent) {} + + // TODO: Change this to `run_invoke_handler` in v3 + /// Extend commands to [`crate::Builder::invoke_handler`]. + #[allow(unused_variables)] + fn extend_api(&mut self, invoke: Invoke) -> bool { + false + } +} + +type SetupHook = + dyn FnOnce(&AppHandle, PluginApi) -> Result<(), Box> + Send; +type OnWindowReady = dyn FnMut(Window) + Send; +type OnWebviewReady = dyn FnMut(Webview) + Send; +type OnEvent = dyn FnMut(&AppHandle, &RunEvent) + Send; +type OnNavigation = dyn Fn(&Webview, &Url) -> bool + Send; +type OnPageLoad = dyn FnMut(&Webview, &PageLoadPayload<'_>) + Send; +type OnDrop = dyn FnOnce(AppHandle) + Send; + +/// A handle to a plugin. +#[derive(Debug)] +#[allow(dead_code)] +pub struct PluginHandle { + name: &'static str, + handle: AppHandle, +} + +impl Clone for PluginHandle { + fn clone(&self) -> Self { + Self { + name: self.name, + handle: self.handle.clone(), + } + } +} + +impl PluginHandle { + /// Returns the application handle. + pub fn app(&self) -> &AppHandle { + &self.handle + } +} + +/// Api exposed to the plugin setup hook. +#[derive(Clone)] +#[allow(dead_code)] +pub struct PluginApi { + handle: AppHandle, + name: &'static str, + raw_config: Arc, + config: C, +} + +impl PluginApi { + /// Returns the plugin configuration. + pub fn config(&self) -> &C { + &self.config + } + + /// Returns the application handle. + pub fn app(&self) -> &AppHandle { + &self.handle + } + + /// Gets the global scope defined on the permissions that are part of the app ACL. + pub fn scope(&self) -> crate::Result> { + self + .handle + .manager + .runtime_authority + .lock() + .unwrap() + .scope_manager + .get_global_scope_typed(&self.handle, self.name) + } +} + +/// Errors that can happen during [`Builder`]. +#[derive(Debug, Clone, Hash, PartialEq, Error)] +#[non_exhaustive] +pub enum BuilderError { + /// Plugin attempted to use a reserved name. + #[error("plugin uses reserved name: {0}")] + ReservedName(String), +} + +const RESERVED_PLUGIN_NAMES: &[&str] = &["core", "tauri"]; + +/// Builds a [`TauriPlugin`]. +/// +/// This Builder offers a more concise way to construct Tauri plugins than implementing the Plugin trait directly. +/// +/// # Conventions +/// +/// When using the Builder Pattern it is encouraged to export a function called `init` that constructs and returns the plugin. +/// While plugin authors can provide every possible way to construct a plugin, +/// sticking to the `init` function convention helps users to quickly identify the correct function to call. +/// +/// ```rust +/// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; +/// +/// pub fn init() -> TauriPlugin { +/// Builder::new("example") +/// .build() +/// } +/// ``` +/// +/// When plugins expose more complex configuration options, it can be helpful to provide a Builder instead: +/// +/// ```rust +/// use tauri::{plugin::{Builder as PluginBuilder, TauriPlugin}, Runtime}; +/// +/// pub struct Builder { +/// option_a: String, +/// option_b: String, +/// option_c: bool +/// } +/// +/// impl Default for Builder { +/// fn default() -> Self { +/// Self { +/// option_a: "foo".to_string(), +/// option_b: "bar".to_string(), +/// option_c: false +/// } +/// } +/// } +/// +/// impl Builder { +/// pub fn new() -> Self { +/// Default::default() +/// } +/// +/// pub fn option_a(mut self, option_a: String) -> Self { +/// self.option_a = option_a; +/// self +/// } +/// +/// pub fn option_b(mut self, option_b: String) -> Self { +/// self.option_b = option_b; +/// self +/// } +/// +/// pub fn option_c(mut self, option_c: bool) -> Self { +/// self.option_c = option_c; +/// self +/// } +/// +/// pub fn build(self) -> TauriPlugin { +/// PluginBuilder::new("example") +/// .setup(move |app_handle, api| { +/// // use the options here to do stuff +/// println!("a: {}, b: {}, c: {}", self.option_a, self.option_b, self.option_c); +/// +/// Ok(()) +/// }) +/// .build() +/// } +/// } +/// ``` +pub struct Builder { + name: &'static str, + invoke_handler: Box>, + setup: Option>>, + js_init_script: Option, + on_navigation: Box>, + on_page_load: Box>, + on_window_ready: Box>, + on_webview_ready: Box>, + on_event: Box>, + on_drop: Option>>, + uri_scheme_protocols: HashMap>>, +} + +impl Builder { + /// Creates a new Plugin builder. + pub fn new(name: &'static str) -> Self { + Self { + name, + setup: None, + js_init_script: None, + invoke_handler: Box::new(|_| false), + on_navigation: Box::new(|_, _| true), + on_page_load: Box::new(|_, _| ()), + on_window_ready: Box::new(|_| ()), + on_webview_ready: Box::new(|_| ()), + on_event: Box::new(|_, _| ()), + on_drop: None, + uri_scheme_protocols: Default::default(), + } + } + + /// Defines the JS message handler callback. + /// It is recommended you use the [tauri::generate_handler] to generate the input to this method, as the input type is not considered stable yet. + /// + /// # Examples + /// + /// ```rust + /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; + /// + /// #[tauri::command] + /// async fn foobar(app: tauri::AppHandle, window: tauri::Window) -> Result<(), String> { + /// println!("foobar"); + /// + /// Ok(()) + /// } + /// + /// fn init() -> TauriPlugin { + /// Builder::new("example") + /// .invoke_handler(tauri::generate_handler![foobar]) + /// .build() + /// } + /// + /// ``` + /// [tauri::generate_handler]: ../macro.generate_handler.html + #[must_use] + pub fn invoke_handler(mut self, invoke_handler: F) -> Self + where + F: Fn(Invoke) -> bool + Send + Sync + 'static, + { + self.invoke_handler = Box::new(invoke_handler); + self + } + + /// Sets the provided JavaScript to be run after the global object has been created, + /// but before the HTML document has been parsed and before any other script included by the HTML document is run. + /// + /// The script is wrapped into its own context with `(function () { /* your script here */ })();`, + /// so global variables must be assigned to `window` instead of implicitly declared. + /// + /// Note that calling this function multiple times overrides previous values. + /// + /// This is executed only on the main frame. + /// If you only want to run it in all frames, use [`Self::js_init_script_on_all_frames`] instead. + /// + /// ## Platform-specific + /// + /// - **Windows:** scripts are always added to subframes. + /// - **Android:** When [addDocumentStartJavaScript] is not supported, + /// we prepend initialization scripts to each HTML head (implementation only supported on custom protocol URLs). + /// For remote URLs, we use [onPageStarted] which is not guaranteed to run before other scripts. + /// + /// [addDocumentStartJavaScript]: https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E) + /// [onPageStarted]: https://developer.android.com/reference/android/webkit/WebViewClient#onPageStarted(android.webkit.WebView,%20java.lang.String,%20android.graphics.Bitmap) + /// + /// # Examples + /// + /// ```rust + /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; + /// + /// const INIT_SCRIPT: &str = r#" + /// if (window.location.origin === 'https://tauri.app') { + /// console.log("hello world from js init script"); + /// + /// window.__MY_CUSTOM_PROPERTY__ = { foo: 'bar' }; + /// } + /// "#; + /// + /// fn init() -> TauriPlugin { + /// Builder::new("example") + /// .js_init_script(INIT_SCRIPT) + /// .build() + /// } + /// ``` + #[must_use] + // TODO: Rename to `initialization_script` in v3 + pub fn js_init_script(mut self, js_init_script: impl Into) -> Self { + self.js_init_script = Some(InitializationScript { + script: js_init_script.into(), + for_main_frame_only: true, + }); + self + } + + /// Sets the provided JavaScript to be run after the global object has been created, + /// but before the HTML document has been parsed and before any other script included by the HTML document is run. + /// + /// Since it runs on all top-level document and child frame page navigations, + /// it's recommended to check the `window.location` to guard your script from running on unexpected origins. + /// + /// Note that calling this function multiple times overrides previous values. + /// + /// This is executed on all frames, main frame and also sub frames. + /// If you only want to run it in the main frame, use [`Self::js_init_script`] instead. + /// + /// ## Platform-specific + /// + /// - **Windows:** scripts are always added to subframes. + /// - **Android:** When [addDocumentStartJavaScript] is not supported, + /// we prepend initialization scripts to each HTML head (implementation only supported on custom protocol URLs). + /// For remote URLs, we use [onPageStarted] which is not guaranteed to run before other scripts. + /// + /// [addDocumentStartJavaScript]: https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E) + /// [onPageStarted]: https://developer.android.com/reference/android/webkit/WebViewClient#onPageStarted(android.webkit.WebView,%20java.lang.String,%20android.graphics.Bitmap) + #[must_use] + pub fn js_init_script_on_all_frames(mut self, js_init_script: impl Into) -> Self { + self.js_init_script = Some(InitializationScript { + script: js_init_script.into(), + for_main_frame_only: false, + }); + self + } + + /// Define a closure that runs when the plugin is registered. + /// + /// # Examples + /// + /// ```rust + /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime, Manager}; + /// use std::path::PathBuf; + /// + /// #[derive(Debug, Default)] + /// struct PluginState { + /// dir: Option + /// } + /// + /// fn init() -> TauriPlugin { + /// Builder::new("example") + /// .setup(|app, api| { + /// app.manage(PluginState::default()); + /// + /// Ok(()) + /// }) + /// .build() + /// } + /// ``` + #[must_use] + pub fn setup(mut self, setup: F) -> Self + where + F: FnOnce(&AppHandle, PluginApi) -> Result<(), Box> + + Send + + 'static, + { + self.setup.replace(Box::new(setup)); + self + } + + /// Callback invoked when the webview tries to navigate to a URL. Returning false cancels the navigation. + /// + /// #Example + /// + /// ``` + /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; + /// + /// fn init() -> TauriPlugin { + /// Builder::new("example") + /// .on_navigation(|webview, url| { + /// // allow the production URL or localhost on dev + /// url.scheme() == "tauri" || (cfg!(dev) && url.host_str() == Some("localhost")) + /// }) + /// .build() + /// } + /// ``` + #[must_use] + pub fn on_navigation(mut self, on_navigation: F) -> Self + where + F: Fn(&Webview, &Url) -> bool + Send + 'static, + { + self.on_navigation = Box::new(on_navigation); + self + } + + /// Callback invoked when the webview performs a navigation to a page. + /// + /// # Examples + /// + /// ```rust + /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; + /// + /// fn init() -> TauriPlugin { + /// Builder::new("example") + /// .on_page_load(|webview, payload| { + /// println!("{:?} URL {} in webview {}", payload.event(), payload.url(), webview.label()); + /// }) + /// .build() + /// } + /// ``` + #[must_use] + pub fn on_page_load(mut self, on_page_load: F) -> Self + where + F: FnMut(&Webview, &PageLoadPayload<'_>) + Send + 'static, + { + self.on_page_load = Box::new(on_page_load); + self + } + + /// Callback invoked when the window is created. + /// + /// # Examples + /// + /// ```rust + /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; + /// + /// fn init() -> TauriPlugin { + /// Builder::new("example") + /// .on_window_ready(|window| { + /// println!("created window {}", window.label()); + /// }) + /// .build() + /// } + /// ``` + #[must_use] + pub fn on_window_ready(mut self, on_window_ready: F) -> Self + where + F: FnMut(Window) + Send + 'static, + { + self.on_window_ready = Box::new(on_window_ready); + self + } + + /// Callback invoked when the webview is created. + /// + /// # Examples + /// + /// ```rust + /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; + /// + /// fn init() -> TauriPlugin { + /// Builder::new("example") + /// .on_webview_ready(|webview| { + /// println!("created webview {}", webview.label()); + /// }) + /// .build() + /// } + /// ``` + #[must_use] + pub fn on_webview_ready(mut self, on_webview_ready: F) -> Self + where + F: FnMut(Webview) + Send + 'static, + { + self.on_webview_ready = Box::new(on_webview_ready); + self + } + + /// Callback invoked when the event loop receives a new event. + /// + /// # Examples + /// + /// ```rust + /// use tauri::{plugin::{Builder, TauriPlugin}, RunEvent, Runtime}; + /// + /// fn init() -> TauriPlugin { + /// Builder::new("example") + /// .on_event(|app_handle, event| { + /// match event { + /// RunEvent::ExitRequested { api, .. } => { + /// // Prevents the app from exiting. + /// // This will cause the core thread to continue running in the background even without any open windows. + /// api.prevent_exit(); + /// } + /// // Ignore all other cases. + /// _ => {} + /// } + /// }) + /// .build() + /// } + /// ``` + #[must_use] + pub fn on_event(mut self, on_event: F) -> Self + where + F: FnMut(&AppHandle, &RunEvent) + Send + 'static, + { + self.on_event = Box::new(on_event); + self + } + + /// Callback invoked when the plugin is dropped. + /// + /// # Examples + /// + /// ```rust + /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; + /// + /// fn init() -> TauriPlugin { + /// Builder::new("example") + /// .on_drop(|app| { + /// println!("plugin has been dropped and is no longer running"); + /// // you can run cleanup logic here + /// }) + /// .build() + /// } + /// ``` + #[must_use] + pub fn on_drop(mut self, on_drop: F) -> Self + where + F: FnOnce(AppHandle) + Send + 'static, + { + self.on_drop.replace(Box::new(on_drop)); + self + } + + /// Registers a URI scheme protocol available to all webviews. + /// + /// Leverages [setURLSchemeHandler](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/2875766-seturlschemehandler) on macOS, + /// [AddWebResourceRequestedFilter](https://docs.microsoft.com/en-us/dotnet/api/microsoft.web.webview2.core.corewebview2.addwebresourcerequestedfilter?view=webview2-dotnet-1.0.774.44) on Windows + /// and [webkit-web-context-register-uri-scheme](https://webkitgtk.org/reference/webkit2gtk/stable/WebKitWebContext.html#webkit-web-context-register-uri-scheme) on Linux. + /// + /// # Known limitations + /// + /// URI scheme protocols are registered when the webview is created. Due to this limitation, if the plugin is registered after a webview has been created, this protocol won't be available. + /// + /// # Arguments + /// + /// * `uri_scheme` The URI scheme to register, such as `example`. + /// * `protocol` the protocol associated with the given URI scheme. It's a function that takes an URL such as `example://localhost/asset.css`. + /// + /// # Examples + /// + /// ```rust + /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; + /// + /// fn init() -> TauriPlugin { + /// Builder::new("myplugin") + /// .register_uri_scheme_protocol("myscheme", |_ctx, req| { + /// http::Response::builder().body(Vec::new()).unwrap() + /// }) + /// .build() + /// } + /// ``` + /// + /// # Warning + /// + /// Pages loaded from a custom protocol will have a different Origin on different platforms. + /// Servers which enforce CORS will need to add the exact same Origin header (or `*`) in `Access-Control-Allow-Origin` + /// if you wish to send requests with native `fetch` and `XmlHttpRequest` APIs. Here are the + /// different Origin headers across platforms: + /// + /// - macOS, iOS and Linux: `://localhost/` (so it will be `my-scheme://localhost/path/to/page). + /// - Windows and Android: `http://.localhost/` by default (so it will be `http://my-scheme.localhost/path/to/page`). + /// To use `https` instead of `http`, use [`super::webview::WebviewBuilder::use_https_scheme`]. + #[must_use] + pub fn register_uri_scheme_protocol< + N: Into, + T: Into>, + H: Fn(UriSchemeContext<'_, R>, http::Request>) -> http::Response + + Send + + Sync + + 'static, + >( + mut self, + uri_scheme: N, + protocol_handler: H, + ) -> Self { + self.uri_scheme_protocols.insert( + uri_scheme.into(), + Arc::new(UriSchemeProtocol { + handler: Box::new(move |ctx, request, responder| { + responder.respond(protocol_handler(ctx, request)) + }), + }), + ); + self + } + + /// Similar to [`Self::register_uri_scheme_protocol`] but with an asynchronous responder that allows you + /// to process the request in a separate thread and respond asynchronously. + /// + /// # Arguments + /// + /// * `uri_scheme` The URI scheme to register, such as `example`. + /// * `protocol` the protocol associated with the given URI scheme. It's a function that takes an URL such as `example://localhost/asset.css`. + /// + /// # Examples + /// + /// ```rust + /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; + /// + /// fn init() -> TauriPlugin { + /// Builder::new("myplugin") + /// .register_asynchronous_uri_scheme_protocol("app-files", |_ctx, request, responder| { + /// // skip leading `/` + /// let path = request.uri().path()[1..].to_string(); + /// std::thread::spawn(move || { + /// if let Ok(data) = std::fs::read(path) { + /// responder.respond( + /// http::Response::builder() + /// .body(data) + /// .unwrap() + /// ); + /// } else { + /// responder.respond( + /// http::Response::builder() + /// .status(http::StatusCode::BAD_REQUEST) + /// .header(http::header::CONTENT_TYPE, mime::TEXT_PLAIN.essence_str()) + /// .body("failed to read file".as_bytes().to_vec()) + /// .unwrap() + /// ); + /// } + /// }); + /// }) + /// .build() + /// } + /// ``` + /// + /// # Warning + /// + /// Pages loaded from a custom protocol will have a different Origin on different platforms. + /// Servers which enforce CORS will need to add the exact same Origin header (or `*`) in `Access-Control-Allow-Origin` + /// if you wish to send requests with native `fetch` and `XmlHttpRequest` APIs. Here are the + /// different Origin headers across platforms: + /// + /// - macOS, iOS and Linux: `://localhost/` (so it will be `my-scheme://localhost/path/to/page). + /// - Windows and Android: `http://.localhost/` by default (so it will be `http://my-scheme.localhost/path/to/page`). + /// To use `https` instead of `http`, use [`super::webview::WebviewBuilder::use_https_scheme`]. + #[must_use] + pub fn register_asynchronous_uri_scheme_protocol< + N: Into, + H: Fn(UriSchemeContext<'_, R>, http::Request>, UriSchemeResponder) + Send + Sync + 'static, + >( + mut self, + uri_scheme: N, + protocol_handler: H, + ) -> Self { + self.uri_scheme_protocols.insert( + uri_scheme.into(), + Arc::new(UriSchemeProtocol { + handler: Box::new(protocol_handler), + }), + ); + self + } + + /// Builds the [`TauriPlugin`]. + pub fn try_build(self) -> Result, BuilderError> { + if let Some(&reserved) = RESERVED_PLUGIN_NAMES.iter().find(|&r| r == &self.name) { + return Err(BuilderError::ReservedName(reserved.into())); + } + + Ok(TauriPlugin { + name: self.name, + app: None, + invoke_handler: self.invoke_handler, + setup: self.setup, + js_init_script: self.js_init_script, + on_navigation: self.on_navigation, + on_page_load: self.on_page_load, + on_window_ready: self.on_window_ready, + on_webview_ready: self.on_webview_ready, + on_event: self.on_event, + on_drop: self.on_drop, + uri_scheme_protocols: self.uri_scheme_protocols, + }) + } + + /// Builds the [`TauriPlugin`]. + /// + /// # Panics + /// + /// If the builder returns an error during [`Self::try_build`], then this method will panic. + pub fn build(self) -> TauriPlugin { + self.try_build().expect("valid plugin") + } +} + +/// Plugin struct that is returned by the [`Builder`]. Should only be constructed through the builder. +pub struct TauriPlugin { + name: &'static str, + app: Option>, + invoke_handler: Box>, + setup: Option>>, + js_init_script: Option, + on_navigation: Box>, + on_page_load: Box>, + on_window_ready: Box>, + on_webview_ready: Box>, + on_event: Box>, + on_drop: Option>>, + uri_scheme_protocols: HashMap>>, +} + +impl Drop for TauriPlugin { + fn drop(&mut self) { + if let (Some(on_drop), Some(app)) = (self.on_drop.take(), self.app.take()) { + on_drop(app); + } + } +} + +impl Plugin for TauriPlugin { + fn name(&self) -> &'static str { + self.name + } + + fn initialize( + &mut self, + app: &AppHandle, + config: JsonValue, + ) -> Result<(), Box> { + self.app.replace(app.clone()); + if let Some(s) = self.setup.take() { + (s)( + app, + PluginApi { + name: self.name, + handle: app.clone(), + raw_config: Arc::new(config.clone()), + config: serde_json::from_value(config).map_err(|err| { + format!( + "Error deserializing 'plugins.{}' within your Tauri configuration: {err}", + self.name + ) + })?, + }, + )?; + } + + for (uri_scheme, protocol) in &self.uri_scheme_protocols { + app + .manager + .webview + .register_uri_scheme_protocol(uri_scheme, protocol.clone()) + } + Ok(()) + } + + fn initialization_script(&self) -> Option { + self + .js_init_script + .clone() + .map(|initialization_script| initialization_script.script) + } + + fn initialization_script_2(&self) -> Option { + self.js_init_script.clone() + } + + fn window_created(&mut self, window: Window) { + (self.on_window_ready)(window) + } + + fn webview_created(&mut self, webview: Webview) { + (self.on_webview_ready)(webview) + } + + fn on_navigation(&mut self, webview: &Webview, url: &Url) -> bool { + (self.on_navigation)(webview, url) + } + + fn on_page_load(&mut self, webview: &Webview, payload: &PageLoadPayload<'_>) { + (self.on_page_load)(webview, payload) + } + + fn on_event(&mut self, app: &AppHandle, event: &RunEvent) { + (self.on_event)(app, event) + } + + fn extend_api(&mut self, invoke: Invoke) -> bool { + (self.invoke_handler)(invoke) + } +} + +/// Plugin collection type. +#[default_runtime(crate::Wry, wry)] +pub(crate) struct PluginStore { + store: Vec>>, +} + +impl fmt::Debug for PluginStore { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let plugins: Vec<&str> = self.store.iter().map(|plugins| plugins.name()).collect(); + f.debug_struct("PluginStore") + .field("plugins", &plugins) + .finish() + } +} + +impl Default for PluginStore { + fn default() -> Self { + Self { store: Vec::new() } + } +} + +impl PluginStore { + /// Adds a plugin to the store. + /// + /// Returns `true` if a plugin with the same name is already in the store. + pub fn register(&mut self, plugin: Box>) -> bool { + let len = self.store.len(); + self.store.retain(|p| p.name() != plugin.name()); + let result = len != self.store.len(); + self.store.push(plugin); + result + } + + /// Removes the plugin with the given name from the store. + pub fn unregister(&mut self, plugin: &str) -> bool { + let len = self.store.len(); + self.store.retain(|p| p.name() != plugin); + len != self.store.len() + } + + /// Initializes the given plugin. + pub(crate) fn initialize( + &self, + plugin: &mut Box>, + app: &AppHandle, + config: &PluginConfig, + ) -> crate::Result<()> { + initialize(plugin, app, config) + } + + /// Initializes all plugins in the store. + pub(crate) fn initialize_all( + &mut self, + app: &AppHandle, + config: &PluginConfig, + ) -> crate::Result<()> { + self + .store + .iter_mut() + .try_for_each(|plugin| initialize(plugin, app, config)) + } + + /// Generates an initialization script from all plugins in the store. + pub(crate) fn initialization_script(&self) -> Vec { + self + .store + .iter() + .filter_map(|p| p.initialization_script_2()) + .map( + |InitializationScript { + script, + for_main_frame_only, + }| InitializationScript { + script: format!("(function () {{ {script} }})();"), + for_main_frame_only, + }, + ) + .collect() + } + + /// Runs the created hook for all plugins in the store. + pub(crate) fn window_created(&mut self, window: Window) { + self.store.iter_mut().for_each(|plugin| { + #[cfg(feature = "tracing")] + let _span = tracing::trace_span!("plugin::hooks::created", name = plugin.name()).entered(); + plugin.window_created(window.clone()) + }) + } + + /// Runs the webview created hook for all plugins in the store. + pub(crate) fn webview_created(&mut self, webview: Webview) { + self + .store + .iter_mut() + .for_each(|plugin| plugin.webview_created(webview.clone())) + } + + pub(crate) fn on_navigation(&mut self, webview: &Webview, url: &Url) -> bool { + for plugin in self.store.iter_mut() { + #[cfg(feature = "tracing")] + let _span = + tracing::trace_span!("plugin::hooks::on_navigation", name = plugin.name()).entered(); + if !plugin.on_navigation(webview, url) { + return false; + } + } + true + } + + /// Runs the on_page_load hook for all plugins in the store. + pub(crate) fn on_page_load(&mut self, webview: &Webview, payload: &PageLoadPayload<'_>) { + self.store.iter_mut().for_each(|plugin| { + #[cfg(feature = "tracing")] + let _span = + tracing::trace_span!("plugin::hooks::on_page_load", name = plugin.name()).entered(); + plugin.on_page_load(webview, payload) + }) + } + + /// Runs the on_event hook for all plugins in the store. + pub(crate) fn on_event(&mut self, app: &AppHandle, event: &RunEvent) { + self + .store + .iter_mut() + .for_each(|plugin| plugin.on_event(app, event)) + } + + /// Runs the plugin [`Plugin::extend_api`] hook if it exists. Returns whether the invoke message was handled or not. + /// + /// The message is not handled when the plugin exists **and** the command does not. + pub(crate) fn run_invoke_handler(&mut self, plugin: &str, invoke: Invoke) -> bool { + for p in self.store.iter_mut() { + if p.name() == plugin { + #[cfg(feature = "tracing")] + let _span = tracing::trace_span!("plugin::hooks::ipc", name = plugin).entered(); + return p.extend_api(invoke); + } + } + invoke.resolver.reject(format!("plugin {plugin} not found")); + true + } +} + +#[cfg_attr(feature = "tracing", tracing::instrument(name = "plugin::hooks::initialize", skip(plugin, app), fields(name = plugin.name())))] +fn initialize( + plugin: &mut Box>, + app: &AppHandle, + config: &PluginConfig, +) -> crate::Result<()> { + plugin + .initialize( + app, + config.0.get(plugin.name()).cloned().unwrap_or_default(), + ) + .map_err(|e| Error::PluginInitialization(plugin.name().to_string(), e.to_string())) +} + +/// Permission state. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[cfg_attr(feature = "specta", derive(specta::Type))] +pub enum PermissionState { + /// Permission access has been granted. + Granted, + /// Permission access has been denied. + Denied, + /// Permission must be requested. + #[default] + Prompt, + /// Permission must be requested, but you must explain to the user why your app needs that permission. **Android only**. + PromptWithRationale, +} + +impl std::fmt::Display for PermissionState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Granted => write!(f, "granted"), + Self::Denied => write!(f, "denied"), + Self::Prompt => write!(f, "prompt"), + Self::PromptWithRationale => write!(f, "prompt-with-rationale"), + } + } +} + +impl Serialize for PermissionState { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} + +impl<'de> Deserialize<'de> for PermissionState { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let s = ::deserialize(deserializer)?; + match s.to_lowercase().as_str() { + "granted" => Ok(Self::Granted), + "denied" => Ok(Self::Denied), + "prompt" => Ok(Self::Prompt), + "prompt-with-rationale" => Ok(Self::PromptWithRationale), + _ => Err(DeError::custom(format!("unknown permission state '{s}'"))), + } + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/plugin/mobile.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/plugin/mobile.rs new file mode 100644 index 000000000..c9187b002 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/plugin/mobile.rs @@ -0,0 +1,511 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use super::{PluginApi, PluginHandle}; + +use crate::{AppHandle, Runtime, ipc::Channel}; +#[cfg(target_os = "android")] +use crate::{ + runtime::RuntimeHandle, + sealed::{ManagerBase, RuntimeOrDispatch}, +}; + +#[cfg(mobile)] +use std::sync::atomic::{AtomicI32, Ordering}; +#[cfg(mobile)] +use tokio::sync::oneshot; + +use serde::{Serialize, de::DeserializeOwned}; + +use std::{ + collections::HashMap, + fmt, + sync::{Mutex, OnceLock, mpsc::channel}, +}; + +type PluginResponse = Result; + +type PendingPluginCallHandler = Box; + +#[cfg(mobile)] +static PENDING_PLUGIN_CALLS_ID: AtomicI32 = AtomicI32::new(0); +static PENDING_PLUGIN_CALLS: OnceLock>> = + OnceLock::new(); +static CHANNELS: OnceLock>>> = OnceLock::new(); + +/// Possible errors when invoking a plugin. +#[derive(Debug, thiserror::Error)] +pub enum PluginInvokeError { + /// Failed to reach platform webview handle. + #[error("the webview is unreachable")] + UnreachableWebview, + /// JNI error. + #[cfg(target_os = "android")] + #[error("jni error: {0}")] + Jni(#[from] jni::errors::Error), + /// Error returned from direct mobile plugin invoke. + #[error(transparent)] + InvokeRejected(#[from] ErrorResponse), + /// Failed to deserialize response. + #[error("failed to deserialize response: {0}")] + CannotDeserializeResponse(serde_json::Error), + /// Failed to serialize request payload. + #[error("failed to serialize payload: {0}")] + CannotSerializePayload(serde_json::Error), +} + +pub(crate) fn register_channel(channel: Channel) { + CHANNELS + .get_or_init(Default::default) + .lock() + .unwrap() + .insert(channel.id(), channel); +} + +/// Glue between Rust and the Kotlin code that sends the plugin response back. +#[cfg(target_os = "android")] +pub fn handle_android_plugin_response( + env: &mut jni::JNIEnv<'_>, + id: i32, + success: jni::objects::JString<'_>, + error: jni::objects::JString<'_>, +) { + let (payload, is_ok): (serde_json::Value, bool) = match ( + env + .is_same_object(&success, jni::objects::JObject::default()) + .unwrap_or_default(), + env + .is_same_object(&error, jni::objects::JObject::default()) + .unwrap_or_default(), + ) { + // both null + (true, true) => (serde_json::Value::Null, true), + // error null + (false, true) => ( + serde_json::from_str(env.get_string(&success).unwrap().to_str().unwrap()).unwrap(), + true, + ), + // success null + (true, false) => ( + serde_json::from_str(env.get_string(&error).unwrap().to_str().unwrap()).unwrap(), + false, + ), + // both are set - impossible in the Kotlin code + (false, false) => unreachable!(), + }; + + // Drop the lock before invoking the handler: it delivers the command response + // to the webview (which can block on the UI thread), and holding + // PENDING_PLUGIN_CALLS across that call deadlocks a concurrent `run_command`. + let handler = PENDING_PLUGIN_CALLS + .get_or_init(Default::default) + .lock() + .unwrap() + .remove(&id); + if let Some(handler) = handler { + handler(if is_ok { Ok(payload) } else { Err(payload) }); + } +} + +/// Glue between Rust and the Kotlin code that sends the channel data. +#[cfg(target_os = "android")] +pub fn send_channel_data( + env: &mut jni::JNIEnv<'_>, + channel_id: i64, + data_str: jni::objects::JString<'_>, +) { + let data: serde_json::Value = + serde_json::from_str(env.get_string(&data_str).unwrap().to_str().unwrap()).unwrap(); + + // Clone the channel out and drop the lock before send(): send() can block + // delivering to the webview, and holding CHANNELS across it deadlocks a + // concurrent channel registration/send. + let channel = CHANNELS + .get_or_init(Default::default) + .lock() + .unwrap() + .get(&(channel_id as u32)) + .cloned(); + if let Some(channel) = channel { + let _ = channel.send(data); + } +} + +/// Error response from the Kotlin and Swift backends. +#[derive(Debug, thiserror::Error, Clone, serde::Deserialize)] +pub struct ErrorResponse { + /// Error code. + pub code: Option, + /// Error message. + pub message: Option, + /// Optional error data. + #[serde(flatten)] + pub data: T, +} + +impl fmt::Display for ErrorResponse { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(code) = &self.code { + write!(f, "[{code}]")?; + if self.message.is_some() { + write!(f, " - ")?; + } + } + if let Some(message) = &self.message { + write!(f, "{message}")?; + } + Ok(()) + } +} + +impl PluginApi { + /// Registers an iOS plugin. + #[cfg(all(target_os = "ios", feature = "wry"))] + pub fn register_ios_plugin( + &self, + init_fn: unsafe fn() -> *const std::ffi::c_void, + ) -> Result, PluginInvokeError> { + if let Some(webview) = self.handle.manager.webviews().values().next() { + let (tx, rx) = channel(); + let name = self.name; + let config = self.raw_config.clone(); + webview + .with_webview(move |w| { + unsafe { + crate::ios::register_plugin( + &name.into(), + init_fn(), + &serde_json::to_string(&config).unwrap().as_str().into(), + w.inner() as _, + ) + }; + tx.send(()).unwrap(); + }) + .map_err(|_| PluginInvokeError::UnreachableWebview)?; + rx.recv().unwrap(); + } else { + unsafe { + crate::ios::register_plugin( + &self.name.into(), + init_fn(), + &serde_json::to_string(&self.raw_config) + .unwrap() + .as_str() + .into(), + std::ptr::null(), + ) + }; + } + Ok(PluginHandle { + name: self.name, + handle: self.handle.clone(), + }) + } + + /// Registers an Android plugin. + #[cfg(target_os = "android")] + pub fn register_android_plugin( + &self, + plugin_identifier: &str, + class_name: &str, + ) -> Result, PluginInvokeError> { + use jni::{JNIEnv, errors::Error as JniError, objects::JObject}; + + fn initialize_plugin( + env: &mut JNIEnv<'_>, + activity: &JObject<'_>, + webview: &JObject<'_>, + runtime_handle: &R::Handle, + plugin_name: &'static str, + plugin_class: String, + plugin_config: &serde_json::Value, + ) -> Result<(), JniError> { + // instantiate plugin + let plugin_class = runtime_handle.find_class(env, activity, plugin_class)?; + let plugin = env.new_object( + plugin_class, + "(Landroid/app/Activity;)V", + &[activity.into()], + )?; + + // load plugin + + let plugin_manager = env + .call_method( + activity, + "getPluginManager", + "()Lapp/tauri/plugin/PluginManager;", + &[], + )? + .l()?; + + let plugin_name = env.new_string(plugin_name)?; + let config = env.new_string(serde_json::to_string(plugin_config).unwrap())?; + env.call_method( + plugin_manager, + "load", + "(Landroid/webkit/WebView;Ljava/lang/String;Lapp/tauri/plugin/Plugin;Ljava/lang/String;)V", + &[ + webview.into(), + (&plugin_name).into(), + (&plugin).into(), + (&config).into(), + ], + )?; + + Ok(()) + } + + let plugin_class = format!("{}/{}", plugin_identifier.replace('.', "/"), class_name); + let plugin_name = self.name; + let plugin_config = self.raw_config.clone(); + let runtime_handle = self.handle.runtime_handle.clone(); + let (tx, rx) = channel(); + self + .handle + .runtime_handle + .run_on_android_context(move |env, activity, webview| { + let result = initialize_plugin::( + env, + activity, + webview, + &runtime_handle, + plugin_name, + plugin_class, + &plugin_config, + ); + tx.send(result).unwrap(); + }); + + rx.recv().unwrap()?; + + Ok(PluginHandle { + name: self.name, + handle: self.handle.clone(), + }) + } +} + +impl PluginHandle { + /// Executes the given mobile command. + /// This is an async optimized variant of run_mobile_plugin + pub async fn run_mobile_plugin_async( + &self, + command: impl AsRef, + payload: impl Serialize, + ) -> Result { + let (tx, rx) = oneshot::channel(); + // the closure is an FnOnce but on Android we need to clone it (error handling) + let tx = std::sync::Arc::new(std::sync::Mutex::new(Some(tx))); + run_command( + self.name, + &self.handle, + command, + serde_json::to_value(payload).map_err(PluginInvokeError::CannotSerializePayload)?, + move |response| { + tx.lock().unwrap().take().unwrap().send(response).unwrap(); + }, + )?; + + let response = rx.await.unwrap(); + + match response { + Ok(r) => serde_json::from_value(r).map_err(PluginInvokeError::CannotDeserializeResponse), + Err(r) => Err( + serde_json::from_value::(r) + .map(Into::into) + .map_err(PluginInvokeError::CannotDeserializeResponse)?, + ), + } + } + + /// Executes the given mobile command. + pub fn run_mobile_plugin( + &self, + command: impl AsRef, + payload: impl Serialize, + ) -> Result { + let (tx, rx) = channel(); + run_command( + self.name, + &self.handle, + command, + serde_json::to_value(payload).map_err(PluginInvokeError::CannotSerializePayload)?, + move |response| { + tx.send(response).unwrap(); + }, + )?; + + let response = rx.recv().unwrap(); + match response { + Ok(r) => serde_json::from_value(r).map_err(PluginInvokeError::CannotDeserializeResponse), + Err(r) => Err( + serde_json::from_value::(r) + .map(Into::into) + .map_err(PluginInvokeError::CannotDeserializeResponse)?, + ), + } + } +} + +#[cfg(target_os = "ios")] +pub(crate) fn run_command, F: FnOnce(PluginResponse) + Send + 'static>( + name: &str, + _handle: &AppHandle, + command: C, + payload: serde_json::Value, + handler: F, +) -> Result<(), PluginInvokeError> { + use std::{ + ffi::CStr, + os::raw::{c_char, c_int, c_ulonglong}, + }; + + let id: i32 = PENDING_PLUGIN_CALLS_ID.fetch_add(1, Ordering::Relaxed); + PENDING_PLUGIN_CALLS + .get_or_init(Default::default) + .lock() + .unwrap() + .insert(id, Box::new(handler)); + + unsafe { + extern "C" fn plugin_command_response_handler( + id: c_int, + success: c_int, + payload: *const c_char, + ) { + let payload = unsafe { + assert!(!payload.is_null()); + CStr::from_ptr(payload) + }; + + let handler = PENDING_PLUGIN_CALLS + .get_or_init(Default::default) + .lock() + .unwrap() + .remove(&id); + if let Some(handler) = handler { + let json = payload.to_str().unwrap(); + match serde_json::from_str(json) { + Ok(payload) => { + handler(if success == 1 { + Ok(payload) + } else { + Err(payload) + }); + } + Err(err) => { + handler(Err(format!("{err}, data: {}", json).into())); + } + } + } + } + + extern "C" fn send_channel_data_handler(id: c_ulonglong, payload: *const c_char) { + let payload = unsafe { + assert!(!payload.is_null()); + CStr::from_ptr(payload) + }; + + // Clone the channel out and drop the lock before send(): send() can block + // delivering to the webview, and holding CHANNELS across it deadlocks a + // concurrent channel registration/send. + let channel = CHANNELS + .get_or_init(Default::default) + .lock() + .unwrap() + .get(&(id as u32)) + .cloned(); + if let Some(channel) = channel { + let payload: serde_json::Value = serde_json::from_str(payload.to_str().unwrap()).unwrap(); + let _ = channel.send(payload); + } + } + + crate::ios::run_plugin_command( + id, + &name.into(), + &command.as_ref().into(), + &serde_json::to_string(&payload).unwrap().as_str().into(), + crate::ios::PluginMessageCallback(plugin_command_response_handler), + crate::ios::ChannelSendDataCallback(send_channel_data_handler), + ); + } + + Ok(()) +} + +#[cfg(target_os = "android")] +pub(crate) fn run_command< + R: Runtime, + C: AsRef, + F: FnOnce(PluginResponse) + Send + Clone + 'static, +>( + name: &str, + handle: &AppHandle, + command: C, + payload: serde_json::Value, + handler: F, +) -> Result<(), PluginInvokeError> { + use jni::{JNIEnv, errors::Error as JniError, objects::JObject}; + + fn run( + id: i32, + plugin: &str, + command: String, + payload: &serde_json::Value, + env: &mut JNIEnv<'_>, + activity: &JObject<'_>, + ) -> Result<(), JniError> { + let plugin = env.new_string(plugin)?; + let command = env.new_string(&command)?; + let data = env.new_string(serde_json::to_string(payload).unwrap())?; + let plugin_manager = env + .call_method( + activity, + "getPluginManager", + "()Lapp/tauri/plugin/PluginManager;", + &[], + )? + .l()?; + + env.call_method( + plugin_manager, + "runCommand", + "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", + &[ + id.into(), + (&plugin).into(), + (&command).into(), + (&data).into(), + ], + )?; + + Ok(()) + } + + let handle = match handle.runtime() { + RuntimeOrDispatch::Runtime(r) => r.handle(), + RuntimeOrDispatch::RuntimeHandle(h) => h, + _ => unreachable!(), + }; + + let id: i32 = PENDING_PLUGIN_CALLS_ID.fetch_add(1, Ordering::Relaxed); + let plugin_name = name.to_string(); + let command = command.as_ref().to_string(); + + PENDING_PLUGIN_CALLS + .get_or_init(Default::default) + .lock() + .unwrap() + .insert(id, Box::new(handler.clone())); + + handle.run_on_android_context(move |env, activity, _webview| { + if let Err(e) = run(id, &plugin_name, command, &payload, env, activity) { + handler(Err(e.to_string().into())); + } + }); + + Ok(()) +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/process.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/process.rs new file mode 100644 index 000000000..c4218a95c --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/process.rs @@ -0,0 +1,130 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Types and functions related to child processes management. + +use crate::Env; + +use std::path::PathBuf; + +/// Finds the current running binary's path. +/// +/// With exception to any following platform-specific behavior, the path is cached as soon as +/// possible, and then used repeatedly instead of querying for a new path every time this function +/// is called. +/// +/// # Platform-specific behavior +/// +/// ## Linux +/// +/// On Linux, this function will **attempt** to detect if it's currently running from a +/// valid [AppImage] and use that path instead. +/// +/// ## macOS +/// +/// On `macOS`, this function will return an error if the original path contained any symlinks +/// due to less protection on macOS regarding symlinks. This behavior can be disabled by setting the +/// `process-relaunch-dangerous-allow-symlink-macos` feature, although it is *highly discouraged*. +/// +/// # Security +/// +/// See [`tauri_utils::platform::current_exe`] for possible security implications. +/// +/// # Examples +/// +/// ```rust,no_run +/// use tauri::{process::current_binary, Env, Manager}; +/// let current_binary_path = current_binary(&Env::default()).unwrap(); +/// +/// tauri::Builder::default() +/// .setup(|app| { +/// let current_binary_path = current_binary(&app.env())?; +/// Ok(()) +/// }); +/// ``` +/// +/// [AppImage]: https://appimage.org/ +pub fn current_binary(_env: &Env) -> std::io::Result { + // if we are running from an AppImage, we ONLY want the set AppImage path + #[cfg(target_os = "linux")] + if let Some(app_image_path) = &_env.appimage { + return Ok(PathBuf::from(app_image_path)); + } + + tauri_utils::platform::current_exe() +} + +/// Restarts the currently running binary. +/// +/// See [`current_binary`] for platform specific behavior, and +/// [`tauri_utils::platform::current_exe`] for possible security implications. +/// +/// # Examples +/// +/// ```rust,no_run +/// use tauri::{process::restart, Env, Manager}; +/// +/// tauri::Builder::default() +/// .setup(|app| { +/// restart(&app.env()); +/// Ok(()) +/// }); +/// ``` +pub fn restart(env: &Env) -> ! { + use std::process::{Command, exit}; + + if let Ok(path) = current_binary(env) { + // on macOS on updates the binary name might have changed + // so we'll read the Contents/Info.plist file to determine the binary path + #[cfg(target_os = "macos")] + restart_macos_app(&path, env); + + if let Err(e) = Command::new(path).args(env.args_os.iter().skip(1)).spawn() { + log::error!("failed to restart app: {e}"); + } + } + + exit(0); +} + +#[cfg(target_os = "macos")] +fn restart_macos_app(current_binary: &std::path::Path, env: &Env) { + use std::process::{Command, exit}; + + if let Some(macos_directory) = current_binary.parent() { + if macos_directory.components().next_back() + != Some(std::path::Component::Normal(std::ffi::OsStr::new("MacOS"))) + { + return; + } + + if let Some(contents_directory) = macos_directory.parent() { + if contents_directory.components().next_back() + != Some(std::path::Component::Normal(std::ffi::OsStr::new( + "Contents", + ))) + { + return; + } + + if let Ok(info_plist) = + plist::from_file::<_, plist::Dictionary>(contents_directory.join("Info.plist")) + { + if let Some(binary_name) = info_plist + .get("CFBundleExecutable") + .and_then(|v| v.as_string()) + { + if let Err(e) = Command::new(macos_directory.join(binary_name)) + .args(env.args_os.iter().skip(1)) + .spawn() + { + log::error!("failed to restart app: {e}"); + } + + exit(0); + } + } + } + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/protocol/asset.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/protocol/asset.rs new file mode 100644 index 000000000..7d488fade --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/protocol/asset.rs @@ -0,0 +1,309 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{path::SafePathBuf, scope, webview::UriSchemeProtocolHandler}; +use http::{Request, Response, header::*, status::StatusCode}; +use http_range::HttpRange; +use std::{borrow::Cow, io::SeekFrom}; +use tauri_utils::mime_type::MimeType; +use tokio::fs::File; +use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; + +pub fn get(scope: scope::fs::Scope, window_origin: String) -> UriSchemeProtocolHandler { + Box::new(move |_, request, responder| { + let scope = scope.clone(); + let window_origin = window_origin.clone(); + crate::async_runtime::spawn(async move { + match get_response(request, &scope, &window_origin).await { + Ok(response) => responder.respond(response), + Err(e) => responder.respond( + http::Response::builder() + .status(http::StatusCode::INTERNAL_SERVER_ERROR) + .header(CONTENT_TYPE, mime::TEXT_PLAIN.essence_str()) + .header("Access-Control-Allow-Origin", &window_origin) + .body(e.to_string().into_bytes()) + .unwrap(), + ), + } + }); + }) +} + +async fn get_response( + request: Request>, + scope: &scope::fs::Scope, + window_origin: &str, +) -> Result>, Box> { + // skip leading `/` + let path = + percent_encoding::percent_decode(&request.uri().path().as_bytes()[1..]).decode_utf8_lossy(); + + let mut resp = Response::builder().header("Access-Control-Allow-Origin", window_origin); + + let safe_path = match SafePathBuf::new(path.as_ref().into()) { + Ok(path) => path, + Err(e) => { + log::error!("asset protocol path \"{path}\" is not valid: {e}"); + return resp.status(403).body(Vec::new().into()).map_err(Into::into); + } + }; + + if !scope.is_allowed(&safe_path) { + log::error!("asset protocol not configured to allow the path: {path}"); + return resp.status(403).body(Vec::new().into()).map_err(Into::into); + } + + // Separate block for easier error handling + let mut file = match File::open(&safe_path).await { + Ok(file) => file, + Err(e) => { + #[cfg(target_os = "android")] + { + if path.starts_with("/storage/emulated/0/Android/data/") { + log::error!( + "Failed to open Android external storage file '{path}': {e}. This may be due to missing storage permissions." + ); + } + } + return if e.kind() == std::io::ErrorKind::NotFound { + log::error!("File does not exist at path: {path}"); + return resp.status(404).body(Vec::new().into()).map_err(Into::into); + } else if e.kind() == std::io::ErrorKind::PermissionDenied { + log::error!("Missing OS permission to access path \"{path}\": {e}"); + return resp.status(403).body(Vec::new().into()).map_err(Into::into); + } else { + Err(e.into()) + }; + } + }; + + let len = file.metadata().await?.len(); + let (mime_type, read_bytes) = { + // get file mime type + let nbytes = len.min(8192); + let mut magic_buf = Vec::with_capacity(nbytes as usize); + (&mut file).take(nbytes).read_to_end(&mut magic_buf).await?; + file.rewind().await?; + ( + MimeType::parse(&magic_buf, &path), + // return the `magic_bytes` if we read the whole file + // to avoid reading it again later if this is not a range request + if len < 8192 { Some(magic_buf) } else { None }, + ) + }; + + resp = resp.header(CONTENT_TYPE, &mime_type); + + // handle 206 (partial range) http requests + let response = + if let Some(range_header) = request.headers().get("range").and_then(|r| r.to_str().ok()) { + resp = resp.header(ACCEPT_RANGES, "bytes"); + resp = resp.header(ACCESS_CONTROL_EXPOSE_HEADERS, "content-range"); + + let not_satisfiable = || { + Response::builder() + .status(StatusCode::RANGE_NOT_SATISFIABLE) + .header(CONTENT_RANGE, format!("bytes */{len}")) + .body(vec![].into()) + .map_err(Into::into) + }; + + // parse range header + let ranges = if let Ok(ranges) = HttpRange::parse(range_header, len) { + ranges + .iter() + // map the output to spec range , example: 0-499 + .map(|r| (r.start, r.start + r.length - 1)) + .collect::>() + } else { + return not_satisfiable(); + }; + + /// The Maximum bytes we send in one range + const MAX_LEN: u64 = 1000 * 1024; + + // single-part range header + if ranges.len() == 1 { + let &(start, mut end) = ranges.first().unwrap(); + + // check if a range is not satisfiable + // + // this should be already taken care of by the range parsing library + // but checking here again for extra assurance + if start >= len || end >= len || end < start { + return not_satisfiable(); + } + + // adjust end byte for MAX_LEN + end = start + (end - start).min(len - start).min(MAX_LEN - 1); + + // calculate number of bytes needed to be read + let nbytes = end + 1 - start; + + let buf = { + let mut buf = Vec::with_capacity(nbytes as usize); + file.seek(SeekFrom::Start(start)).await?; + file.take(nbytes).read_to_end(&mut buf).await?; + buf + }; + + resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}")); + resp = resp.header(CONTENT_LENGTH, end + 1 - start); + resp = resp.status(StatusCode::PARTIAL_CONTENT); + resp.body(buf.into()) + } else { + let ranges = ranges + .iter() + .filter_map(|&(start, mut end)| { + // filter out unsatisfiable ranges + // + // this should be already taken care of by the range parsing library + // but checking here again for extra assurance + if start >= len || end >= len || end < start { + None + } else { + // adjust end byte for MAX_LEN + end = start + (end - start).min(len - start).min(MAX_LEN - 1); + Some((start, end)) + } + }) + .collect::>(); + + let boundary = random_boundary(); + let boundary_sep = format!("\r\n--{boundary}\r\n"); + let boundary_closer = format!("\r\n--{boundary}--\r\n"); + + // `Builder::header` appends, we want to replace the file mime type set earlier + if let Some(headers) = resp.headers_mut() { + headers.insert( + CONTENT_TYPE, + HeaderValue::from_str(&format!("multipart/byteranges; boundary={boundary}"))?, + ); + } + + let buf = { + // multi-part range header + let mut buf = Vec::new(); + + for (start, end) in ranges { + // a new range is being written, write the range boundary + buf.write_all(boundary_sep.as_bytes()).await?; + + // write the needed headers `Content-Type` and `Content-Range` + buf + .write_all(format!("{CONTENT_TYPE}: {mime_type}\r\n").as_bytes()) + .await?; + buf + .write_all(format!("{CONTENT_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes()) + .await?; + + // write the separator to indicate the start of the range body + buf.write_all("\r\n".as_bytes()).await?; + + // calculate number of bytes needed to be read + let nbytes = end + 1 - start; + + buf.reserve(nbytes as usize); + file.seek(SeekFrom::Start(start)).await?; + (&mut file).take(nbytes).read_to_end(&mut buf).await?; + } + // all ranges have been written, write the closing boundary + buf.write_all(boundary_closer.as_bytes()).await?; + + buf + }; + + resp = resp.status(StatusCode::PARTIAL_CONTENT); + resp.body(buf.into()) + } + } else if request.method() == http::Method::HEAD { + // if the HEAD method is used, we should not return a body + resp = resp.header(CONTENT_LENGTH, len); + resp.body(Vec::new().into()) + } else { + // avoid reading the file if we already read it + // as part of mime type detection + let buf = if let Some(b) = read_bytes { + b + } else { + let mut local_buf = Vec::with_capacity(len as usize); + file.read_to_end(&mut local_buf).await?; + local_buf + }; + resp = resp.header(CONTENT_LENGTH, len); + resp.body(buf.into()) + }; + + response.map_err(Into::into) +} + +fn random_boundary() -> String { + let mut x = [0_u8; 30]; + getrandom::fill(&mut x).expect("failed to get random bytes"); + (x[..]) + .iter() + .map(|&x| format!("{x:x}")) + .fold(String::new(), |mut a, x| { + a.push_str(x.as_str()); + a + }) +} + +#[cfg(test)] +mod tests { + use super::get_response; + use crate::scope::fs::Scope; + use http::{Request, header::CONTENT_TYPE, status::StatusCode}; + use tauri_utils::config::FsScope; + + #[test] + fn multi_range_request() { + let app = crate::test::mock_app(); + + let path = std::env::temp_dir().join(format!( + "tauri-asset-protocol-multi-range-{}.bin", + std::process::id() + )); + std::fs::write(&path, vec![b'a'; 1000]).unwrap(); + + let scope = Scope::new(&app, &FsScope::default()).unwrap(); + scope.allow_file(&path).unwrap(); + + let encoded_path = percent_encoding::percent_encode( + path.to_string_lossy().as_bytes(), + percent_encoding::NON_ALPHANUMERIC, + ) + .to_string(); + + let request = Request::builder() + .uri(format!("asset://localhost/{encoded_path}")) + .header("range", "bytes=0-9, 20-29") + .body(Vec::new()) + .unwrap(); + + let response = + crate::async_runtime::block_on(get_response(request, &scope, "http://tauri.localhost")) + .unwrap(); + std::fs::remove_file(&path).unwrap(); + + assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); + + let content_types = response + .headers() + .get_all(CONTENT_TYPE) + .iter() + .map(|v| v.to_str().unwrap().to_string()) + .collect::>(); + assert_eq!(content_types.len(), 1); + + let boundary = content_types[0] + .strip_prefix("multipart/byteranges; boundary=") + .unwrap(); + assert!( + response + .body() + .ends_with(format!("\r\n--{boundary}--\r\n").as_bytes()) + ); + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/protocol/isolation.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/protocol/isolation.rs new file mode 100644 index 000000000..e1407159e --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/protocol/isolation.rs @@ -0,0 +1,112 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::Assets; +use http::header::CONTENT_TYPE; +use serialize_to_javascript::Template; +use tauri_utils::{ + assets::EmbeddedAssets, + config::{Csp, HeaderAddition}, +}; + +use std::sync::Arc; + +use crate::{ + Runtime, + manager::{AppManager, set_csp, webview::PROCESS_IPC_MESSAGE_FN}, + webview::UriSchemeProtocolHandler, +}; + +pub fn get( + manager: Arc>, + schema: &str, + assets: Arc, + aes_gcm_key: [u8; 32], + window_origin: String, + use_https_scheme: bool, +) -> UriSchemeProtocolHandler { + let frame_src = if cfg!(any(windows, target_os = "android")) { + let https = if use_https_scheme { "https" } else { "http" }; + format!("{https}://{schema}.localhost") + } else { + format!("{schema}:") + }; + + let assets = assets as Arc>; + + Box::new(move |_, request, responder| { + let response = match request_to_path(&request).as_str() { + "index.html" => match assets.get(&"index.html".into()) { + Some(asset) => { + let mut asset = String::from_utf8_lossy(asset.as_ref()).into_owned(); + let csp_map = set_csp( + &mut asset, + &assets, + &"index.html".into(), + &manager, + Csp::Policy(format!("default-src 'none'; frame-src {frame_src}")), + ); + let csp = Csp::DirectiveMap(csp_map).to_string(); + + let template = tauri_utils::pattern::isolation::IsolationJavascriptRuntime { + runtime_aes_gcm_key: &aes_gcm_key, + origin: window_origin.clone(), + process_ipc_message_fn: PROCESS_IPC_MESSAGE_FN, + }; + match template.render(asset.as_ref(), &Default::default()) { + Ok(asset) => http::Response::builder() + .add_configured_headers(manager.config.app.security.headers.as_ref()) + .header(CONTENT_TYPE, mime::TEXT_HTML.as_ref()) + .header("Content-Security-Policy", csp) + .body(asset.into_string().into_bytes()), + Err(_) => http::Response::builder() + .status(http::StatusCode::INTERNAL_SERVER_ERROR) + .header(CONTENT_TYPE, mime::TEXT_PLAIN.as_ref()) + .body(Vec::new()), + } + } + + None => http::Response::builder() + .status(http::StatusCode::NOT_FOUND) + .header(CONTENT_TYPE, mime::TEXT_PLAIN.as_ref()) + .body(Vec::new()), + }, + _ => http::Response::builder() + .status(http::StatusCode::NOT_FOUND) + .header(CONTENT_TYPE, mime::TEXT_PLAIN.as_ref()) + .body(Vec::new()), + }; + + if let Ok(r) = response { + responder.respond(r); + } else { + responder.respond( + http::Response::builder() + .status(http::StatusCode::INTERNAL_SERVER_ERROR) + .body("failed to get response".as_bytes()) + .unwrap(), + ); + } + }) +} + +fn request_to_path(request: &http::Request>) -> String { + let path = request + .uri() + .path() + .trim_start_matches('/') + .trim_end_matches('/'); + + let path = percent_encoding::percent_decode(path.as_bytes()) + .decode_utf8_lossy() + .to_string(); + + if path.is_empty() { + // if the url has no path, we should load `index.html` + "index.html".to_string() + } else { + // skip leading `/` + path.chars().skip(1).collect() + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/protocol/mod.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/protocol/mod.rs new file mode 100644 index 000000000..e6420793d --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/protocol/mod.rs @@ -0,0 +1,53 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Custom protocol handlers + +use percent_encoding::{AsciiSet, NON_ALPHANUMERIC}; + +#[cfg(feature = "protocol-asset")] +pub mod asset; +#[cfg(feature = "isolation")] +pub mod isolation; +pub mod tauri; + +/// Percent-encoding set matching JavaScript's `encodeURIComponent`, +/// which leaves `A-Z a-z 0-9 - _ . ! ~ * ' ( )` unescaped. +pub(crate) const ENCODE_URI_COMPONENT: &AsciiSet = &NON_ALPHANUMERIC + .remove(b'-') + .remove(b'_') + .remove(b'.') + .remove(b'!') + .remove(b'~') + .remove(b'*') + .remove(b'\'') + .remove(b'(') + .remove(b')'); + +/// The origin (`scheme://host`, without a trailing slash) a custom protocol is served from. +/// +/// On Windows and Android custom protocols are served over `http(s)://{scheme}.localhost`; +/// everywhere else they are served over `{scheme}://localhost`. +pub(crate) fn origin(scheme: &str, use_https: bool) -> String { + if cfg!(windows) || cfg!(target_os = "android") { + let http = if use_https { "https" } else { "http" }; + format!("{http}://{scheme}.localhost") + } else { + format!("{scheme}://localhost") + } +} + +#[cfg(test)] +mod tests { + #[test] + fn encode_uri_component_matches_js() { + // encodeURIComponent("aZ09-_.!~*'() /\\?#&%+é") + let input = "aZ09-_.!~*'() /\\?#&%+é"; + let expected = "aZ09-_.!~*'()%20%2F%5C%3F%23%26%25%2B%C3%A9"; + assert_eq!( + percent_encoding::utf8_percent_encode(input, super::ENCODE_URI_COMPONENT).to_string(), + expected + ); + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/protocol/tauri.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/protocol/tauri.rs new file mode 100644 index 000000000..3b940b49d --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/protocol/tauri.rs @@ -0,0 +1,267 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{borrow::Cow, sync::Arc}; + +use http::{Request, Response as HttpResponse, StatusCode, header::CONTENT_TYPE}; +use tauri_utils::config::HeaderAddition; + +use crate::{ + Runtime, + manager::{AppManager, webview::PROXY_DEV_SERVER}, + webview::{UriSchemeProtocolHandler, WebResourceRequestHandler}, +}; + +#[cfg(all(dev, mobile))] +use std::{collections::HashMap, sync::Mutex}; + +#[cfg(all(dev, mobile))] +#[derive(Clone)] +struct CachedResponse { + status: http::StatusCode, + headers: http::HeaderMap, + body: Vec, +} + +pub fn get( + manager: Arc>, + window_origin: String, + web_resource_request_handler: Option>, +) -> UriSchemeProtocolHandler { + #[cfg(all(dev, mobile))] + let (url, client, response_cache) = { + let use_https = window_origin.starts_with("https"); + let mut url = manager.get_app_url(use_https).as_str().to_string(); + if url.ends_with('/') { + url.pop(); + } + + #[allow(unused_mut)] + let mut client_builder = reqwest::ClientBuilder::new(); + if use_https { + #[cfg(feature = "rustls-tls")] + if rustls::crypto::CryptoProvider::get_default().is_none() { + let _ = rustls::crypto::ring::default_provider().install_default(); + } + + // we can't load env vars at runtime, gotta embed them in the lib + #[allow(unused_variables)] + if let Some(cert_pem) = option_env!("TAURI_DEV_ROOT_CERTIFICATE") { + #[cfg(any( + feature = "native-tls", + feature = "native-tls-vendored", + feature = "rustls-tls" + ))] + { + log::info!("adding dev server root certificate"); + let certificate = reqwest::Certificate::from_pem(cert_pem.as_bytes()) + .expect("failed to parse TAURI_DEV_ROOT_CERTIFICATE"); + client_builder = client_builder.tls_certs_merge([certificate]); + } + + #[cfg(not(any( + feature = "native-tls", + feature = "native-tls-vendored", + feature = "rustls-tls" + )))] + { + log::warn!( + "the dev root-certificate-path option was provided, but you must enable one of the following Tauri features in Cargo.toml: native-tls, native-tls-vendored, rustls-tls" + ); + } + } else { + log::warn!( + "loading HTTPS URL; you might need to provide a certificate via the `dev --root-certificate-path` option. You must enable one of the following Tauri features in Cargo.toml: native-tls, native-tls-vendored, rustls-tls" + ); + } + } + let client = client_builder.build().unwrap(); + + let response_cache = Mutex::new(HashMap::new()); + + (url, client, response_cache) + }; + + let context = Arc::new(Context { + manager, + web_resource_request_handler, + window_origin, + #[cfg(all(dev, mobile))] + client, + #[cfg(all(dev, mobile))] + url, + #[cfg(all(dev, mobile))] + response_cache, + }); + + Box::new(move |_, request, responder| { + let context = context.clone(); + crate::async_runtime::spawn(async move { + match get_response(&context, request).await { + Ok(response) => responder.respond(response), + Err(e) => responder.respond( + HttpResponse::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .header(CONTENT_TYPE, mime::TEXT_PLAIN.essence_str()) + .header("Access-Control-Allow-Origin", &context.window_origin) + .body(e.to_string().into_bytes()) + .unwrap(), + ), + } + }); + }) +} + +struct Context { + manager: Arc>, + window_origin: String, + web_resource_request_handler: Option>, + + #[cfg(all(dev, mobile))] + url: String, + #[cfg(all(dev, mobile))] + client: reqwest::Client, + #[cfg(all(dev, mobile))] + response_cache: Mutex>, +} + +async fn get_response( + context: &Context, + request: Request>, +) -> Result>, Box> { + let Context { + manager, + web_resource_request_handler, + window_origin, + #[cfg(all(dev, mobile))] + client, + #[cfg(all(dev, mobile))] + url, + #[cfg(all(dev, mobile))] + response_cache, + } = context; + + // use the entire URI as we are going to proxy the request + let path = if PROXY_DEV_SERVER { + request.uri().to_string() + } else { + // ignore query string and fragment + request + .uri() + .to_string() + .split(&['?', '#']) + .next() + .unwrap() + .into() + }; + + let path = path + .strip_prefix("tauri://localhost") + .map(|p| p.to_string()) + // the `strip_prefix` only returns None when a request is made to `https://tauri.$P` on Windows and Android + // where `$P` is not `localhost/*` + .unwrap_or_default(); + + #[allow(unused_mut)] + let mut builder = HttpResponse::builder() + .add_configured_headers(manager.config.app.security.headers.as_ref()) + .header("Access-Control-Allow-Origin", window_origin); + + #[cfg(all(dev, mobile))] + let mut response = + proxy_dev_request(client, url, response_cache, path, builder, &request).await?; + + #[cfg(not(all(dev, mobile)))] + let mut response = { + let asset = manager.get_asset( + path, + request.uri().scheme() == Some(&http::uri::Scheme::HTTPS), + )?; + builder = builder.header(CONTENT_TYPE, &asset.mime_type); + if let Some(csp) = &asset.csp_header { + builder = builder.header("Content-Security-Policy", csp); + } + builder.body(asset.bytes.into())? + }; + + if let Some(handler) = web_resource_request_handler { + handler(request, &mut response); + } + + Ok(response) +} + +#[cfg(all(dev, mobile))] +async fn proxy_dev_request( + client: &reqwest::Client, + url: &String, + response_cache: &Mutex>, + path: String, + mut builder: http::response::Builder, + request: &Request>, +) -> Result>, Box> { + let decoded_path = percent_encoding::percent_decode(path.as_bytes()) + .decode_utf8_lossy() + .to_string(); + let url = format!( + "{}/{}", + url.trim_end_matches('/'), + decoded_path.trim_start_matches('/') + ); + + let mut proxy_builder = client.request(request.method().clone(), &url); + for (name, value) in request.headers() { + proxy_builder = proxy_builder.header(name, value); + } + proxy_builder = proxy_builder.body(request.body().clone()); + + let response = proxy_builder.send().await.map_err(|e|{ + let error_message = format!( + "Failed to request {url}: {e}{}", + if let Some(s) = e.status() { + format!("status code: {}", s.as_u16()) + } else if cfg!(target_os = "ios") { + ", did you grant local network permissions? That is required to reach the development server. Please grant the permission via the prompt or in `Settings > Privacy & Security > Local Network` and restart the app. See https://support.apple.com/en-us/102229 for more information.".to_string() + } else { + "".to_string() + } + ); + log::error!("{error_message}"); + error_message + })?; + + let status = response.status(); + + if status == http::StatusCode::NOT_MODIFIED { + if let Some(response) = response_cache.lock().unwrap().get(&url).cloned() { + for (name, value) in &response.headers { + builder = builder.header(name, value); + } + + return Ok(builder.status(response.status).body(response.body.into())?); + } + } + + let headers = response.headers().clone(); + let body = response.bytes().await?.to_vec(); + let response = CachedResponse { + status, + headers, + body, + }; + + response_cache + .lock() + .unwrap() + .insert(url.clone(), response.clone()); + + for (name, value) in &response.headers { + builder = builder.header(name, value); + } + + builder + .status(response.status) + .body(response.body.into()) + .map_err(Into::into) +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/resources/mod.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/resources/mod.rs new file mode 100644 index 000000000..956155858 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/resources/mod.rs @@ -0,0 +1,223 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// a modified version of https://github.com/denoland/deno/blob/0ae83847f498a2886ae32172e50fd5bdbab2f524/core/resources.rs#L220 + +pub(crate) mod plugin; + +use std::{ + any::{Any, TypeId, type_name}, + borrow::Cow, + collections::BTreeMap, + sync::Arc, +}; + +/// Resources are Rust objects that are stored in [ResourceTable] and managed by tauri. +/// +/// They are identified in JS by a numeric ID (the resource ID, or rid). +/// Resources can be created in commands. Resources can also be retrieved in commands by +/// their rid. Resources are thread-safe. +/// +/// Resources are reference counted in Rust. This means that they can be +/// cloned and passed around. When the last reference is dropped, the resource +/// is automatically closed. As long as the resource exists in the resource +/// table, the reference count is at least 1. +pub trait Resource: Any + 'static + Send + Sync { + /// Returns a string representation of the resource. The default implementation + /// returns the Rust type name, but specific resource types may override this + /// trait method. + fn name(&self) -> Cow<'_, str> { + type_name::().into() + } + + /// Resources may implement the `close()` trait method if they need to do + /// resource specific clean-ups, such as cancelling pending futures, after a + /// resource has been removed from the resource table. + fn close(self: Arc) {} +} + +impl dyn Resource { + #[inline(always)] + fn is(&self) -> bool { + self.type_id() == TypeId::of::() + } + + #[inline(always)] + pub(crate) fn downcast_arc(self: &Arc) -> Option<&Arc> { + if self.is::() { + // A resource is stored as `Arc` in a BTreeMap + // and is safe to cast to `Arc` because of the runtime + // check done in `self.is::()` + let ptr = self as *const Arc<_> as *const Arc; + Some(unsafe { &*ptr }) + } else { + None + } + } +} + +/// A `ResourceId` is an integer value referencing a resource. It could be +/// considered to be the tauri equivalent of a `file descriptor` in POSIX like +/// operating systems. +pub type ResourceId = u32; + +/// Map-like data structure storing Tauri's resources (equivalent to file +/// descriptors). +/// +/// Provides basic methods for element access. A resource can be of any type. +/// Different types of resources can be stored in the same map, and provided +/// with a name for description. +/// +/// Each resource is identified through a _resource ID (rid)_, which acts as +/// the key in the map. +#[derive(Default)] +pub struct ResourceTable { + index: BTreeMap>, +} + +impl ResourceTable { + fn new_random_rid() -> u32 { + let mut bytes = [0_u8; 4]; + getrandom::fill(&mut bytes).expect("failed to get random bytes"); + u32::from_ne_bytes(bytes) + } + + /// Inserts resource into the resource table, which takes ownership of it. + /// + /// The resource type is erased at runtime and must be statically known + /// when retrieving it through `get()`. + /// + /// Returns a unique resource ID, which acts as a key for this resource. + pub fn add(&mut self, resource: T) -> ResourceId { + self.add_arc(Arc::new(resource)) + } + + /// Inserts a `Arc`-wrapped resource into the resource table. + /// + /// The resource type is erased at runtime and must be statically known + /// when retrieving it through `get()`. + /// + /// Returns a unique resource ID, which acts as a key for this resource. + pub fn add_arc(&mut self, resource: Arc) -> ResourceId { + let resource = resource as Arc; + self.add_arc_dyn(resource) + } + + /// Inserts a `Arc`-wrapped resource into the resource table. + /// + /// The resource type is erased at runtime and must be statically known + /// when retrieving it through `get()`. + /// + /// Returns a unique resource ID, which acts as a key for this resource. + pub fn add_arc_dyn(&mut self, resource: Arc) -> ResourceId { + let mut rid = Self::new_random_rid(); + while self.index.contains_key(&rid) { + rid = Self::new_random_rid(); + } + + let removed_resource = self.index.insert(rid, resource); + assert!(removed_resource.is_none()); + rid + } + + /// Returns true if any resource with the given `rid` exists. + pub fn has(&self, rid: ResourceId) -> bool { + self.index.contains_key(&rid) + } + + /// Returns a reference counted pointer to the resource of type `T` with the + /// given `rid`. If `rid` is not present or has a type different than `T`, + /// this function returns [`Error::BadResourceId`](crate::Error::BadResourceId). + pub fn get(&self, rid: ResourceId) -> crate::Result> { + self + .index + .get(&rid) + .and_then(|rc| rc.downcast_arc::()) + .cloned() + .ok_or_else(|| crate::Error::BadResourceId(rid)) + } + + /// Returns a reference counted pointer to the resource of the given `rid`. + /// If `rid` is not present, this function returns [`crate::Error::BadResourceId`]. + pub fn get_any(&self, rid: ResourceId) -> crate::Result> { + self + .index + .get(&rid) + .ok_or_else(|| crate::Error::BadResourceId(rid)) + .cloned() + } + + /// Replaces a resource with a new resource. + /// + /// Panics if the resource does not exist. + pub fn replace(&mut self, rid: ResourceId, resource: T) { + let result = self + .index + .insert(rid, Arc::new(resource) as Arc); + assert!(result.is_some()); + } + + /// Removes a resource of type `T` from the resource table and returns it. + /// If a resource with the given `rid` exists but its type does not match `T`, + /// it is not removed from the resource table. Note that the resource's + /// `close()` method is *not* called. + /// + /// Also note that there might be a case where + /// the returned `Arc` is referenced by other variables. That is, we cannot + /// assume that `Arc::strong_count(&returned_arc)` is always equal to 1 on success. + /// In particular, be really careful when you want to extract the inner value of + /// type `T` from `Arc`. + pub fn take(&mut self, rid: ResourceId) -> crate::Result> { + let resource = self.get::(rid)?; + self.index.remove(&rid); + Ok(resource) + } + + /// Removes a resource from the resource table and returns it. Note that the + /// resource's `close()` method is *not* called. + /// + /// Also note that there might be a + /// case where the returned `Arc` is referenced by other variables. That is, + /// we cannot assume that `Arc::strong_count(&returned_arc)` is always equal to 1 + /// on success. In particular, be really careful when you want to extract the + /// inner value of type `T` from `Arc`. + pub fn take_any(&mut self, rid: ResourceId) -> crate::Result> { + self + .index + .remove(&rid) + .ok_or_else(|| crate::Error::BadResourceId(rid)) + } + + /// Returns an iterator that yields a `(id, name)` pair for every resource + /// that's currently in the resource table. This can be used for debugging + /// purposes. Note that the order in + /// which items appear is not specified. + pub fn names(&self) -> impl Iterator)> { + self + .index + .iter() + .map(|(&id, resource)| (id, resource.name())) + } + + /// Removes the resource with the given `rid` from the resource table. If the + /// only reference to this resource existed in the resource table, this will + /// cause the resource to be dropped. However, since resources are reference + /// counted, therefore pending ops are not automatically cancelled. A resource + /// may implement the `close()` method to perform clean-ups such as canceling + /// ops. + pub fn close(&mut self, rid: ResourceId) -> crate::Result<()> { + self + .index + .remove(&rid) + .ok_or_else(|| crate::Error::BadResourceId(rid)) + .map(|resource| resource.close()) + } + + /// Removes and frees all resources stored. Note that the + /// resource's `close()` method is *not* called. + pub(crate) fn clear(&mut self) { + self.index.clear() + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/resources/plugin.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/resources/plugin.rs new file mode 100644 index 000000000..548a01acf --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/resources/plugin.rs @@ -0,0 +1,31 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{ + Manager, Runtime, Webview, command, + plugin::{Builder, TauriPlugin}, +}; + +use super::ResourceId; + +#[command(root = "crate")] +fn close(webview: Webview, rid: ResourceId) -> crate::Result<()> { + let mut result = webview.resources_table().close(rid); + if result.is_err() { + result = webview.window().resources_table().close(rid); + if result.is_err() { + result = webview.app_handle().resources_table().close(rid); + } + } + result +} + +pub(crate) fn init() -> TauriPlugin { + Builder::new("resources") + .invoke_handler(crate::generate_handler![ + #![plugin(resources)] + close + ]) + .build() +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/scope/fs.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/scope/fs.rs new file mode 100644 index 000000000..4dabeddf9 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/scope/fs.rs @@ -0,0 +1,739 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + collections::{HashMap, HashSet}, + fmt, + path::{MAIN_SEPARATOR, Path, PathBuf}, + sync::{ + Arc, Mutex, + atomic::{AtomicBool, AtomicU32, Ordering}, + }, +}; + +use tauri_utils::config::FsScope; + +use crate::ScopeEventId; + +pub use glob::Pattern; + +/// Scope change event. +#[derive(Debug, Clone)] +pub enum Event { + /// A path has been allowed. + PathAllowed(PathBuf), + /// A path has been forbidden. + PathForbidden(PathBuf), +} + +type EventListener = Box; + +enum Pending { + Unlisten(ScopeEventId), + Listen { + id: ScopeEventId, + handler: EventListener, + }, + Emit(Event), +} + +struct ScopeInner { + allowed_patterns: Mutex>, + forbidden_patterns: Mutex>, + match_options: glob::MatchOptions, + event_listeners: Mutex>, + pending: Mutex>, + emitting: AtomicBool, + next_event_id: AtomicU32, +} + +/// Scope for filesystem access. +#[derive(Clone)] +pub struct Scope { + inner: Arc, +} + +impl Scope { + fn next_event_id(&self) -> u32 { + self.inner.next_event_id.fetch_add(1, Ordering::Relaxed) + } +} + +impl fmt::Debug for Scope { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Scope") + .field( + "allowed_patterns", + &self + .inner + .allowed_patterns + .lock() + .unwrap() + .iter() + .map(|p| p.as_str()) + .collect::>(), + ) + .field( + "forbidden_patterns", + &self + .inner + .forbidden_patterns + .lock() + .unwrap() + .iter() + .map(|p| p.as_str()) + .collect::>(), + ) + .finish() + } +} + +fn push_pattern, F: Fn(&str) -> Result>( + list: &mut HashSet, + pattern: P, + f: F, +) -> crate::Result<()> { + // Reconstruct pattern path components with appropriate separator + // so `some\path/to/dir/**\*` would be `some/path/to/dir/**/*` on Unix + // and `some\path\to\dir\**\*` on Windows. + let path: PathBuf = pattern.as_ref().components().collect(); + + // Add pattern as is to be matched with paths as is + let path_str = path.to_string_lossy(); + list.insert(f(&path_str)?); + + // On Windows, if path starts with a Prefix, try to strip it if possible + // so `\\?\C:\\SomeDir` would result in a scope of: + // - `\\?\C:\\SomeDir` + // - `C:\\SomeDir` + #[cfg(windows)] + { + use std::path::{Component, Prefix}; + + let mut components = path.components(); + + let is_unc = match components.next() { + Some(Component::Prefix(p)) => match p.kind() { + Prefix::VerbatimDisk(..) => true, + _ => false, // Other kinds of UNC paths + }, + _ => false, // relative or empty + }; + + if is_unc { + // we remove UNC manually, instead of `dunce::simplified` because + // `path` could have `*` in it and that's not allowed on Windows and + // `dunce::simplified` will check that and return `path` as is without simplification + let simplified = path + .to_str() + .and_then(|s| s.get(4..)) + .map_or(path.as_path(), Path::new); + + let simplified_str = simplified.to_string_lossy(); + if simplified_str != path_str { + list.insert(f(&simplified_str)?); + } + } + } + + // Add canonicalized version of the pattern or canonicalized version of its parents + // so `/data/user/0/appid/assets/*` would be canonicalized to `/data/data/appid/assets/*` + // and can then be matched against any of them. + if let Some(p) = canonicalize_parent(path) { + list.insert(f(&p.to_string_lossy())?); + } + + Ok(()) +} + +/// Attempt to canonicalize path or its parents in case we have a path like `/data/user/0/appid/**` +/// where `**` obviously does not exist but we need to canonicalize the parent. +/// +/// example: given the `/data/user/0/appid/assets/*` path, +/// it's a glob pattern so it won't exist (std::fs::canonicalize() fails); +/// +/// the second iteration needs to check `/data/user/0/appid/assets` and save the `*` component to append later. +/// +/// if it also does not exist, a third iteration is required to check `/data/user/0/appid` +/// with `assets/*` as the cached value (`checked_path` variable) +/// on Android that gets canonicalized to `/data/data/appid` so the final value will be `/data/data/appid/assets/*` +/// which is the value we want to check when we execute the `Scope::is_allowed` function +fn canonicalize_parent(mut path: PathBuf) -> Option { + let mut failed_components = None; + + loop { + if let Ok(path) = path.canonicalize() { + break Some(if let Some(p) = failed_components { + path.join(p) + } else { + path + }); + } + + // grap the last component of the path + if let Some(mut last) = path.iter().next_back().map(PathBuf::from) { + // remove the last component of the path so the next iteration checks its parent + // if there is no more parent components, we failed to canonicalize + if !path.pop() { + break None; + } + + // append the already checked path to the last component + // to construct `/` and saved it for next iteration + if let Some(failed_components) = &failed_components { + last.push(failed_components); + } + failed_components.replace(last); + } else { + break None; + } + } +} +impl Scope { + /// Creates a new scope from a [`FsScope`] configuration. + pub fn new>( + manager: &M, + scope: &FsScope, + ) -> crate::Result { + let mut allowed_patterns = HashSet::new(); + for path in scope.allowed_paths() { + if let Ok(path) = manager.path().parse(path) { + push_pattern(&mut allowed_patterns, path, Pattern::new)?; + } + } + + let mut forbidden_patterns = HashSet::new(); + if let Some(forbidden_paths) = scope.forbidden_paths() { + for path in forbidden_paths { + if let Ok(path) = manager.path().parse(path) { + push_pattern(&mut forbidden_patterns, path, Pattern::new)?; + } + } + } + + let require_literal_leading_dot = match scope { + FsScope::Scope { + require_literal_leading_dot: Some(require), + .. + } => *require, + // dotfiles are not supposed to be exposed by default on unix + #[cfg(unix)] + _ => true, + #[cfg(windows)] + _ => false, + }; + + Ok(Self { + inner: Arc::new(ScopeInner { + allowed_patterns: Mutex::new(allowed_patterns), + forbidden_patterns: Mutex::new(forbidden_patterns), + event_listeners: Default::default(), + pending: Default::default(), + emitting: AtomicBool::new(false), + next_event_id: Default::default(), + match_options: glob::MatchOptions { + // this is needed so `/dir/*` doesn't match files within subdirectories such as `/dir/subdir/file.txt` + // see: + require_literal_separator: true, + require_literal_leading_dot, + ..Default::default() + }, + }), + }) + } + + /// The list of allowed patterns. + pub fn allowed_patterns(&self) -> HashSet { + self.inner.allowed_patterns.lock().unwrap().clone() + } + + /// The list of forbidden patterns. + pub fn forbidden_patterns(&self) -> HashSet { + self.inner.forbidden_patterns.lock().unwrap().clone() + } + + /// Listen to an event on this scope. + pub fn listen(&self, f: F) -> ScopeEventId { + let id = self.next_event_id(); + self.listen_with_id(id, Box::new(f)); + id + } + + fn listen_with_id(&self, id: ScopeEventId, handler: EventListener) { + if self.inner.emitting.load(Ordering::Relaxed) { + self + .inner + .pending + .lock() + .unwrap() + .push(Pending::Listen { id, handler }); + } else { + self + .inner + .event_listeners + .lock() + .unwrap() + .insert(id, handler); + } + } + + /// Listen to an event on this scope and immediately unlisten. + pub fn once(&self, f: F) -> ScopeEventId { + let self_ = self.clone(); + let handler = std::cell::Cell::new(Some(f)); + let id = self.next_event_id(); + self.listen_with_id( + id, + Box::new(move |event| { + self_.unlisten(id); + let handler = handler + .take() + .expect("attempted to call handler more than once"); + handler(event); + }), + ); + id + } + + /// Removes an event listener on this scope. + pub fn unlisten(&self, id: ScopeEventId) { + if self.inner.emitting.load(Ordering::Relaxed) { + self + .inner + .pending + .lock() + .unwrap() + .push(Pending::Unlisten(id)); + } else { + self.inner.event_listeners.lock().unwrap().remove(&id); + } + } + + fn emit(&self, event: Event) { + let was_emitting = self.inner.emitting.swap(true, Ordering::Relaxed); + if was_emitting { + self + .inner + .pending + .lock() + .unwrap() + .push(Pending::Emit(event)); + return; + } + + { + let listeners = self.inner.event_listeners.lock().unwrap(); + let handlers = listeners.values(); + for listener in handlers { + listener(&event); + } + self.inner.emitting.store(false, Ordering::Relaxed); + } + + let pending = { + let mut lock = self.inner.pending.lock().unwrap(); + std::mem::take(&mut *lock) + }; + for action in pending { + match action { + Pending::Unlisten(id) => self.unlisten(id), + Pending::Listen { id, handler } => self.listen_with_id(id, handler), + Pending::Emit(event) => self.emit(event), + } + } + } + + /// Extend the allowed patterns with the given directory. + /// + /// After this function has been called, the frontend will be able to use the Tauri API to read + /// the directory and all of its files. If `recursive` is `true`, subdirectories will be accessible too. + pub fn allow_directory>(&self, path: P, recursive: bool) -> crate::Result<()> { + let path = path.as_ref(); + { + let mut list = self.inner.allowed_patterns.lock().unwrap(); + + // allow the directory to be read + push_pattern(&mut list, path, escaped_pattern)?; + // allow its files and subdirectories to be read + push_pattern(&mut list, path, |p| { + escaped_pattern_with(p, if recursive { "**" } else { "*" }) + })?; + } + self.emit(Event::PathAllowed(path.to_path_buf())); + Ok(()) + } + + /// Extend the allowed patterns with the given file path. + /// + /// After this function has been called, the frontend will be able to use the Tauri API to read the contents of this file. + pub fn allow_file>(&self, path: P) -> crate::Result<()> { + let path = path.as_ref(); + push_pattern( + &mut self.inner.allowed_patterns.lock().unwrap(), + path, + escaped_pattern, + )?; + self.emit(Event::PathAllowed(path.to_path_buf())); + Ok(()) + } + + /// Set the given directory path to be forbidden by this scope. + /// + /// **Note:** this takes precedence over allowed paths, so its access gets denied **always**. + pub fn forbid_directory>(&self, path: P, recursive: bool) -> crate::Result<()> { + let path = path.as_ref(); + { + let mut list = self.inner.forbidden_patterns.lock().unwrap(); + + // allow the directory to be read + push_pattern(&mut list, path, escaped_pattern)?; + // allow its files and subdirectories to be read + push_pattern(&mut list, path, |p| { + escaped_pattern_with(p, if recursive { "**" } else { "*" }) + })?; + } + self.emit(Event::PathForbidden(path.to_path_buf())); + Ok(()) + } + + /// Set the given file path to be forbidden by this scope. + /// + /// **Note:** this takes precedence over allowed paths, so its access gets denied **always**. + pub fn forbid_file>(&self, path: P) -> crate::Result<()> { + let path = path.as_ref(); + push_pattern( + &mut self.inner.forbidden_patterns.lock().unwrap(), + path, + escaped_pattern, + )?; + self.emit(Event::PathForbidden(path.to_path_buf())); + Ok(()) + } + + /// Determines if the given path is allowed on this scope. + /// + /// Returns `false` if the path was explicitly forbidden or neither allowed nor forbidden. + /// + /// May return `false` if the path points to a broken symlink. + pub fn is_allowed>(&self, path: P) -> bool { + let path = try_resolve_symlink_and_canonicalize(path); + + if let Ok(path) = path { + let path: PathBuf = path.components().collect(); + let forbidden = self + .inner + .forbidden_patterns + .lock() + .unwrap() + .iter() + .any(|p| p.matches_path_with(&path, self.inner.match_options)); + + if forbidden { + false + } else { + self + .inner + .allowed_patterns + .lock() + .unwrap() + .iter() + .any(|p| p.matches_path_with(&path, self.inner.match_options)) + } + } else { + false + } + } + + /// Determines if the given path is explicitly forbidden on this scope. + /// + /// May return `true` if the path points to a broken symlink. + pub fn is_forbidden>(&self, path: P) -> bool { + let path = try_resolve_symlink_and_canonicalize(path); + + if let Ok(path) = path { + let path: PathBuf = path.components().collect(); + self + .inner + .forbidden_patterns + .lock() + .unwrap() + .iter() + .any(|p| p.matches_path_with(&path, self.inner.match_options)) + } else { + true + } + } +} + +fn try_resolve_symlink_and_canonicalize>(path: P) -> crate::Result { + let path = path.as_ref(); + let path = if path.is_symlink() { + std::fs::read_link(path)? + } else { + path.to_path_buf() + }; + if !path.exists() { + crate::Result::Ok(path) + } else { + std::fs::canonicalize(path).map_err(Into::into) + } +} + +fn escaped_pattern(p: &str) -> Result { + Pattern::new(&glob::Pattern::escape(p)) +} + +fn escaped_pattern_with(p: &str, append: &str) -> Result { + if p.ends_with(MAIN_SEPARATOR) { + Pattern::new(&format!("{}{append}", glob::Pattern::escape(p))) + } else { + Pattern::new(&format!( + "{}{}{append}", + glob::Pattern::escape(p), + MAIN_SEPARATOR + )) + } +} + +#[cfg(test)] +mod tests { + use std::{collections::HashSet, sync::Arc}; + + use glob::Pattern; + + use crate::fs::ScopeInner; + + use super::{Scope, push_pattern}; + + fn new_scope() -> Scope { + Scope { + inner: Arc::new(ScopeInner { + allowed_patterns: Default::default(), + forbidden_patterns: Default::default(), + event_listeners: Default::default(), + pending: Default::default(), + emitting: Default::default(), + next_event_id: Default::default(), + match_options: glob::MatchOptions { + // this is needed so `/dir/*` doesn't match files within subdirectories such as `/dir/subdir/file.txt` + // see: + require_literal_separator: true, + // dotfiles are not supposed to be exposed by default on unix + #[cfg(unix)] + require_literal_leading_dot: true, + #[cfg(windows)] + require_literal_leading_dot: false, + ..Default::default() + }, + }), + } + } + + #[test] + fn path_is_escaped() { + let scope = new_scope(); + #[cfg(unix)] + { + scope.allow_directory("/home/tauri/**", false).unwrap(); + assert!(scope.is_allowed("/home/tauri/**")); + assert!(scope.is_allowed("/home/tauri/**/file")); + assert!(!scope.is_allowed("/home/tauri/anyfile")); + } + #[cfg(windows)] + { + scope.allow_directory("C:\\home\\tauri\\**", false).unwrap(); + assert!(scope.is_allowed("C:\\home\\tauri\\**")); + assert!(scope.is_allowed("C:\\home\\tauri\\**\\file")); + assert!(!scope.is_allowed("C:\\home\\tauri\\anyfile")); + } + + let scope = new_scope(); + #[cfg(unix)] + { + scope.allow_file("/home/tauri/**").unwrap(); + assert!(scope.is_allowed("/home/tauri/**")); + assert!(!scope.is_allowed("/home/tauri/**/file")); + assert!(!scope.is_allowed("/home/tauri/anyfile")); + } + #[cfg(windows)] + { + scope.allow_file("C:\\home\\tauri\\**").unwrap(); + assert!(scope.is_allowed("C:\\home\\tauri\\**")); + assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file")); + assert!(!scope.is_allowed("C:\\home\\tauri\\anyfile")); + } + + let scope = new_scope(); + #[cfg(unix)] + { + scope.allow_directory("/home/tauri", true).unwrap(); + scope.forbid_directory("/home/tauri/**", false).unwrap(); + assert!(!scope.is_allowed("/home/tauri/**")); + assert!(!scope.is_allowed("/home/tauri/**/file")); + assert!(scope.is_allowed("/home/tauri/**/inner/file")); + assert!(scope.is_allowed("/home/tauri/inner/folder/anyfile")); + assert!(scope.is_allowed("/home/tauri/anyfile")); + } + #[cfg(windows)] + { + scope.allow_directory("C:\\home\\tauri", true).unwrap(); + scope + .forbid_directory("C:\\home\\tauri\\**", false) + .unwrap(); + assert!(!scope.is_allowed("C:\\home\\tauri\\**")); + assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file")); + assert!(scope.is_allowed("C:\\home\\tauri\\**\\inner\\file")); + assert!(scope.is_allowed("C:\\home\\tauri\\inner\\folder\\anyfile")); + assert!(scope.is_allowed("C:\\home\\tauri\\anyfile")); + } + + let scope = new_scope(); + #[cfg(unix)] + { + scope.allow_directory("/home/tauri", true).unwrap(); + scope.forbid_file("/home/tauri/**").unwrap(); + assert!(!scope.is_allowed("/home/tauri/**")); + assert!(scope.is_allowed("/home/tauri/**/file")); + assert!(scope.is_allowed("/home/tauri/**/inner/file")); + assert!(scope.is_allowed("/home/tauri/anyfile")); + } + #[cfg(windows)] + { + scope.allow_directory("C:\\home\\tauri", true).unwrap(); + scope.forbid_file("C:\\home\\tauri\\**").unwrap(); + assert!(!scope.is_allowed("C:\\home\\tauri\\**")); + assert!(scope.is_allowed("C:\\home\\tauri\\**\\file")); + assert!(scope.is_allowed("C:\\home\\tauri\\**\\inner\\file")); + assert!(scope.is_allowed("C:\\home\\tauri\\anyfile")); + } + + let scope = new_scope(); + #[cfg(unix)] + { + scope.allow_directory("/home/tauri", false).unwrap(); + assert!(scope.is_allowed("/home/tauri/**")); + assert!(!scope.is_allowed("/home/tauri/**/file")); + assert!(!scope.is_allowed("/home/tauri/**/inner/file")); + assert!(scope.is_allowed("/home/tauri/anyfile")); + } + #[cfg(windows)] + { + scope.allow_directory("C:\\home\\tauri", false).unwrap(); + assert!(scope.is_allowed("C:\\home\\tauri\\**")); + assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file")); + assert!(!scope.is_allowed("C:\\home\\tauri\\**\\inner\\file")); + assert!(scope.is_allowed("C:\\home\\tauri\\anyfile")); + } + } + + #[cfg(windows)] + #[test] + fn windows_root_paths() { + let scope = new_scope(); + { + // UNC network path + scope.allow_directory("\\\\localhost\\c$", true).unwrap(); + assert!(scope.is_allowed("\\\\localhost\\c$")); + assert!(scope.is_allowed("\\\\localhost\\c$\\Windows")); + assert!(scope.is_allowed("\\\\localhost\\c$\\NonExistentFile")); + assert!(!scope.is_allowed("\\\\localhost\\d$")); + assert!(!scope.is_allowed("\\\\OtherServer\\Share")); + } + + let scope = new_scope(); + { + // Verbatim UNC network path + scope + .allow_directory("\\\\?\\UNC\\localhost\\c$", true) + .unwrap(); + assert!(scope.is_allowed("\\\\localhost\\c$")); + assert!(scope.is_allowed("\\\\localhost\\c$\\Windows")); + assert!(scope.is_allowed("\\\\?\\UNC\\localhost\\c$\\Windows\\NonExistentFile")); + // A non-existent file cannot be canonicalized to a verbatim UNC path, so this will fail to match + assert!(!scope.is_allowed("\\\\localhost\\c$\\Windows\\NonExistentFile")); + assert!(!scope.is_allowed("\\\\localhost\\d$")); + assert!(!scope.is_allowed("\\\\OtherServer\\Share")); + } + + let scope = new_scope(); + { + // Device namespace + scope.allow_file("\\\\.\\COM1").unwrap(); + assert!(scope.is_allowed("\\\\.\\COM1")); + assert!(!scope.is_allowed("\\\\.\\COM2")); + } + + let scope = new_scope(); + { + // Disk root + scope.allow_directory("C:\\", true).unwrap(); + assert!(scope.is_allowed("C:\\Windows")); + assert!(scope.is_allowed("C:\\Windows\\system.ini")); + assert!(scope.is_allowed("C:\\NonExistentFile")); + assert!(!scope.is_allowed("D:\\home")); + } + + let scope = new_scope(); + { + // Verbatim disk root + scope.allow_directory("\\\\?\\C:\\", true).unwrap(); + assert!(scope.is_allowed("C:\\Windows")); + assert!(scope.is_allowed("C:\\Windows\\system.ini")); + assert!(scope.is_allowed("C:\\NonExistentFile")); + assert!(!scope.is_allowed("D:\\home")); + } + + let scope = new_scope(); + { + // Verbatim path + scope.allow_file("\\\\?\\anyfile").unwrap(); + assert!(scope.is_allowed("\\\\?\\anyfile")); + assert!(!scope.is_allowed("\\\\?\\otherfile")); + } + } + + #[test] + fn push_pattern_generated_paths() { + macro_rules! assert_pattern { + ($patterns:ident, $pattern:literal) => { + assert!($patterns.contains(&Pattern::new($pattern).unwrap())) + }; + } + + let mut patterns = HashSet::new(); + + #[cfg(not(windows))] + { + push_pattern(&mut patterns, "/path/to/dir/", Pattern::new).expect("failed to push pattern"); + push_pattern(&mut patterns, "/path/to/dir/**", Pattern::new).expect("failed to push pattern"); + + assert_pattern!(patterns, "/path/to/dir"); + assert_pattern!(patterns, "/path/to/dir/**"); + } + + #[cfg(windows)] + { + push_pattern(&mut patterns, "C:\\path\\to\\dir", Pattern::new) + .expect("failed to push pattern"); + push_pattern(&mut patterns, "C:\\path\\to\\dir\\**", Pattern::new) + .expect("failed to push pattern"); + + assert_pattern!(patterns, "C:\\path\\to\\dir"); + assert_pattern!(patterns, "C:\\path\\to\\dir\\**"); + assert_pattern!(patterns, "\\\\?\\C:\\path\\to\\dir"); + assert_pattern!(patterns, "\\\\?\\C:\\path\\to\\dir\\**"); + } + } + + #[test] + fn event_no_deadlocks() { + let scope = new_scope(); + let scope_clone = scope.clone(); + scope.once(move |event| { + assert!(matches!(event, super::Event::PathAllowed(_))); + scope_clone.allow_file("/another-test-path").unwrap(); + }); + scope.allow_file("/test-path").unwrap(); + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/scope/mod.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/scope/mod.rs new file mode 100644 index 000000000..af60c9d07 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/scope/mod.rs @@ -0,0 +1,80 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Runtime access scopes. +//! +//! A scope narrows what an allowed feature may actually touch at runtime. +//! Tauri has two complementary scoping mechanisms: +//! +//! - **The IPC scope**, which is part of the [Access Control List](crate::ipc::RuntimeAuthority): +//! capabilities define which commands each window/webview and origin may call, and the scope +//! attached to a permission is read from a command with [`CommandScope`](crate::ipc::CommandScope) +//! and [`GlobalScope`](crate::ipc::GlobalScope). That scope is static - it is resolved from the +//! capability files at compile time (or added at runtime with +//! [`Manager::add_capability`](crate::Manager::add_capability)) - and it is the command +//! implementation that decides what to do with it. +//! - **The file system scope** in [`mod@fs`], a mutable list of allowed and forbidden glob patterns +//! that the [asset protocol](https://v2.tauri.app/security/asset-protocol/) checks before serving +//! a file. It is seeded from `app > security > assetProtocol > scope` in the configuration and +//! can be changed while the application is running. +//! +//! Use `Manager::asset_protocol_scope` (`protocol-asset` Cargo feature) to get the asset +//! protocol scope and [`fs::Scope::allow_directory`], [`fs::Scope::allow_file`], +//! [`fs::Scope::forbid_directory`] and [`fs::Scope::forbid_file`] to change it, for instance to +//! grant access to a file the user just picked in a dialog: +//! +//! ```rust +//! # #[cfg(feature = "protocol-asset")] +//! # fn run() { +//! use tauri::Manager; +//! +//! tauri::Builder::default() +//! .setup(|app| { +//! app.asset_protocol_scope().allow_directory("/home/user/pictures", true)?; +//! Ok(()) +//! }); +//! # } +//! ``` +//! +//! Scope changes are not persisted: they are lost when the application restarts. +//! Use the [persisted scope plugin](https://v2.tauri.app/plugin/persisted-scope/) +//! if you need them to survive a restart. + +/// FS scope. +pub mod fs; + +use std::path::Path; + +/// Unique id of a scope event. +pub type ScopeEventId = u32; + +/// Managed state for all the core scopes in a tauri application. +pub struct Scopes { + #[cfg(feature = "protocol-asset")] + pub(crate) asset_protocol: fs::Scope, +} + +#[allow(unused)] +impl Scopes { + /// Allows a directory on the scopes. + pub fn allow_directory>(&self, path: P, recursive: bool) -> crate::Result<()> { + #[cfg(feature = "protocol-asset")] + self.asset_protocol.allow_directory(path, recursive)?; + Ok(()) + } + + /// Allows a file on the scopes. + pub fn allow_file>(&self, path: P) -> crate::Result<()> { + #[cfg(feature = "protocol-asset")] + self.asset_protocol.allow_file(path)?; + Ok(()) + } + + /// Forbids a file on the scopes. + pub fn forbid_file>(&self, path: P) -> crate::Result<()> { + #[cfg(feature = "protocol-asset")] + self.asset_protocol.forbid_file(path)?; + Ok(()) + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/state.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/state.rs new file mode 100644 index 000000000..fb563f571 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/state.rs @@ -0,0 +1,291 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + any::{Any, TypeId}, + collections::HashMap, + hash::BuildHasherDefault, + sync::Mutex, +}; + +use crate::{ + Manager, Runtime, + ipc::{CommandArg, CommandItem, InvokeError}, +}; + +/// A guard for a state value. +/// +/// See [`Manager::manage`](`crate::Manager::manage`) for usage examples. +pub struct State<'r, T>(&'r T); + +impl<'r, T> State<'r, T> { + /// Retrieve a borrow to the underlying value with a lifetime of `'r`. + /// Using this method is typically unnecessary as `State` implements + /// [`std::ops::Deref`] with a [`std::ops::Deref::Target`] of `T`. + #[inline(always)] + pub fn inner(&self) -> &'r T { + self.0 + } +} + +impl std::ops::Deref for State<'_, T> { + type Target = T; + + #[inline(always)] + fn deref(&self) -> &T { + self.0 + } +} + +impl Clone for State<'_, T> { + fn clone(&self) -> Self { + State(self.0) + } +} + +impl PartialEq for State<'_, T> { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl std::fmt::Debug for State<'_, T> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("State").field(&self.0).finish() + } +} + +impl<'r, 'de: 'r, T: Send + Sync + 'static, R: Runtime> CommandArg<'de, R> for State<'r, T> { + /// Grabs the [`State`] from the [`CommandItem`]. This will never fail. + fn from_command(command: CommandItem<'de, R>) -> Result { + command.message.webview_ref().try_state().ok_or_else(|| { + InvokeError::from_anyhow(anyhow::anyhow!( + "state not managed for field `{}` on command `{}`. You must call `.manage()` before using this command", + command.key, command.name + )) + }) + } +} + +// Taken from: https://github.com/SergioBenitez/state/blob/556c1b94db8ce8427a0e72de7983ab5a9af4cc41/src/ident_hash.rs +// This is a _super_ stupid hash. It just uses its input as the hash value. This +// hash is meant to be used _only_ for "prehashed" values. In particular, we use +// this so that hashing a TypeId is essentially a noop. This is because TypeIds +// are already unique integers. +#[derive(Default)] +struct IdentHash(u64); + +impl std::hash::Hasher for IdentHash { + fn finish(&self) -> u64 { + self.0 + } + + fn write(&mut self, bytes: &[u8]) { + for byte in bytes { + self.write_u8(*byte); + } + } + + fn write_u8(&mut self, i: u8) { + self.0 = (self.0 << 8) | (i as u64); + } + + fn write_u64(&mut self, i: u64) { + self.0 = i; + } +} + +/// Safety: +/// - The `key` must equal to `(*value).type_id()`, see the safety doc in methods of [StateManager] for details. +/// - Once you insert a value, you can't remove/mutated/move it anymore, see [StateManager::try_get] for details. +type TypeIdMap = HashMap, BuildHasherDefault>; + +/// The Tauri state manager. +// TODO: make private for v3 +#[derive(Debug)] +pub struct StateManager { + map: Mutex, +} + +impl StateManager { + pub(crate) fn new() -> Self { + Self { + map: Default::default(), + } + } + + pub(crate) fn set(&self, state: T) -> bool { + let mut map = self.map.lock().unwrap(); + let type_id = TypeId::of::(); + let already_set = map.contains_key(&type_id); + if !already_set { + let state = Box::new(state) as Box; + map.insert(type_id, state); + } + !already_set + } + + /// SAFETY: Calling this method will move the `value`, + /// which will cause references obtained through [Self::try_get] to dangle. + pub(crate) unsafe fn unmanage(&self) -> Option { + let mut map = self.map.lock().unwrap(); + let type_id = TypeId::of::(); + let state = map.remove(&type_id)?; + let value = state + .downcast::() + .expect("the type of the key should be same as the type of the value"); + Some(*value) + } + + /// Gets the state associated with the specified type. + pub fn get(&self) -> State<'_, T> { + self + .try_get() + .unwrap_or_else(|| panic!("state not found for type {}", std::any::type_name::())) + } + + /// Gets the state associated with the specified type. + pub fn try_get(&self) -> Option> { + let map = self.map.lock().unwrap(); + let type_id = TypeId::of::(); + let state = map.get(&type_id)?; + let value = state + .downcast_ref::() + .expect("the type of the key should be same as the type of the value"); + // SAFETY: We ensure the lifetime of `value` is the same as [StateManager] and `value` will not be mutated/moved. + let v_ref = unsafe { &*(value as *const T) }; + Some(State(v_ref)) + } +} + +// Ported from https://github.com/SergioBenitez/state/blob/556c1b94db8ce8427a0e72de7983ab5a9af4cc41/tests/main.rs +#[cfg(test)] +mod tests { + use super::StateManager; + + use std::sync::{Arc, RwLock}; + use std::thread; + + // Tiny structures to test that dropping works as expected. + struct DroppingStruct(Arc>); + struct DroppingStructWrap(#[allow(dead_code)] DroppingStruct); + + impl Drop for DroppingStruct { + fn drop(&mut self) { + *self.0.write().unwrap() = true; + } + } + + #[test] + #[should_panic(expected = "state not found for type core::option::Option")] + fn get_panics() { + let state = StateManager::new(); + state.get::>(); + } + + #[test] + fn simple_set_get() { + let state = StateManager::new(); + assert!(state.set(1u32)); + assert_eq!(*state.get::(), 1); + } + + #[test] + fn simple_set_get_unmanage() { + let state = StateManager::new(); + assert!(state.set(1u32)); + assert_eq!(*state.get::(), 1); + // safety: the reference returned by `try_get` is already dropped. + assert!(unsafe { state.unmanage::() }.is_some()); + assert!(unsafe { state.unmanage::() }.is_none()); + assert_eq!(state.try_get::(), None); + assert!(state.set(2u32)); + assert_eq!(*state.get::(), 2); + } + + #[test] + fn dst_set_get() { + let state = StateManager::new(); + assert!(state.set::<[u32; 4]>([1, 2, 3, 4u32])); + assert_eq!(*state.get::<[u32; 4]>(), [1, 2, 3, 4]); + } + + #[test] + fn set_get_remote() { + let state = Arc::new(StateManager::new()); + let sate_ = Arc::clone(&state); + thread::spawn(move || { + sate_.set(10isize); + }) + .join() + .unwrap(); + + assert_eq!(*state.get::(), 10); + } + + #[test] + fn two_put_get() { + let state = StateManager::new(); + assert!(state.set("Hello, world!".to_string())); + + let s_old = state.get::(); + assert_eq!(*s_old, "Hello, world!"); + + assert!(!state.set::("Bye bye!".into())); + assert_eq!(*state.get::(), "Hello, world!"); + assert_eq!(state.get::(), s_old); + } + + #[test] + fn many_puts_only_one_succeeds() { + let state = Arc::new(StateManager::new()); + let mut threads = vec![]; + for _ in 0..1000 { + let state_ = Arc::clone(&state); + threads.push(thread::spawn(move || state_.set(10i64))) + } + + let results: Vec = threads.into_iter().map(|t| t.join().unwrap()).collect(); + assert_eq!(results.into_iter().filter(|&b| b).count(), 1); + assert_eq!(*state.get::(), 10); + } + + // Ensure setting when already set doesn't cause a drop. + #[test] + fn test_no_drop_on_set() { + let state = StateManager::new(); + let drop_flag = Arc::new(RwLock::new(false)); + let dropping_struct = DroppingStruct(drop_flag.clone()); + + let _drop_flag_ignore = Arc::new(RwLock::new(false)); + let _dropping_struct_ignore = DroppingStruct(_drop_flag_ignore); + + state.set::(dropping_struct); + assert!(!state.set::(_dropping_struct_ignore)); + assert!(!*drop_flag.read().unwrap()); + } + + // Ensure dropping a type_map drops its contents. + #[test] + fn drop_inners_on_drop() { + let drop_flag_a = Arc::new(RwLock::new(false)); + let dropping_struct_a = DroppingStruct(drop_flag_a.clone()); + + let drop_flag_b = Arc::new(RwLock::new(false)); + let dropping_struct_b = DroppingStructWrap(DroppingStruct(drop_flag_b.clone())); + + { + let state = StateManager::new(); + state.set(dropping_struct_a); + assert!(!*drop_flag_a.read().unwrap()); + + state.set(dropping_struct_b); + assert!(!*drop_flag_a.read().unwrap()); + assert!(!*drop_flag_b.read().unwrap()); + } + + assert!(*drop_flag_a.read().unwrap()); + assert!(*drop_flag_b.read().unwrap()); + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/test/mock_runtime.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/test/mock_runtime.rs new file mode 100644 index 000000000..44a681b0f --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/test/mock_runtime.rs @@ -0,0 +1,1425 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![allow(dead_code)] +#![allow(missing_docs)] + +use tauri_runtime::{ + DeviceEventFilter, Error, EventLoopProxy, ExitRequestedEventAction, Icon, ProgressBarState, + Result, RunEvent, Runtime, RuntimeHandle, RuntimeInitArgs, UserAttentionType, UserEvent, + WebviewDispatch, WindowDispatch, WindowEventId, + dpi::{PhysicalPosition, PhysicalSize, Position, Size}, + monitor::Monitor, + webview::{DetachedWebview, PendingWebview}, + window::{ + CursorIcon, DetachedWindow, DetachedWindowWebview, PendingWindow, RawWindow, WindowBuilder, + WindowBuilderBase, WindowEvent, WindowId, + }, +}; + +#[cfg(target_os = "macos")] +use tauri_utils::TitleBarStyle; +use tauri_utils::{Theme, config::WindowConfig}; +use url::Url; + +#[cfg(windows)] +use windows::Win32::Foundation::HWND; + +use std::{ + cell::RefCell, + collections::HashMap, + fmt, + sync::{ + Arc, Mutex, + atomic::{AtomicBool, AtomicU32, Ordering}, + mpsc::{Receiver, SyncSender, channel, sync_channel}, + }, +}; + +type ShortcutMap = HashMap>; + +enum Message { + Task(Box), + CloseWindow(WindowId), + DestroyWindow(WindowId), +} + +struct Webview; + +struct Window { + label: String, + webviews: Vec, +} + +#[derive(Clone)] +pub struct RuntimeContext { + is_running: Arc, + windows: Arc>>, + shortcuts: Arc>, + run_tx: SyncSender, + main_thread_id: std::thread::ThreadId, + next_window_id: Arc, + next_webview_id: Arc, + next_window_event_id: Arc, + next_webview_event_id: Arc, +} + +// SAFETY: we ensure this type is only used on the main thread. +#[allow(clippy::non_send_fields_in_send_ty)] +unsafe impl Send for RuntimeContext {} + +// SAFETY: we ensure this type is only used on the main thread. +#[allow(clippy::non_send_fields_in_send_ty)] +unsafe impl Sync for RuntimeContext {} + +impl RuntimeContext { + fn send_message(&self, message: Message) -> Result<()> { + if std::thread::current().id() == self.main_thread_id { + if let Message::Task(task) = message { + task(); + return Ok(()); + } + } + if self.is_running.load(Ordering::Relaxed) { + self + .run_tx + .send(message) + .map_err(|_| Error::FailedToSendMessage) + } else { + match message { + Message::Task(task) => task(), + Message::CloseWindow(id) | Message::DestroyWindow(id) => { + self.windows.borrow_mut().remove(&id); + } + } + Ok(()) + } + } + + fn next_window_id(&self) -> WindowId { + self.next_window_id.fetch_add(1, Ordering::Relaxed).into() + } + + fn next_webview_id(&self) -> u32 { + self.next_webview_id.fetch_add(1, Ordering::Relaxed) + } + + fn next_window_event_id(&self) -> WindowEventId { + self.next_window_event_id.fetch_add(1, Ordering::Relaxed) + } + + fn next_webview_event_id(&self) -> WindowEventId { + self.next_webview_event_id.fetch_add(1, Ordering::Relaxed) + } +} + +impl fmt::Debug for RuntimeContext { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RuntimeContext").finish() + } +} + +#[derive(Debug, Clone)] +pub struct MockRuntimeHandle { + context: RuntimeContext, +} + +impl RuntimeHandle for MockRuntimeHandle { + type Runtime = MockRuntime; + + fn create_proxy(&self) -> EventProxy { + EventProxy {} + } + + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + fn set_activation_policy( + &self, + activation_policy: tauri_runtime::ActivationPolicy, + ) -> Result<()> { + Ok(()) + } + + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + fn set_dock_visibility(&self, visible: bool) -> Result<()> { + Ok(()) + } + + fn request_exit(&self, code: i32) -> Result<()> { + unimplemented!() + } + + /// Create a new webview window. + fn create_window) + Send + 'static>( + &self, + pending: PendingWindow, + _after_window_creation: Option, + ) -> Result> { + let id = self.context.next_window_id(); + + let (webview_id, webviews) = if let Some(w) = &pending.webview { + (Some(self.context.next_webview_id()), vec![Webview]) + } else { + (None, Vec::new()) + }; + + self.context.windows.borrow_mut().insert( + id, + Window { + label: pending.label.clone(), + webviews, + }, + ); + + let webview = webview_id.map(|id| { + let pending_webview = pending.webview.unwrap(); + DetachedWindowWebview { + webview: DetachedWebview { + label: pending.label.clone(), + dispatcher: MockWebviewDispatcher { + id, + context: self.context.clone(), + url: Arc::new(Mutex::new(pending_webview.url)), + last_evaluated_script: Default::default(), + }, + }, + use_https_scheme: pending_webview.webview_attributes.use_https_scheme, + } + }); + + Ok(DetachedWindow { + id, + label: pending.label, + dispatcher: MockWindowDispatcher { + id, + context: self.context.clone(), + }, + webview, + }) + } + + fn create_webview( + &self, + window_id: WindowId, + pending: PendingWebview, + ) -> Result> { + let id = self.context.next_webview_id(); + let webview = Webview; + if let Some(w) = self.context.windows.borrow_mut().get_mut(&window_id) { + w.webviews.push(webview); + } + + Ok(DetachedWebview { + label: pending.label, + dispatcher: MockWebviewDispatcher { + id, + context: self.context.clone(), + last_evaluated_script: Default::default(), + url: Arc::new(Mutex::new(pending.url)), + }, + }) + } + + /// Run a task on the main thread. + fn run_on_main_thread(&self, f: F) -> Result<()> { + self.context.send_message(Message::Task(Box::new(f))) + } + + fn display_handle( + &self, + ) -> std::result::Result, raw_window_handle::HandleError> { + #[cfg(target_os = "linux")] + return Ok(unsafe { + raw_window_handle::DisplayHandle::borrow_raw(raw_window_handle::RawDisplayHandle::Xlib( + raw_window_handle::XlibDisplayHandle::new(None, 0), + )) + }); + #[cfg(target_os = "macos")] + return Ok(unsafe { + raw_window_handle::DisplayHandle::borrow_raw(raw_window_handle::RawDisplayHandle::AppKit( + raw_window_handle::AppKitDisplayHandle::new(), + )) + }); + #[cfg(windows)] + return Ok(unsafe { + raw_window_handle::DisplayHandle::borrow_raw(raw_window_handle::RawDisplayHandle::Windows( + raw_window_handle::WindowsDisplayHandle::new(), + )) + }); + #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))] + unimplemented!(); + } + + fn primary_monitor(&self) -> Result> { + unimplemented!() + } + + fn monitor_from_point(&self, x: f64, y: f64) -> Result> { + unimplemented!() + } + + fn available_monitors(&self) -> Result> { + unimplemented!() + } + + fn set_theme(&self, theme: Option) { + unimplemented!() + } + + /// Shows the application, but does not automatically focus it. + #[cfg(target_os = "macos")] + fn show(&self) -> Result<()> { + Ok(()) + } + + /// Hides the application. + #[cfg(target_os = "macos")] + fn hide(&self) -> Result<()> { + Ok(()) + } + + fn set_device_event_filter(&self, _: DeviceEventFilter) { + // no-op + } + + #[cfg(target_os = "android")] + fn find_class<'a>( + &self, + env: &mut jni::JNIEnv<'a>, + activity: &jni::objects::JObject<'_>, + name: impl Into, + ) -> std::result::Result, jni::errors::Error> { + todo!() + } + + #[cfg(target_os = "android")] + fn run_on_android_context(&self, f: F) + where + F: FnOnce(&mut jni::JNIEnv<'_>, &jni::objects::JObject<'_>, &jni::objects::JObject<'_>) + + Send + + 'static, + { + todo!() + } + + #[cfg(any(target_os = "macos", target_os = "ios"))] + fn fetch_data_store_identifiers) + Send + 'static>( + &self, + cb: F, + ) -> Result<()> { + todo!() + } + + #[cfg(any(target_os = "macos", target_os = "ios"))] + fn remove_data_store) + Send + 'static>( + &self, + uuid: [u8; 16], + cb: F, + ) -> Result<()> { + todo!() + } + + fn cursor_position(&self) -> Result> { + Ok(PhysicalPosition::new(0.0, 0.0)) + } +} + +#[derive(Debug, Clone)] +pub struct MockWebviewDispatcher { + id: u32, + context: RuntimeContext, + url: Arc>, + last_evaluated_script: Arc>>, +} + +impl MockWebviewDispatcher { + pub fn last_evaluated_script(&self) -> Option { + self.last_evaluated_script.lock().unwrap().clone() + } +} + +#[derive(Debug, Clone)] +pub struct MockWindowDispatcher { + id: WindowId, + context: RuntimeContext, +} + +#[derive(Debug, Clone)] +pub struct MockWindowBuilder {} + +impl WindowBuilderBase for MockWindowBuilder {} + +impl WindowBuilder for MockWindowBuilder { + fn new() -> Self { + Self {} + } + + fn with_config(config: &WindowConfig) -> Self { + Self {} + } + + fn center(self) -> Self { + self + } + + fn position(self, x: f64, y: f64) -> Self { + self + } + + fn inner_size(self, min_width: f64, min_height: f64) -> Self { + self + } + + fn min_inner_size(self, min_width: f64, min_height: f64) -> Self { + self + } + + fn max_inner_size(self, max_width: f64, max_height: f64) -> Self { + self + } + + fn inner_size_constraints( + self, + constraints: tauri_runtime::window::WindowSizeConstraints, + ) -> Self { + self + } + + fn prevent_overflow(self) -> Self { + self + } + + fn prevent_overflow_with_margin(self, margin: tauri_runtime::dpi::Size) -> Self { + self + } + + fn resizable(self, resizable: bool) -> Self { + self + } + + fn maximizable(self, resizable: bool) -> Self { + self + } + + fn minimizable(self, resizable: bool) -> Self { + self + } + + fn closable(self, resizable: bool) -> Self { + self + } + + fn title>(self, title: S) -> Self { + self + } + + fn fullscreen(self, fullscreen: bool) -> Self { + self + } + + fn focused(self, focused: bool) -> Self { + self + } + + fn focusable(self, focusable: bool) -> Self { + self + } + + fn maximized(self, maximized: bool) -> Self { + self + } + + fn visible(self, visible: bool) -> Self { + self + } + + fn transparent(self, transparent: bool) -> Self { + self + } + + fn decorations(self, decorations: bool) -> Self { + self + } + + fn always_on_bottom(self, always_on_bottom: bool) -> Self { + self + } + + fn always_on_top(self, always_on_top: bool) -> Self { + self + } + + fn visible_on_all_workspaces(self, visible_on_all_workspaces: bool) -> Self { + self + } + + fn content_protected(self, protected: bool) -> Self { + self + } + + fn icon(self, icon: Icon<'_>) -> Result { + Ok(self) + } + + fn skip_taskbar(self, skip: bool) -> Self { + self + } + + fn window_classname>(self, classname: S) -> Self { + self + } + + fn no_redirection_bitmap(self, enable: bool) -> Self { + self + } + + fn shadow(self, enable: bool) -> Self { + self + } + + #[cfg(windows)] + fn owner(self, owner: HWND) -> Self { + self + } + + #[cfg(windows)] + fn parent(self, parent: HWND) -> Self { + self + } + + #[cfg(target_os = "macos")] + fn parent(self, parent: *mut std::ffi::c_void) -> Self { + self + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn transient_for(self, parent: &impl gtk::glib::IsA) -> Self { + self + } + + #[cfg(windows)] + fn drag_and_drop(self, enabled: bool) -> Self { + self + } + + #[cfg(target_os = "macos")] + fn title_bar_style(self, style: TitleBarStyle) -> Self { + self + } + + #[cfg(target_os = "macos")] + fn traffic_light_position>(self, position: P) -> Self { + self + } + + #[cfg(target_os = "macos")] + fn hidden_title(self, transparent: bool) -> Self { + self + } + + #[cfg(target_os = "macos")] + fn tabbing_identifier(self, identifier: &str) -> Self { + self + } + + fn theme(self, theme: Option) -> Self { + self + } + + fn has_icon(&self) -> bool { + false + } + + fn get_theme(&self) -> Option { + None + } + + fn background_color(self, _color: tauri_utils::config::Color) -> Self { + self + } + + #[cfg(target_os = "android")] + fn activity_name>(self, _class_name: S) -> Self { + self + } + + #[cfg(target_os = "android")] + fn created_by_activity_name>(self, _class_name: S) -> Self { + self + } + + #[cfg(target_os = "ios")] + fn requested_by_scene_identifier>(self, _identifier: S) -> Self { + self + } +} + +impl WebviewDispatch for MockWebviewDispatcher { + type Runtime = MockRuntime; + + fn run_on_main_thread(&self, f: F) -> Result<()> { + self.context.send_message(Message::Task(Box::new(f))) + } + + fn on_webview_event( + &self, + f: F, + ) -> tauri_runtime::WebviewEventId { + self.context.next_window_event_id() + } + + fn with_webview) + Send + 'static>(&self, f: F) -> Result<()> { + Ok(()) + } + + #[cfg(any(debug_assertions, feature = "devtools"))] + fn open_devtools(&self) {} + + #[cfg(any(debug_assertions, feature = "devtools"))] + fn close_devtools(&self) {} + + #[cfg(any(debug_assertions, feature = "devtools"))] + fn is_devtools_open(&self) -> Result { + Ok(false) + } + + fn set_zoom(&self, scale_factor: f64) -> Result<()> { + Ok(()) + } + + fn eval_script>(&self, script: S) -> Result<()> { + self + .last_evaluated_script + .lock() + .unwrap() + .replace(script.into()); + Ok(()) + } + + fn eval_script_with_callback>( + &self, + script: S, + callback: impl Fn(String) + Send + 'static, + ) -> Result<()> { + self + .last_evaluated_script + .lock() + .unwrap() + .replace(script.into()); + Ok(()) + } + + fn url(&self) -> Result { + Ok(self.url.lock().unwrap().clone()) + } + + fn bounds(&self) -> Result { + Ok(tauri_runtime::dpi::Rect::default()) + } + + fn position(&self) -> Result> { + Ok(PhysicalPosition { x: 0, y: 0 }) + } + + fn size(&self) -> Result> { + Ok(PhysicalSize { + width: 0, + height: 0, + }) + } + + fn navigate(&self, url: Url) -> Result<()> { + *self.url.lock().unwrap() = url.to_string(); + Ok(()) + } + + fn reload(&self) -> Result<()> { + Ok(()) + } + + fn print(&self) -> Result<()> { + Ok(()) + } + + fn close(&self) -> Result<()> { + Ok(()) + } + + fn set_bounds(&self, bounds: tauri_runtime::dpi::Rect) -> Result<()> { + Ok(()) + } + + fn set_size(&self, _size: Size) -> Result<()> { + Ok(()) + } + + fn set_position(&self, _position: Position) -> Result<()> { + Ok(()) + } + + fn set_focus(&self) -> Result<()> { + Ok(()) + } + + fn reparent(&self, window_id: WindowId) -> Result<()> { + Ok(()) + } + + fn cookies(&self) -> Result>> { + Ok(Vec::new()) + } + + fn cookies_for_url(&self, url: Url) -> Result>> { + Ok(Vec::new()) + } + + fn set_cookie(&self, cookie: tauri_runtime::Cookie<'_>) -> Result<()> { + Ok(()) + } + + fn delete_cookie(&self, cookie: tauri_runtime::Cookie<'_>) -> Result<()> { + Ok(()) + } + + fn set_auto_resize(&self, auto_resize: bool) -> Result<()> { + Ok(()) + } + + fn clear_all_browsing_data(&self) -> Result<()> { + Ok(()) + } + + fn hide(&self) -> Result<()> { + Ok(()) + } + + fn show(&self) -> Result<()> { + Ok(()) + } + + fn set_background_color(&self, color: Option) -> Result<()> { + Ok(()) + } +} + +impl WindowDispatch for MockWindowDispatcher { + type Runtime = MockRuntime; + + type WindowBuilder = MockWindowBuilder; + + fn run_on_main_thread(&self, f: F) -> Result<()> { + self.context.send_message(Message::Task(Box::new(f))) + } + + fn on_window_event(&self, f: F) -> WindowEventId { + self.context.next_window_event_id() + } + + fn scale_factor(&self) -> Result { + Ok(1.0) + } + + fn inner_position(&self) -> Result> { + Ok(PhysicalPosition { x: 0, y: 0 }) + } + + fn outer_position(&self) -> Result> { + Ok(PhysicalPosition { x: 0, y: 0 }) + } + + fn inner_size(&self) -> Result> { + Ok(PhysicalSize { + width: 0, + height: 0, + }) + } + + fn outer_size(&self) -> Result> { + Ok(PhysicalSize { + width: 0, + height: 0, + }) + } + + fn is_fullscreen(&self) -> Result { + Ok(false) + } + + fn is_minimized(&self) -> Result { + Ok(false) + } + + fn is_maximized(&self) -> Result { + Ok(false) + } + + fn is_focused(&self) -> Result { + Ok(false) + } + + fn is_decorated(&self) -> Result { + Ok(false) + } + + fn is_resizable(&self) -> Result { + Ok(false) + } + + fn is_maximizable(&self) -> Result { + Ok(true) + } + + fn is_minimizable(&self) -> Result { + Ok(true) + } + + fn is_closable(&self) -> Result { + Ok(true) + } + + fn is_visible(&self) -> Result { + Ok(true) + } + + fn title(&self) -> Result { + Ok(String::new()) + } + + fn current_monitor(&self) -> Result> { + Ok(None) + } + + fn primary_monitor(&self) -> Result> { + Ok(None) + } + + fn monitor_from_point(&self, x: f64, y: f64) -> Result> { + Ok(None) + } + + fn available_monitors(&self) -> Result> { + Ok(Vec::new()) + } + + fn theme(&self) -> Result { + Ok(Theme::Light) + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn gtk_window(&self) -> Result { + unimplemented!() + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn default_vbox(&self) -> Result { + unimplemented!() + } + + #[cfg(target_os = "android")] + fn activity_name(&self) -> Result { + unimplemented!() + } + + #[cfg(target_os = "ios")] + fn scene_identifier(&self) -> Result { + unimplemented!() + } + + fn window_handle( + &self, + ) -> std::result::Result, raw_window_handle::HandleError> { + #[cfg(target_os = "linux")] + return unsafe { + Ok(raw_window_handle::WindowHandle::borrow_raw( + raw_window_handle::RawWindowHandle::Xlib(raw_window_handle::XlibWindowHandle::new(0)), + )) + }; + #[cfg(target_os = "macos")] + return unsafe { + Ok(raw_window_handle::WindowHandle::borrow_raw( + raw_window_handle::RawWindowHandle::AppKit(raw_window_handle::AppKitWindowHandle::new( + std::ptr::NonNull::from(&()).cast(), + )), + )) + }; + #[cfg(windows)] + return unsafe { + Ok(raw_window_handle::WindowHandle::borrow_raw( + raw_window_handle::RawWindowHandle::Win32(raw_window_handle::Win32WindowHandle::new( + std::num::NonZeroIsize::MIN, + )), + )) + }; + #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))] + unimplemented!(); + } + + fn center(&self) -> Result<()> { + Ok(()) + } + + fn request_user_attention(&self, request_type: Option) -> Result<()> { + Ok(()) + } + + fn create_window) + Send + 'static>( + &mut self, + pending: PendingWindow, + _after_window_creation: Option, + ) -> Result> { + let id = self.context.next_window_id(); + + let (webview_id, webviews) = if let Some(w) = &pending.webview { + (Some(self.context.next_webview_id()), vec![Webview]) + } else { + (None, Vec::new()) + }; + + self.context.windows.borrow_mut().insert( + id, + Window { + label: pending.label.clone(), + webviews, + }, + ); + + let webview = webview_id.map(|id| { + let pending_webview = pending.webview.unwrap(); + DetachedWindowWebview { + webview: DetachedWebview { + label: pending.label.clone(), + dispatcher: MockWebviewDispatcher { + id, + context: self.context.clone(), + url: Arc::new(Mutex::new(pending_webview.url)), + last_evaluated_script: Default::default(), + }, + }, + use_https_scheme: pending_webview.webview_attributes.use_https_scheme, + } + }); + + Ok(DetachedWindow { + id, + label: pending.label, + dispatcher: MockWindowDispatcher { + id, + context: self.context.clone(), + }, + webview, + }) + } + + fn create_webview( + &mut self, + pending: PendingWebview, + ) -> Result> { + let id = self.context.next_webview_id(); + let webview = Webview; + if let Some(w) = self.context.windows.borrow_mut().get_mut(&self.id) { + w.webviews.push(webview); + } + + Ok(DetachedWebview { + label: pending.label, + dispatcher: MockWebviewDispatcher { + id, + context: self.context.clone(), + last_evaluated_script: Default::default(), + url: Arc::new(Mutex::new(pending.url)), + }, + }) + } + + fn set_resizable(&self, resizable: bool) -> Result<()> { + Ok(()) + } + + fn set_maximizable(&self, maximizable: bool) -> Result<()> { + Ok(()) + } + + fn set_minimizable(&self, minimizable: bool) -> Result<()> { + Ok(()) + } + + fn set_closable(&self, closable: bool) -> Result<()> { + Ok(()) + } + + fn set_title>(&self, title: S) -> Result<()> { + Ok(()) + } + + fn maximize(&self) -> Result<()> { + Ok(()) + } + + fn unmaximize(&self) -> Result<()> { + Ok(()) + } + + fn minimize(&self) -> Result<()> { + Ok(()) + } + + fn unminimize(&self) -> Result<()> { + Ok(()) + } + + fn show(&self) -> Result<()> { + Ok(()) + } + + fn hide(&self) -> Result<()> { + Ok(()) + } + + fn close(&self) -> Result<()> { + self.context.send_message(Message::CloseWindow(self.id))?; + Ok(()) + } + + fn destroy(&self) -> Result<()> { + self.context.send_message(Message::DestroyWindow(self.id))?; + Ok(()) + } + + fn set_decorations(&self, decorations: bool) -> Result<()> { + Ok(()) + } + + fn set_shadow(&self, shadow: bool) -> Result<()> { + Ok(()) + } + + fn set_always_on_bottom(&self, always_on_bottom: bool) -> Result<()> { + Ok(()) + } + + fn set_always_on_top(&self, always_on_top: bool) -> Result<()> { + Ok(()) + } + + fn set_visible_on_all_workspaces(&self, visible_on_all_workspaces: bool) -> Result<()> { + Ok(()) + } + + fn set_content_protected(&self, protected: bool) -> Result<()> { + Ok(()) + } + + fn set_size(&self, size: Size) -> Result<()> { + Ok(()) + } + + fn set_min_size(&self, size: Option) -> Result<()> { + Ok(()) + } + + fn set_max_size(&self, size: Option) -> Result<()> { + Ok(()) + } + + fn set_position(&self, position: Position) -> Result<()> { + Ok(()) + } + + fn set_fullscreen(&self, fullscreen: bool) -> Result<()> { + Ok(()) + } + + fn set_fullscreen_on_monitor(&self, position: PhysicalPosition) -> Result<()> { + Ok(()) + } + + #[cfg(target_os = "macos")] + fn set_simple_fullscreen(&self, enable: bool) -> Result<()> { + Ok(()) + } + + fn set_focus(&self) -> Result<()> { + Ok(()) + } + + fn set_focusable(&self, focusable: bool) -> Result<()> { + Ok(()) + } + + fn set_icon(&self, icon: Icon<'_>) -> Result<()> { + Ok(()) + } + + fn set_skip_taskbar(&self, skip: bool) -> Result<()> { + Ok(()) + } + + fn set_cursor_grab(&self, grab: bool) -> Result<()> { + Ok(()) + } + + fn set_cursor_visible(&self, visible: bool) -> Result<()> { + Ok(()) + } + + fn set_cursor_icon(&self, icon: CursorIcon) -> Result<()> { + Ok(()) + } + + fn set_cursor_position>(&self, position: Pos) -> Result<()> { + Ok(()) + } + + fn set_ignore_cursor_events(&self, ignore: bool) -> Result<()> { + Ok(()) + } + + fn start_dragging(&self) -> Result<()> { + Ok(()) + } + + fn start_resize_dragging(&self, direction: tauri_runtime::ResizeDirection) -> Result<()> { + Ok(()) + } + + fn set_progress_bar(&self, progress_state: ProgressBarState) -> Result<()> { + Ok(()) + } + + fn set_badge_count(&self, count: Option, desktop_filename: Option) -> Result<()> { + Ok(()) + } + + fn set_badge_label(&self, label: Option) -> Result<()> { + Ok(()) + } + + fn set_overlay_icon(&self, icon: Option>) -> Result<()> { + Ok(()) + } + + fn set_title_bar_style(&self, style: tauri_utils::TitleBarStyle) -> Result<()> { + Ok(()) + } + + fn set_traffic_light_position(&self, position: Position) -> Result<()> { + Ok(()) + } + + fn set_size_constraints( + &self, + constraints: tauri_runtime::window::WindowSizeConstraints, + ) -> Result<()> { + Ok(()) + } + + fn set_theme(&self, theme: Option) -> Result<()> { + Ok(()) + } + + fn set_enabled(&self, enabled: bool) -> Result<()> { + Ok(()) + } + + fn is_enabled(&self) -> Result { + Ok(true) + } + + fn is_always_on_top(&self) -> Result { + Ok(false) + } + + fn set_background_color(&self, color: Option) -> Result<()> { + Ok(()) + } +} + +#[derive(Debug, Clone)] +pub struct EventProxy {} + +impl EventLoopProxy for EventProxy { + fn send_event(&self, event: T) -> Result<()> { + Ok(()) + } +} + +#[derive(Debug)] +pub struct MockRuntime { + is_running: Arc, + pub context: RuntimeContext, + run_rx: Receiver, +} + +impl MockRuntime { + fn init() -> Self { + let is_running = Arc::new(AtomicBool::new(false)); + let (tx, rx) = sync_channel(256); + let context = RuntimeContext { + is_running: is_running.clone(), + windows: Default::default(), + shortcuts: Default::default(), + run_tx: tx, + main_thread_id: std::thread::current().id(), + next_window_id: Default::default(), + next_webview_id: Default::default(), + next_window_event_id: Default::default(), + next_webview_event_id: Default::default(), + }; + Self { + is_running, + context, + run_rx: rx, + } + } +} + +impl Runtime for MockRuntime { + type WindowDispatcher = MockWindowDispatcher; + type WebviewDispatcher = MockWebviewDispatcher; + type Handle = MockRuntimeHandle; + type EventLoopProxy = EventProxy; + + fn new(_args: RuntimeInitArgs) -> Result { + Ok(Self::init()) + } + + #[cfg(any( + windows, + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn new_any_thread(_args: RuntimeInitArgs) -> Result { + Ok(Self::init()) + } + + fn create_proxy(&self) -> EventProxy { + EventProxy {} + } + + fn handle(&self) -> Self::Handle { + MockRuntimeHandle { + context: self.context.clone(), + } + } + + fn create_window) + Send + 'static>( + &self, + pending: PendingWindow, + _after_window_creation: Option, + ) -> Result> { + let id = self.context.next_window_id(); + + let (webview_id, webviews) = if let Some(w) = &pending.webview { + (Some(self.context.next_webview_id()), vec![Webview]) + } else { + (None, Vec::new()) + }; + + self.context.windows.borrow_mut().insert( + id, + Window { + label: pending.label.clone(), + webviews, + }, + ); + + let webview = webview_id.map(|id| { + let pending_webview = pending.webview.unwrap(); + DetachedWindowWebview { + webview: DetachedWebview { + label: pending.label.clone(), + dispatcher: MockWebviewDispatcher { + id, + context: self.context.clone(), + url: Arc::new(Mutex::new(pending_webview.url)), + last_evaluated_script: Default::default(), + }, + }, + use_https_scheme: pending_webview.webview_attributes.use_https_scheme, + } + }); + + Ok(DetachedWindow { + id, + label: pending.label, + dispatcher: MockWindowDispatcher { + id, + context: self.context.clone(), + }, + webview, + }) + } + + fn create_webview( + &self, + window_id: WindowId, + pending: PendingWebview, + ) -> Result> { + let id = self.context.next_webview_id(); + let webview = Webview; + if let Some(w) = self.context.windows.borrow_mut().get_mut(&window_id) { + w.webviews.push(webview); + } + + Ok(DetachedWebview { + label: pending.label, + dispatcher: MockWebviewDispatcher { + id, + context: self.context.clone(), + last_evaluated_script: Default::default(), + url: Arc::new(Mutex::new(pending.url)), + }, + }) + } + + fn primary_monitor(&self) -> Option { + unimplemented!() + } + + fn monitor_from_point(&self, x: f64, y: f64) -> Option { + unimplemented!() + } + + fn available_monitors(&self) -> Vec { + unimplemented!() + } + + fn set_theme(&self, theme: Option) { + unimplemented!() + } + + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + fn set_activation_policy(&mut self, activation_policy: tauri_runtime::ActivationPolicy) {} + + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + fn set_dock_visibility(&mut self, visible: bool) {} + + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + fn set_activate_ignoring_other_apps(&mut self, ignore: bool) {} + + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + fn show(&self) {} + + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + fn hide(&self) {} + + fn set_device_event_filter(&mut self, filter: DeviceEventFilter) {} + + #[cfg(any( + target_os = "macos", + windows, + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn run_iteration)>(&mut self, callback: F) {} + + fn run_return) + 'static>(self, callback: F) -> i32 { + self.run(callback); + + 0 + } + + fn run) + 'static>(self, mut callback: F) { + self.is_running.store(true, Ordering::Relaxed); + callback(RunEvent::Ready); + + loop { + if let Ok(m) = self.run_rx.try_recv() { + match m { + Message::Task(p) => p(), + Message::CloseWindow(id) => { + let label = self + .context + .windows + .borrow() + .get(&id) + .map(|w| w.label.clone()); + if let Some(label) = label { + let (tx, rx) = channel(); + callback(RunEvent::WindowEvent { + label, + event: WindowEvent::CloseRequested { signal_tx: tx }, + }); + + let should_prevent = matches!(rx.try_recv(), Ok(true)); + if !should_prevent { + self.context.windows.borrow_mut().remove(&id); + + let is_empty = self.context.windows.borrow().is_empty(); + if is_empty { + let (tx, rx) = channel(); + callback(RunEvent::ExitRequested { code: None, tx }); + + let recv = rx.try_recv(); + let should_prevent = matches!(recv, Ok(ExitRequestedEventAction::Prevent)); + + if !should_prevent { + break; + } + } + } + } + } + Message::DestroyWindow(id) => { + let removed = self.context.windows.borrow_mut().remove(&id).is_some(); + if removed { + let is_empty = self.context.windows.borrow().is_empty(); + if is_empty { + let (tx, rx) = channel(); + callback(RunEvent::ExitRequested { code: None, tx }); + + let recv = rx.try_recv(); + let should_prevent = matches!(recv, Ok(ExitRequestedEventAction::Prevent)); + + if !should_prevent { + break; + } + } + } + } + } + } + + callback(RunEvent::MainEventsCleared); + + std::thread::sleep(std::time::Duration::from_secs(1)); + } + + callback(RunEvent::Exit); + } + + fn cursor_position(&self) -> Result> { + Ok(PhysicalPosition::new(0.0, 0.0)) + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/test/mod.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/test/mod.rs new file mode 100644 index 000000000..9ad062c9b --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/test/mod.rs @@ -0,0 +1,339 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Utilities for unit testing on Tauri applications. +//! +//! # Stability +//! +//! This module is unstable. +//! +//! # Examples +//! +//! ```rust +//! use tauri::test::{mock_builder, mock_context, noop_assets}; +//! +//! #[tauri::command] +//! fn ping() -> &'static str { +//! "pong" +//! } +//! +//! fn create_app(builder: tauri::Builder) -> tauri::App { +//! builder +//! .invoke_handler(tauri::generate_handler![ping]) +//! // remove the string argument to use your app's config file +//! .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) +//! .expect("failed to build app") +//! } +//! +//! fn main() { +//! // Use `tauri::Builder::default()` to use the default runtime rather than the `MockRuntime`; +//! // let app = create_app(tauri::Builder::default()); +//! let app = create_app(mock_builder()); +//! let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default()).build().unwrap(); +//! +//! // run the `ping` command and assert it returns `pong` +//! let res = tauri::test::get_ipc_response( +//! &webview, +//! tauri::webview::InvokeRequest { +//! cmd: "ping".into(), +//! callback: tauri::ipc::CallbackFn(0), +//! error: tauri::ipc::CallbackFn(1), +//! // alternatively use "tauri://localhost" +//! url: "http://tauri.localhost".parse().unwrap(), +//! body: tauri::ipc::InvokeBody::default(), +//! headers: Default::default(), +//! invoke_key: tauri::test::INVOKE_KEY.to_string(), +//! }, +//! ).map(|b| b.deserialize::().unwrap()); +//! } +//! ``` + +#![allow(unused_variables)] + +mod mock_runtime; +pub use mock_runtime::*; +use serde::Serialize; +use serialize_to_javascript::DefaultTemplate; + +use std::{borrow::Cow, collections::HashMap, fmt::Debug}; + +use crate::{ + App, Assets, Builder, Context, Pattern, Runtime, Webview, + ipc::{InvokeError, InvokeResponse, InvokeResponseBody}, + webview::InvokeRequest, +}; +use tauri_utils::{ + acl::resolved::Resolved, + assets::{AssetKey, AssetsIter, CspHash}, + config::{AppConfig, Config}, +}; + +/// The invoke key used for tests. +pub const INVOKE_KEY: &str = "__invoke-key__"; + +/// An empty [`Assets`] implementation. +pub struct NoopAsset { + assets: HashMap>, + csp_hashes: Vec>, +} + +impl Assets for NoopAsset { + fn get(&self, key: &AssetKey) -> Option> { + None + } + + fn iter(&self) -> Box> { + Box::new( + self + .assets + .iter() + .map(|(k, b)| (Cow::Borrowed(k.as_str()), Cow::Borrowed(b.as_slice()))), + ) + } + + fn csp_hashes(&self, html_path: &AssetKey) -> Box> + '_> { + Box::new(self.csp_hashes.iter().copied()) + } +} + +/// Creates a new empty [`Assets`] implementation. +pub fn noop_assets() -> NoopAsset { + NoopAsset { + assets: Default::default(), + csp_hashes: Default::default(), + } +} + +/// Creates a new [`crate::Context`] for testing. +pub fn mock_context>(assets: A) -> crate::Context { + Context { + config: Config { + schema: None, + product_name: Default::default(), + main_binary_name: Default::default(), + version: Default::default(), + identifier: Default::default(), + app: AppConfig { + with_global_tauri: Default::default(), + windows: Vec::new(), + security: Default::default(), + tray_icon: None, + macos_private_api: false, + enable_gtk_app_id: false, + app_directories_override: None, + }, + bundle: Default::default(), + build: Default::default(), + plugins: Default::default(), + }, + assets: Box::new(assets), + default_window_icon: None, + app_icon: None, + #[cfg(all(desktop, feature = "tray-icon"))] + tray_icon: None, + package_info: crate::PackageInfo { + name: "test".into(), + version: "0.1.0".parse().unwrap(), + authors: "Tauri", + description: "Tauri test", + crate_name: "test", + }, + pattern: Pattern::Brownfield, + runtime_authority: crate::runtime_authority!(Default::default(), Resolved::default()), + plugin_global_api_scripts: None, + + #[cfg(dev)] + config_parent: None, + } +} + +/// Creates a new [`Builder`] using the [`MockRuntime`]. +/// +/// To use a dummy [`Context`], see [`mock_app`]. +/// +/// # Examples +/// +/// ```rust +/// #[cfg(test)] +/// fn do_something() { +/// let app = tauri::test::mock_builder() +/// // remove the string argument to use your app's config file +/// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) +/// .unwrap(); +/// } +/// ``` +pub fn mock_builder() -> Builder { + let mut builder = Builder::::new().enable_macos_default_menu(false); + + builder.invoke_initialization_script = crate::app::InvokeInitializationScript { + process_ipc_message_fn: crate::manager::webview::PROCESS_IPC_MESSAGE_FN, + os_name: std::env::consts::OS, + fetch_channel_data_command: crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND, + invoke_key: INVOKE_KEY, + } + .render_default(&Default::default()) + .unwrap() + .into_string(); + + builder.invoke_key = INVOKE_KEY.to_string(); + + builder +} + +/// Creates a new [`App`] for testing using the [`mock_context`] with a [`noop_assets`]. +pub fn mock_app() -> App { + mock_builder().build(mock_context(noop_assets())).unwrap() +} + +/// Executes the given IPC message and assert the response matches the expected value. +/// +/// # Examples +/// +/// ```rust +/// use tauri::test::{mock_builder, mock_context, noop_assets}; +/// +/// #[tauri::command] +/// fn ping() -> &'static str { +/// "pong" +/// } +/// +/// fn create_app(builder: tauri::Builder) -> tauri::App { +/// builder +/// .invoke_handler(tauri::generate_handler![ping]) +/// // remove the string argument to use your app's config file +/// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) +/// .expect("failed to build app") +/// } +/// +/// fn main() { +/// let app = create_app(mock_builder()); +/// let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default()).build().unwrap(); +/// +/// // run the `ping` command and assert it returns `pong` +/// tauri::test::assert_ipc_response( +/// &webview, +/// tauri::webview::InvokeRequest { +/// cmd: "ping".into(), +/// callback: tauri::ipc::CallbackFn(0), +/// error: tauri::ipc::CallbackFn(1), +/// url: if cfg!(any(windows, target_os = "android")) { +/// "http://tauri.localhost" +/// } else { +/// "tauri://localhost" +/// }.parse().unwrap(), +/// body: tauri::ipc::InvokeBody::default(), +/// headers: Default::default(), +/// invoke_key: tauri::test::INVOKE_KEY.to_string(), +/// }, +/// Ok("pong") +/// ); +/// } +/// ``` +pub fn assert_ipc_response< + T: Serialize + Debug + Send + Sync + 'static, + W: AsRef>, +>( + webview: &W, + request: InvokeRequest, + expected: Result, +) { + let response = + get_ipc_response(webview, request).map(|b| b.deserialize::().unwrap()); + assert_eq!( + response, + expected + .map(|e| serde_json::to_value(e).unwrap()) + .map_err(|e| serde_json::to_value(e).unwrap()) + ); +} + +#[allow(clippy::needless_doctest_main)] +/// Executes the given IPC message and get the return value. +/// +/// # Examples +/// +/// ```rust +/// use tauri::test::{mock_builder, mock_context, noop_assets}; +/// +/// #[tauri::command] +/// fn ping() -> &'static str { +/// "pong" +/// } +/// +/// fn create_app(builder: tauri::Builder) -> tauri::App { +/// builder +/// .invoke_handler(tauri::generate_handler![ping]) +/// // remove the string argument to use your app's config file +/// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) +/// .expect("failed to build app") +/// } +/// +/// fn main() { +/// let app = create_app(mock_builder()); +/// let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default()).build().unwrap(); +/// +/// // run the `ping` command and assert it returns `pong` +/// let res = tauri::test::get_ipc_response( +/// &webview, +/// tauri::webview::InvokeRequest { +/// cmd: "ping".into(), +/// callback: tauri::ipc::CallbackFn(0), +/// error: tauri::ipc::CallbackFn(1), +/// url: if cfg!(any(windows, target_os = "android")) { +/// "http://tauri.localhost" +/// } else { +/// "tauri://localhost" +/// }.parse().unwrap(), +/// body: tauri::ipc::InvokeBody::default(), +/// headers: Default::default(), +/// invoke_key: tauri::test::INVOKE_KEY.to_string(), +/// }, +/// ); +/// assert!(res.is_ok()); +/// assert_eq!(res.unwrap().deserialize::().unwrap(), String::from("pong")); +/// } +///``` +pub fn get_ipc_response>>( + webview: &W, + request: InvokeRequest, +) -> Result { + let (tx, rx) = std::sync::mpsc::sync_channel(1); + webview.as_ref().clone().on_message( + request, + Box::new(move |_window, _cmd, response, _callback, _error| { + tx.send(response).unwrap(); + }), + ); + + let res = rx.recv().expect("Failed to receive result from command"); + match res { + InvokeResponse::Ok(b) => Ok(b), + InvokeResponse::Err(InvokeError(v)) => Err(v), + } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use super::mock_app; + + #[test] + fn run_app() { + let app = mock_app(); + + let w = crate::WebviewWindowBuilder::new(&app, "main", Default::default()) + .build() + .unwrap(); + + std::thread::spawn(move || { + std::thread::sleep(Duration::from_secs(1)); + w.close().unwrap(); + }); + + app.run(|_app, event| { + println!("{event:?}"); + }); + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/tray/mod.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/tray/mod.rs new file mode 100644 index 000000000..dea5feff2 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/tray/mod.rs @@ -0,0 +1,701 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Tray icon types and utilities. + +pub(crate) mod plugin; + +use crate::app::{GlobalMenuEventListener, GlobalTrayIconEventListener}; +use crate::menu::ContextMenu; +use crate::menu::MenuEvent; +use crate::resources::Resource; +use crate::{ + AppHandle, Manager, PhysicalPosition, Rect, Runtime, image::Image, menu::run_item_main_thread, +}; +use crate::{ResourceId, UnsafeSend}; +use serde::Serialize; +use std::path::Path; +pub use tray_icon::TrayIconId; + +/// Describes the mouse button state. +#[derive(Default, Clone, Copy, PartialEq, Eq, Debug, Serialize)] +pub enum MouseButtonState { + /// Mouse button pressed. + #[default] + Up, + /// Mouse button released. + Down, +} + +impl From for MouseButtonState { + fn from(value: tray_icon::MouseButtonState) -> Self { + match value { + tray_icon::MouseButtonState::Up => MouseButtonState::Up, + tray_icon::MouseButtonState::Down => MouseButtonState::Down, + } + } +} + +/// Describes which mouse button triggered the event.. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Default)] +pub enum MouseButton { + /// Left mouse button. + #[default] + Left, + /// Right mouse button. + Right, + /// Middle mouse button. + Middle, +} + +impl From for MouseButton { + fn from(value: tray_icon::MouseButton) -> Self { + match value { + tray_icon::MouseButton::Left => MouseButton::Left, + tray_icon::MouseButton::Right => MouseButton::Right, + tray_icon::MouseButton::Middle => MouseButton::Middle, + } + } +} + +/// Describes a tray icon event. +/// +/// ## Platform-specific: +/// +/// - **Linux**: Unsupported. The event is not emitted even though the icon is shown +/// and will still show a context menu on right click. +#[derive(Debug, Clone, Serialize)] +#[serde(tag = "type")] +#[non_exhaustive] +pub enum TrayIconEvent { + /// A click happened on the tray icon. + #[serde(rename_all = "camelCase")] + Click { + /// Id of the tray icon which triggered this event. + id: TrayIconId, + /// Physical Position of this event. + position: PhysicalPosition, + /// Position and size of the tray icon. + rect: Rect, + /// Mouse button that triggered this event. + button: MouseButton, + /// Mouse button state when this event was triggered. + button_state: MouseButtonState, + }, + /// A double click happened on the tray icon. **Windows Only** + DoubleClick { + /// Id of the tray icon which triggered this event. + id: TrayIconId, + /// Physical Position of this event. + position: PhysicalPosition, + /// Position and size of the tray icon. + rect: Rect, + /// Mouse button that triggered this event. + button: MouseButton, + }, + /// The mouse entered the tray icon region. + Enter { + /// Id of the tray icon which triggered this event. + id: TrayIconId, + /// Physical Position of this event. + position: PhysicalPosition, + /// Position and size of the tray icon. + rect: Rect, + }, + /// The mouse moved over the tray icon region. + Move { + /// Id of the tray icon which triggered this event. + id: TrayIconId, + /// Physical Position of this event. + position: PhysicalPosition, + /// Position and size of the tray icon. + rect: Rect, + }, + /// The mouse left the tray icon region. + Leave { + /// Id of the tray icon which triggered this event. + id: TrayIconId, + /// Physical Position of this event. + position: PhysicalPosition, + /// Position and size of the tray icon. + rect: Rect, + }, +} + +impl TrayIconEvent { + /// Get the id of the tray icon that triggered this event. + pub fn id(&self) -> &TrayIconId { + match self { + TrayIconEvent::Click { id, .. } => id, + TrayIconEvent::DoubleClick { id, .. } => id, + TrayIconEvent::Enter { id, .. } => id, + TrayIconEvent::Move { id, .. } => id, + TrayIconEvent::Leave { id, .. } => id, + } + } +} + +impl From for TrayIconEvent { + fn from(value: tray_icon::TrayIconEvent) -> Self { + match value { + tray_icon::TrayIconEvent::Click { + id, + position, + rect, + button, + button_state, + } => TrayIconEvent::Click { + id, + position, + rect: Rect { + position: rect.position.into(), + size: rect.size.into(), + }, + button: button.into(), + button_state: button_state.into(), + }, + tray_icon::TrayIconEvent::DoubleClick { + id, + position, + rect, + button, + } => TrayIconEvent::DoubleClick { + id, + position, + rect: Rect { + position: rect.position.into(), + size: rect.size.into(), + }, + button: button.into(), + }, + tray_icon::TrayIconEvent::Enter { id, position, rect } => TrayIconEvent::Enter { + id, + position, + rect: Rect { + position: rect.position.into(), + size: rect.size.into(), + }, + }, + tray_icon::TrayIconEvent::Move { id, position, rect } => TrayIconEvent::Move { + id, + position, + rect: Rect { + position: rect.position.into(), + size: rect.size.into(), + }, + }, + tray_icon::TrayIconEvent::Leave { id, position, rect } => TrayIconEvent::Leave { + id, + position, + rect: Rect { + position: rect.position.into(), + size: rect.size.into(), + }, + }, + _ => todo!(), + } + } +} + +/// [`TrayIcon`] builder struct and associated methods. +#[derive(Default)] +pub struct TrayIconBuilder { + on_menu_event: Option>>, + on_tray_icon_event: Option>>, + inner: tray_icon::TrayIconBuilder, +} + +impl TrayIconBuilder { + /// Creates a new tray icon builder. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Sometimes the icon won't be visible unless a menu is set. + /// Setting an empty [`Menu`](crate::menu::Menu) is enough. + pub fn new() -> Self { + Self { + inner: tray_icon::TrayIconBuilder::new(), + on_menu_event: None, + on_tray_icon_event: None, + } + } + + /// Creates a new tray icon builder with the specified id. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Sometimes the icon won't be visible unless a menu is set. + /// Setting an empty [`Menu`](crate::menu::Menu) is enough. + pub fn with_id>(id: I) -> Self { + let mut builder = Self::new(); + builder.inner = builder.inner.with_id(id); + builder + } + + /// Set the a menu for this tray icon. + /// + /// ## Platform-specific: + /// + /// - **Linux**: once a menu is set, it cannot be removed or replaced but you can change its content. + pub fn menu(mut self, menu: &M) -> Self { + self.inner = self.inner.with_menu(menu.inner_context_owned()); + self + } + + /// Set an icon for this tray icon. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Sometimes the icon won't be visible unless a menu is set. + /// Setting an empty [`Menu`](crate::menu::Menu) is enough. + pub fn icon(mut self, icon: Image<'_>) -> Self { + let icon = icon.try_into().ok(); + if let Some(icon) = icon { + self.inner = self.inner.with_icon(icon); + } + self + } + + /// Set a tooltip for this tray icon. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn tooltip>(mut self, s: S) -> Self { + self.inner = self.inner.with_tooltip(s); + self + } + + /// Set the tray icon title. + /// + /// ## Platform-specific + /// + /// - **Linux:** The title will not be shown unless there is an icon + /// as well. The title is useful for numerical and other frequently + /// updated information. In general, it shouldn't be shown unless a + /// user requests it as it can take up a significant amount of space + /// on the user's panel. This may not be shown in all visualizations. + /// - **Windows:** Unsupported. + pub fn title>(mut self, title: S) -> Self { + self.inner = self.inner.with_title(title); + self + } + + /// Set tray icon temp dir path. **Linux only**. + /// + /// On Linux, we need to write the icon to the disk and usually it will + /// be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`. + pub fn temp_dir_path>(mut self, s: P) -> Self { + self.inner = self.inner.with_temp_dir_path(s); + self + } + + /// Use the icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**. + pub fn icon_as_template(mut self, is_template: bool) -> Self { + self.inner = self.inner.with_icon_as_template(is_template); + self + } + + /// Whether to show the tray menu on left click or not, default is `true`. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + #[deprecated( + since = "2.2.0", + note = "Use `TrayIconBuilder::show_menu_on_left_click` instead." + )] + pub fn menu_on_left_click(mut self, enable: bool) -> Self { + self.inner = self.inner.with_menu_on_left_click(enable); + self + } + + /// Whether to show the tray menu on left click or not, default is `true`. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn show_menu_on_left_click(mut self, enable: bool) -> Self { + self.inner = self.inner.with_menu_on_left_click(enable); + self + } + + /// Set a handler for menu events. + /// + /// Note that this handler is called for any menu event, + /// whether it is coming from this window, another window or from the tray icon menu. + pub fn on_menu_event, MenuEvent) + Sync + Send + 'static>( + mut self, + f: F, + ) -> Self { + self.on_menu_event.replace(Box::new(f)); + self + } + + /// Set a handler for this tray icon events. + pub fn on_tray_icon_event, TrayIconEvent) + Sync + Send + 'static>( + mut self, + f: F, + ) -> Self { + self.on_tray_icon_event.replace(Box::new(f)); + self + } + + /// Access the unique id that will be assigned to the tray icon + /// this builder will create. + pub fn id(&self) -> &TrayIconId { + self.inner.id() + } + + pub(crate) fn build_inner( + self, + app_handle: &AppHandle, + ) -> crate::Result<(TrayIcon, ResourceId)> { + let id = self.id().clone(); + + // SAFETY: + // the menu within this builder was created on main thread + // and will be accessed on the main thread + let unsafe_builder = UnsafeSend(self.inner); + + let (tx, rx) = std::sync::mpsc::channel(); + let unsafe_tray = app_handle + .run_on_main_thread(move || { + // SAFETY: will only be accessed on main thread + let _ = tx.send(unsafe_builder.take().build().map(UnsafeSend)); + }) + .and_then(|_| rx.recv().map_err(|_| crate::Error::FailedToReceiveMessage))??; + + let icon = TrayIcon { + id, + inner: unsafe_tray.take(), + app_handle: app_handle.clone(), + }; + + let rid = icon.register( + &icon.app_handle, + self.on_menu_event, + self.on_tray_icon_event, + ); + + Ok((icon, rid)) + } + + /// Builds and adds a new [`TrayIcon`] to the system tray. + pub fn build>(self, manager: &M) -> crate::Result> { + let (icon, _rid) = self.build_inner(manager.app_handle())?; + Ok(icon) + } +} + +/// Tray icon struct and associated methods. +/// +/// This type is reference-counted and the icon is removed when the last instance is dropped. +/// +/// See [`TrayIconBuilder`] to construct this type. +#[tauri_macros::default_runtime(crate::Wry, wry)] +pub struct TrayIcon { + id: TrayIconId, + inner: tray_icon::TrayIcon, + app_handle: AppHandle, +} + +impl Clone for TrayIcon { + fn clone(&self) -> Self { + Self { + id: self.id.clone(), + inner: self.inner.clone(), + app_handle: self.app_handle.clone(), + } + } +} + +/// # Safety +/// +/// We make sure it always runs on the main thread. +unsafe impl Sync for TrayIcon {} +unsafe impl Send for TrayIcon {} + +impl TrayIcon { + fn register( + &self, + app_handle: &AppHandle, + on_menu_event: Option>>, + on_tray_icon_event: Option>>, + ) -> ResourceId { + if let Some(handler) = on_menu_event { + app_handle + .manager + .menu + .global_event_listeners + .lock() + .unwrap() + .push(handler); + } + + if let Some(handler) = on_tray_icon_event { + app_handle + .manager + .tray + .event_listeners + .lock() + .unwrap() + .insert(self.id.clone(), handler); + } + + let rid = app_handle.resources_table().add(self.clone()); + app_handle + .manager + .tray + .icons + .lock() + .unwrap() + .push((self.id().clone(), rid)); + rid + } + + /// The application handle associated with this type. + pub fn app_handle(&self) -> &AppHandle { + &self.app_handle + } + + /// Register a handler for menu events. + /// + /// Note that this handler is called for any menu event, + /// whether it is coming from this window, another window or from the tray icon menu. + pub fn on_menu_event, MenuEvent) + Sync + Send + 'static>(&self, f: F) { + self + .app_handle + .manager + .menu + .global_event_listeners + .lock() + .unwrap() + .push(Box::new(f)); + } + + /// Register a handler for this tray icon events. + pub fn on_tray_icon_event, TrayIconEvent) + Sync + Send + 'static>( + &self, + f: F, + ) { + self + .app_handle + .manager + .tray + .event_listeners + .lock() + .unwrap() + .insert(self.id.clone(), Box::new(f)); + } + + /// Returns the id associated with this tray icon. + pub fn id(&self) -> &TrayIconId { + &self.id + } + + /// Sets a new tray icon. If `None` is provided, it will remove the icon. + pub fn set_icon(&self, icon: Option>) -> crate::Result<()> { + let icon = match icon { + Some(i) => Some(i.try_into()?), + None => None, + }; + run_item_main_thread!(self, |self_: Self| self_.inner.set_icon(icon))?.map_err(Into::into) + } + + /// Sets a new tray menu. + /// + /// ## Platform-specific: + /// + /// - **Linux**: once a menu is set it cannot be removed so `None` has no effect + pub fn set_menu(&self, menu: Option) -> crate::Result<()> { + run_item_main_thread!(self, |self_: Self| { + self_.inner.set_menu(menu.map(|m| m.inner_context_owned())) + }) + } + + /// Sets the tooltip for this tray icon. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported + pub fn set_tooltip>(&self, tooltip: Option) -> crate::Result<()> { + let s = tooltip.map(|s| s.as_ref().to_string()); + run_item_main_thread!(self, |self_: Self| self_.inner.set_tooltip(s))?.map_err(Into::into) + } + + /// Sets the title for this tray icon. + /// + /// ## Platform-specific: + /// + /// - **Linux:** The title will not be shown unless there is an icon + /// as well. The title is useful for numerical and other frequently + /// updated information. In general, it shouldn't be shown unless a + /// user requests it as it can take up a significant amount of space + /// on the user's panel. This may not be shown in all visualizations. + /// - **Windows:** Unsupported + pub fn set_title>(&self, title: Option) -> crate::Result<()> { + let s = title.map(|s| s.as_ref().to_string()); + run_item_main_thread!(self, |self_: Self| self_.inner.set_title(s)) + } + + /// Show or hide this tray icon. + pub fn set_visible(&self, visible: bool) -> crate::Result<()> { + run_item_main_thread!(self, |self_: Self| self_.inner.set_visible(visible))?.map_err(Into::into) + } + + /// Sets the tray icon temp dir path. **Linux only**. + /// + /// On Linux, we need to write the icon to the disk and usually it will + /// be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`. + pub fn set_temp_dir_path>(&self, path: Option

) -> crate::Result<()> { + #[allow(unused)] + let p = path.map(|p| p.as_ref().to_path_buf()); + #[cfg(target_os = "linux")] + run_item_main_thread!(self, |self_: Self| self_.inner.set_temp_dir_path(p))?; + Ok(()) + } + + /// Sets the current icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**. + pub fn set_icon_as_template(&self, #[allow(unused)] is_template: bool) -> crate::Result<()> { + #[cfg(target_os = "macos")] + run_item_main_thread!(self, |self_: Self| { + self_.inner.set_icon_as_template(is_template) + })?; + Ok(()) + } + + /// Sets the tray icon and template status atomically. **macOS only**. + /// + /// On macOS, calling `set_icon` followed by `set_icon_as_template` causes a visible + /// flicker as the icon is rendered twice. This method sets both atomically to prevent that. + /// + /// ## Platform-specific: + /// + /// - **Linux / Windows:** Falls back to calling `set_icon`. + pub fn set_icon_with_as_template( + &self, + icon: Option>, + #[allow(unused)] is_template: bool, + ) -> crate::Result<()> { + #[cfg(target_os = "macos")] + { + let tray_icon = match icon { + Some(i) => Some(i.try_into()?), + None => None, + }; + run_item_main_thread!(self, |self_: Self| { + self_ + .inner + .set_icon_with_as_template(tray_icon, is_template) + })??; + } + #[cfg(not(target_os = "macos"))] + { + self.set_icon(icon)?; + } + Ok(()) + } + + /// Disable or enable showing the tray menu on left click. + /// + /// + /// ## Platform-specific: + /// + /// - **Linux**: Unsupported. + pub fn set_show_menu_on_left_click(&self, #[allow(unused)] enable: bool) -> crate::Result<()> { + #[cfg(any(target_os = "macos", windows))] + run_item_main_thread!(self, |self_: Self| { + self_.inner.set_show_menu_on_left_click(enable) + })?; + Ok(()) + } + + /// Get tray icon rect. + /// + /// ## Platform-specific: + /// + /// - **Linux**: Unsupported, always returns `None`. + pub fn rect(&self) -> crate::Result> { + run_item_main_thread!(self, |self_: Self| { + self_.inner.rect().map(|rect| Rect { + position: rect.position.into(), + size: rect.size.into(), + }) + }) + } + + /// Do something with the inner [`tray_icon::TrayIcon`] on main thread + /// + /// Note that `tray-icon` crate may be updated in minor releases of Tauri. + /// Therefore, it’s recommended to pin Tauri to at least a minor version when you’re using `with_inner_tray_icon`. + pub fn with_inner_tray_icon(&self, f: F) -> crate::Result + where + F: FnOnce(&tray_icon::TrayIcon) -> T + Send + 'static, + T: Send + 'static, + { + run_item_main_thread!(self, |self_: Self| { f(&self_.inner) }) + } +} + +impl Resource for TrayIcon { + fn close(self: std::sync::Arc) { + let mut icons = self.app_handle.manager.tray.icons.lock().unwrap(); + for (i, (tray_icon_id, _rid)) in icons.iter_mut().enumerate() { + if tray_icon_id == &self.id { + icons.swap_remove(i); + return; + } + } + } +} + +#[cfg(test)] +mod tests { + #[test] + fn tray_event_json_serialization() { + // NOTE: if this test is ever changed, you probably need to change `TrayIconEvent` in JS as well + + use super::*; + let event = TrayIconEvent::Click { + button: MouseButton::Left, + button_state: MouseButtonState::Down, + id: TrayIconId::new("id"), + position: crate::PhysicalPosition::default(), + rect: crate::Rect { + position: tray_icon::Rect::default().position.into(), + size: tray_icon::Rect::default().size.into(), + }, + }; + + let value = serde_json::to_value(&event).unwrap(); + assert_eq!( + value, + serde_json::json!({ + "type": "Click", + "button": "Left", + "buttonState": "Down", + "id": "id", + "position": { + "x": 0.0, + "y": 0.0, + }, + "rect": { + "size": { + "Physical": { + "width": 0, + "height": 0, + } + }, + "position": { + "Physical": { + "x": 0, + "y": 0, + } + }, + } + }) + ); + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/tray/plugin.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/tray/plugin.rs new file mode 100644 index 000000000..aed4873d9 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/tray/plugin.rs @@ -0,0 +1,253 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::path::PathBuf; +use std::sync::Arc; + +use anyhow::Context; +use serde::Deserialize; + +use crate::{ + AppHandle, Manager, Runtime, Webview, command, + image::JsImage, + ipc::Channel, + menu::{Menu, Submenu, plugin::ItemKind}, + plugin::{Builder, TauriPlugin}, + resources::ResourceId, + tray::TrayIconBuilder, +}; + +use super::{TrayIcon, TrayIconEvent}; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct TrayIconOptions { + id: Option, + menu: Option<(ResourceId, ItemKind)>, + icon: Option, + tooltip: Option, + title: Option, + temp_dir_path: Option, + icon_as_template: Option, + menu_on_left_click: Option, + show_menu_on_left_click: Option, +} + +#[command(root = "crate")] +fn new( + webview: Webview, + options: TrayIconOptions, + handler: Channel, +) -> crate::Result<(ResourceId, String)> { + let mut builder = if let Some(id) = options.id { + TrayIconBuilder::::with_id(id) + } else { + TrayIconBuilder::::new() + }; + + builder = builder.on_tray_icon_event(move |_tray, e| { + let _ = handler.send(e); + }); + + let resources_table = webview.resources_table(); + + if let Some((rid, kind)) = options.menu { + match kind { + ItemKind::Menu => { + let menu = resources_table.get::>(rid)?; + builder = builder.menu(&*menu); + } + ItemKind::Submenu => { + let submenu = resources_table.get::>(rid)?; + builder = builder.menu(&*submenu); + } + _ => return Err(crate::Error::UnexpectedMenuKind), + }; + } + if let Some(icon) = options.icon { + builder = builder.icon(Arc::unwrap_or_clone(icon.into_img(&resources_table)?)); + } + if let Some(tooltip) = options.tooltip { + builder = builder.tooltip(tooltip); + } + if let Some(title) = options.title { + builder = builder.title(title); + } + if let Some(temp_dir_path) = options.temp_dir_path { + builder = builder.temp_dir_path(temp_dir_path); + } + if let Some(icon_as_template) = options.icon_as_template { + builder = builder.icon_as_template(icon_as_template); + } + #[allow(deprecated)] + if let Some(menu_on_left_click) = options.menu_on_left_click { + builder = builder.menu_on_left_click(menu_on_left_click); + } + if let Some(show_menu_on_left_click) = options.show_menu_on_left_click { + builder = builder.show_menu_on_left_click(show_menu_on_left_click); + } + + let (tray, rid) = builder.build_inner(webview.app_handle())?; + let id = tray.id().as_ref().to_string(); + + Ok((rid, id)) +} + +#[command(root = "crate")] +fn get_by_id(app: AppHandle, id: &str) -> Option { + app.manager.tray.tray_resource_by_id(id) +} + +#[command(root = "crate")] +fn remove_by_id(app: AppHandle, id: &str) -> crate::Result<()> { + app + .remove_tray_by_id(id) + .with_context(|| format!("Can't find a tray associated with this id: {id}"))?; + Ok(()) +} + +#[command(root = "crate")] +fn set_icon( + app: AppHandle, + webview: Webview, + rid: ResourceId, + icon: Option, +) -> crate::Result<()> { + let resources_table = app.resources_table(); + let tray = resources_table.get::>(rid)?; + let webview_resources_table = webview.resources_table(); + let icon = match icon { + Some(i) => Some(Arc::unwrap_or_clone(i.into_img(&webview_resources_table)?)), + None => None, + }; + tray.set_icon(icon) +} + +#[command(root = "crate")] +fn set_menu( + app: AppHandle, + webview: Webview, + rid: ResourceId, + menu: Option<(ResourceId, ItemKind)>, +) -> crate::Result<()> { + let resources_table = app.resources_table(); + let tray = resources_table.get::>(rid)?; + if let Some((rid, kind)) = menu { + let webview_resources_table = webview.resources_table(); + match kind { + ItemKind::Menu => { + let menu = webview_resources_table.get::>(rid)?; + tray.set_menu(Some((*menu).clone()))?; + } + ItemKind::Submenu => { + let submenu = webview_resources_table.get::>(rid)?; + tray.set_menu(Some((*submenu).clone()))?; + } + _ => return Err(crate::Error::UnexpectedMenuKind), + }; + } else { + tray.set_menu(None::>)?; + } + Ok(()) +} + +#[command(root = "crate")] +fn set_tooltip( + app: AppHandle, + rid: ResourceId, + tooltip: Option, +) -> crate::Result<()> { + let resources_table = app.resources_table(); + let tray = resources_table.get::>(rid)?; + tray.set_tooltip(tooltip) +} + +#[command(root = "crate")] +fn set_title( + app: AppHandle, + rid: ResourceId, + title: Option, +) -> crate::Result<()> { + let resources_table = app.resources_table(); + let tray = resources_table.get::>(rid)?; + tray.set_title(title) +} + +#[command(root = "crate")] +fn set_visible(app: AppHandle, rid: ResourceId, visible: bool) -> crate::Result<()> { + let resources_table = app.resources_table(); + let tray = resources_table.get::>(rid)?; + tray.set_visible(visible) +} + +#[command(root = "crate")] +fn set_temp_dir_path( + app: AppHandle, + rid: ResourceId, + path: Option, +) -> crate::Result<()> { + let resources_table = app.resources_table(); + let tray = resources_table.get::>(rid)?; + tray.set_temp_dir_path(path) +} + +#[command(root = "crate")] +fn set_icon_as_template( + app: AppHandle, + rid: ResourceId, + as_template: bool, +) -> crate::Result<()> { + let resources_table = app.resources_table(); + let tray = resources_table.get::>(rid)?; + tray.set_icon_as_template(as_template) +} + +#[command(root = "crate")] +fn set_icon_with_as_template( + app: AppHandle, + webview: Webview, + rid: ResourceId, + icon: Option, + as_template: bool, +) -> crate::Result<()> { + let resources_table = app.resources_table(); + let tray = resources_table.get::>(rid)?; + let webview_resources_table = webview.resources_table(); + let icon = match icon { + Some(i) => Some(Arc::unwrap_or_clone(i.into_img(&webview_resources_table)?)), + None => None, + }; + tray.set_icon_with_as_template(icon, as_template) +} + +#[command(root = "crate")] +fn set_show_menu_on_left_click( + app: AppHandle, + rid: ResourceId, + on_left: bool, +) -> crate::Result<()> { + let resources_table = app.resources_table(); + let tray = resources_table.get::>(rid)?; + tray.set_show_menu_on_left_click(on_left) +} + +pub(crate) fn init() -> TauriPlugin { + Builder::new("tray") + .invoke_handler(crate::generate_handler![ + #![plugin(tray)] + new, + get_by_id, + remove_by_id, + set_icon, + set_menu, + set_tooltip, + set_title, + set_visible, + set_temp_dir_path, + set_icon_as_template, + set_icon_with_as_template, + set_show_menu_on_left_click, + ]) + .build() +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/vibrancy/macos.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/vibrancy/macos.rs new file mode 100644 index 000000000..823cab5a6 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/vibrancy/macos.rs @@ -0,0 +1,120 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![allow(deprecated)] + +use crate::utils::config::WindowEffectsConfig; +use crate::window::{Effect, EffectState}; +use raw_window_handle::HasWindowHandle; +use window_vibrancy::{ + LiquidGlassOptions, NSGlassEffectViewStyle, NSVisualEffectMaterial, NSVisualEffectState, +}; + +pub fn apply_effects(window: impl HasWindowHandle, effects: WindowEffectsConfig) { + let WindowEffectsConfig { + effects, + radius, + state, + color, + interactive, + } = effects; + + // window-vibrancy inserts a new subview on every call, so drop the previous effect first + clear_effects(&window); + + if let Some(effect) = effects + .iter() + .find(|e| matches!(e, Effect::LiquidGlassRegular | Effect::LiquidGlassClear)) + { + let mut options = LiquidGlassOptions::new(match effect { + Effect::LiquidGlassRegular => NSGlassEffectViewStyle::Regular, + Effect::LiquidGlassClear => NSGlassEffectViewStyle::Clear, + _ => unreachable!(), + }) + .interactive(interactive); + + if let Some(color) = color { + options = options.tint_color(color.into()); + } + if let Some(radius) = radius { + options = options.radius(radius); + } + + match window_vibrancy::apply_liquid_glass(&window, options) { + Ok(()) => return, + // macOS 15 and below: fall back to the Visual Effect material, if any + Err(window_vibrancy::Error::UnsupportedPlatformVersion(_)) => {} + Err(e) => { + log::error!("failed to apply liquid glass effect: {e}"); + return; + } + } + } + + let effect = if let Some(effect) = effects.into_iter().find(|e| { + matches!( + e, + Effect::AppearanceBased + | Effect::Light + | Effect::Dark + | Effect::MediumLight + | Effect::UltraDark + | Effect::Titlebar + | Effect::Selection + | Effect::Menu + | Effect::Popover + | Effect::Sidebar + | Effect::HeaderView + | Effect::Sheet + | Effect::WindowBackground + | Effect::HudWindow + | Effect::FullScreenUI + | Effect::Tooltip + | Effect::ContentBackground + | Effect::UnderWindowBackground + | Effect::UnderPageBackground + ) + }) { + effect + } else { + return; + }; + + window_vibrancy::apply_vibrancy( + window, + match effect { + Effect::AppearanceBased => NSVisualEffectMaterial::AppearanceBased, + Effect::Light => NSVisualEffectMaterial::Light, + Effect::Dark => NSVisualEffectMaterial::Dark, + Effect::MediumLight => NSVisualEffectMaterial::MediumLight, + Effect::UltraDark => NSVisualEffectMaterial::UltraDark, + Effect::Titlebar => NSVisualEffectMaterial::Titlebar, + Effect::Selection => NSVisualEffectMaterial::Selection, + Effect::Menu => NSVisualEffectMaterial::Menu, + Effect::Popover => NSVisualEffectMaterial::Popover, + Effect::Sidebar => NSVisualEffectMaterial::Sidebar, + Effect::HeaderView => NSVisualEffectMaterial::HeaderView, + Effect::Sheet => NSVisualEffectMaterial::Sheet, + Effect::WindowBackground => NSVisualEffectMaterial::WindowBackground, + Effect::HudWindow => NSVisualEffectMaterial::HudWindow, + Effect::FullScreenUI => NSVisualEffectMaterial::FullScreenUI, + Effect::Tooltip => NSVisualEffectMaterial::Tooltip, + Effect::ContentBackground => NSVisualEffectMaterial::ContentBackground, + Effect::UnderWindowBackground => NSVisualEffectMaterial::UnderWindowBackground, + Effect::UnderPageBackground => NSVisualEffectMaterial::UnderPageBackground, + _ => unreachable!(), + }, + state.map(|s| match s { + EffectState::FollowsWindowActiveState => NSVisualEffectState::FollowsWindowActiveState, + EffectState::Active => NSVisualEffectState::Active, + EffectState::Inactive => NSVisualEffectState::Inactive, + }), + radius, + ); +} + +pub fn clear_effects(window: impl HasWindowHandle) { + let _ = window_vibrancy::clear_vibrancy(&window); + let _ = window_vibrancy::clear_liquid_glass(&window); +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/vibrancy/mod.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/vibrancy/mod.rs new file mode 100644 index 000000000..b5a67400f --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/vibrancy/mod.rs @@ -0,0 +1,32 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![allow(unused)] + +use tauri_utils::config::WindowEffectsConfig; + +use crate::{Runtime, Window}; + +#[cfg(target_os = "macos")] +mod macos; +#[cfg(windows)] +mod windows; + +pub fn set_window_effects( + window: &Window, + effects: Option, +) -> crate::Result<()> { + if let Some(_effects) = effects { + #[cfg(windows)] + windows::apply_effects(window, _effects); + #[cfg(target_os = "macos")] + macos::apply_effects(window, _effects); + } else { + #[cfg(windows)] + windows::clear_effects(window); + #[cfg(target_os = "macos")] + macos::clear_effects(window); + } + Ok(()) +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/vibrancy/windows.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/vibrancy/windows.rs new file mode 100644 index 000000000..ace786dc3 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/vibrancy/windows.rs @@ -0,0 +1,53 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![allow(non_snake_case)] +#![allow(non_camel_case_types)] +#![allow(clippy::upper_case_acronyms)] + +use std::ffi::c_void; + +use crate::utils::config::WindowEffectsConfig; +use crate::window::{Color, Effect}; +use raw_window_handle::HasWindowHandle; +use windows::Win32::Foundation::HWND; + +pub fn apply_effects(window: impl HasWindowHandle, effects: WindowEffectsConfig) { + let WindowEffectsConfig { effects, color, .. } = effects; + let effect = if let Some(effect) = effects.iter().find(|e| { + matches!( + e, + Effect::Mica + | Effect::MicaDark + | Effect::MicaLight + | Effect::Acrylic + | Effect::Blur + | Effect::Tabbed + | Effect::TabbedDark + | Effect::TabbedLight + ) + }) { + effect + } else { + return; + }; + + match effect { + Effect::Blur => window_vibrancy::apply_blur(window, color.map(Into::into)), + Effect::Acrylic => window_vibrancy::apply_acrylic(window, color.map(Into::into)), + Effect::Mica => window_vibrancy::apply_mica(window, None), + Effect::MicaDark => window_vibrancy::apply_mica(window, Some(true)), + Effect::MicaLight => window_vibrancy::apply_mica(window, Some(false)), + Effect::Tabbed => window_vibrancy::apply_tabbed(window, None), + Effect::TabbedDark => window_vibrancy::apply_tabbed(window, Some(true)), + Effect::TabbedLight => window_vibrancy::apply_tabbed(window, Some(false)), + _ => unreachable!(), + }; +} + +pub fn clear_effects(window: impl HasWindowHandle) { + window_vibrancy::clear_blur(&window); + window_vibrancy::clear_acrylic(&window); + window_vibrancy::clear_mica(&window); +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/webview/mod.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/webview/mod.rs new file mode 100644 index 000000000..980cab6af --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/webview/mod.rs @@ -0,0 +1,2848 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! The Tauri webview types and functions. + +pub(crate) mod plugin; +mod webview_window; + +pub use webview_window::{WebviewWindow, WebviewWindowBuilder}; + +/// Cookie crate used for [`Webview::set_cookie`] and [`Webview::delete_cookie`]. +/// +/// # Stability +/// +/// This re-exported crate is still on an alpha release and might receive updates in minor Tauri releases. +pub use cookie; +use http::HeaderMap; +use serde::Serialize; +use tauri_macros::default_runtime; +pub use tauri_runtime::webview::{ + NewWindowFeatures, PageLoadEvent, PermissionKind, PermissionResponse, ScrollBarStyle, +}; +// Remove this re-export in v3 +pub use tauri_runtime::Cookie; +use tauri_runtime::{ + WebviewDispatch, + webview::{DetachedWebview, InitializationScript, PendingWebview, WebviewAttributes}, +}; +#[cfg(desktop)] +use tauri_runtime::{ + WindowDispatch, + dpi::{PhysicalPosition, PhysicalSize, Position, Size}, +}; +pub use tauri_utils::config::Color; +use tauri_utils::config::{BackgroundThrottlingPolicy, WebviewUrl, WindowConfig}; +pub use url::Url; + +use crate::{ + AppHandle, Emitter, Event, EventId, EventLoopMessage, EventName, Listener, Manager, + ResourceTable, Runtime, Window, + app::{UriSchemeResponder, WebviewEvent}, + event::{EmitArgs, EventTarget}, + ipc::{ + CallbackFn, CommandArg, CommandItem, CommandScope, GlobalScope, Invoke, InvokeBody, + InvokeError, InvokeMessage, InvokeResolver, Origin, OwnedInvokeResponder, ScopeObject, + }, + manager::AppManager, + path::SafePathBuf, + sealed::{ManagerBase, RuntimeOrDispatch}, +}; + +use std::{ + borrow::Cow, + hash::{Hash, Hasher}, + path::{Path, PathBuf}, + sync::{Arc, Mutex, MutexGuard}, +}; + +pub(crate) type WebResourceRequestHandler = + dyn Fn(http::Request>, &mut http::Response>) + Send + Sync; +pub(crate) type NavigationHandler = dyn Fn(&Url) -> bool + Send; +pub(crate) type NewWindowHandler = dyn Fn(Url, NewWindowFeatures) -> NewWindowResponse + Send; +pub(crate) type UriSchemeProtocolHandler = + Box>, UriSchemeResponder) + Send + Sync>; +pub(crate) type OnPageLoad = dyn Fn(Webview, PageLoadPayload<'_>) + Send + Sync + 'static; +pub(crate) type OnDocumentTitleChanged = dyn Fn(Webview, String) + Send + 'static; +pub(crate) type DownloadHandler = dyn Fn(Webview, DownloadEvent<'_>) -> bool + Send + Sync; +pub(crate) type PermissionRequestHandler = + dyn Fn(Webview, PermissionKind) -> PermissionResponse + Send + Sync + 'static; + +#[derive(Clone, Serialize)] +pub(crate) struct CreatedEvent { + pub(crate) label: String, +} + +/// Download event for the [`WebviewBuilder#method.on_download`] hook. +#[non_exhaustive] +pub enum DownloadEvent<'a> { + /// Download requested. + Requested { + /// The url being downloaded. + url: Url, + /// Represents where the file will be downloaded to. + /// Can be used to set the download location by assigning a new path to it. + /// The assigned path _must_ be absolute. + destination: &'a mut PathBuf, + }, + /// Download finished. + Finished { + /// The URL of the original download request. + url: Url, + /// Potentially representing the filesystem path the file was downloaded to. + /// + /// A value of `None` being passed instead of a `PathBuf` does not necessarily indicate that the download + /// did not succeed, and may instead indicate some other failure - always check the third parameter if you need to + /// know if the download succeeded. + /// + /// ## Platform-specific: + /// + /// - **macOS**: The second parameter indicating the path the file was saved to is always empty, due to API + /// limitations. + path: Option, + /// Indicates if the download succeeded or not. + success: bool, + }, +} + +/// The payload for the [`WebviewBuilder::on_page_load`] hook. +#[derive(Debug, Clone)] +pub struct PageLoadPayload<'a> { + pub(crate) url: &'a Url, + pub(crate) event: PageLoadEvent, +} + +impl<'a> PageLoadPayload<'a> { + /// The page URL. + pub fn url(&self) -> &'a Url { + self.url + } + + /// The page load event. + pub fn event(&self) -> PageLoadEvent { + self.event + } +} + +/// The IPC invoke request. +/// +/// # Stability +/// +/// This struct is **NOT** part of the public stable API and is only meant to be used +/// by internal code and external testing/fuzzing tools or custom invoke systems. +#[derive(Debug)] +pub struct InvokeRequest { + /// The invoke command. + pub cmd: String, + /// The success callback. + pub callback: CallbackFn, + /// The error callback. + pub error: CallbackFn, + /// URL of the frame that requested this command. + pub url: Url, + /// The body of the request. + pub body: InvokeBody, + /// The request headers. + pub headers: HeaderMap, + /// The invoke key. Must match what was passed to the app manager. + pub invoke_key: String, +} + +/// The platform webview handle. Accessed with [`Webview#method.with_webview`]; +#[cfg(feature = "wry")] +#[cfg_attr(docsrs, doc(cfg(feature = "wry")))] +pub struct PlatformWebview(tauri_runtime_wry::Webview); + +#[cfg(feature = "wry")] +impl PlatformWebview { + /// Returns [`webkit2gtk::WebView`] handle. + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + #[cfg_attr( + docsrs, + doc(cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))) + )] + pub fn inner(&self) -> webkit2gtk::WebView { + self.0.clone() + } + + /// Returns the WebView2 controller. + #[cfg(windows)] + #[cfg_attr(docsrs, doc(cfg(windows)))] + pub fn controller( + &self, + ) -> webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Controller { + self.0.controller.clone() + } + + /// Returns the WebView2 environment. + #[cfg(windows)] + #[cfg_attr(docsrs, doc(cfg(windows)))] + pub fn environment( + &self, + ) -> webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Environment { + self.0.environment.clone() + } + + /// Returns the [WKWebView] handle. + /// + /// [WKWebView]: https://developer.apple.com/documentation/webkit/wkwebview + #[cfg(any(target_os = "macos", target_os = "ios"))] + #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "ios"))))] + pub fn inner(&self) -> *mut std::ffi::c_void { + self.0.webview + } + + /// Returns WKWebView [controller] handle. + /// + /// [controller]: https://developer.apple.com/documentation/webkit/wkusercontentcontroller + #[cfg(any(target_os = "macos", target_os = "ios"))] + #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "ios"))))] + pub fn controller(&self) -> *mut std::ffi::c_void { + self.0.manager + } + + /// Returns [NSWindow] associated with the WKWebView webview. + /// + /// [NSWindow]: https://developer.apple.com/documentation/appkit/nswindow + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + pub fn ns_window(&self) -> *mut std::ffi::c_void { + self.0.ns_window + } + + /// Returns [UIViewController] used by the WKWebView webview NSWindow. + /// + /// [UIViewController]: https://developer.apple.com/documentation/uikit/uiviewcontroller + #[cfg(target_os = "ios")] + #[cfg_attr(docsrs, doc(cfg(target_os = "ios")))] + pub fn view_controller(&self) -> *mut std::ffi::c_void { + self.0.view_controller + } + + /// Returns handle for JNI execution. + #[cfg(target_os = "android")] + pub fn jni_handle(&self) -> tauri_runtime_wry::wry::JniHandle { + self.0 + } +} + +/// Response for the new window request handler. +pub enum NewWindowResponse { + /// Allow the window to be opened with the default implementation. + Allow, + /// Allow the window to be opened, with the given window. + /// + /// ## Platform-specific: + /// + /// **Linux**: The webview must be related to the caller webview. See [`WebviewBuilder::with_related_view`]. + /// **Windows**: The webview must use the same environment as the caller webview. See [`WebviewBuilder::with_environment`]. + /// **macOS**: The webview must use the same webview configuration as the caller webview. See [`WebviewBuilder::with_webview_configuration`] and [`NewWindowFeatures::webview_configuration`]. + Create { + /// Window that was created. + window: crate::WebviewWindow, + }, + /// Deny the window from being opened. + Deny, +} + +macro_rules! unstable_struct { + (#[doc = $doc:expr] $($tokens:tt)*) => { + #[cfg(any(test, feature = "unstable"))] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] + #[doc = $doc] + pub $($tokens)* + + #[cfg(not(any(test, feature = "unstable")))] + pub(crate) $($tokens)* + } +} + +unstable_struct!( + #[doc = "A builder for a webview."] + struct WebviewBuilder { + pub(crate) label: String, + pub(crate) webview_attributes: WebviewAttributes, + pub(crate) web_resource_request_handler: Option>, + pub(crate) navigation_handler: Option>, + pub(crate) new_window_handler: Option>>, + pub(crate) on_page_load_handler: Option>>, + pub(crate) document_title_changed_handler: Option>>, + pub(crate) download_handler: Option>>, + pub(crate) permission_request_handler: Option>>, + } +); + +#[cfg_attr(not(feature = "unstable"), allow(dead_code))] +impl WebviewBuilder { + /// Initializes a webview builder with the given webview label and URL to load. + /// + /// # Known issues + /// + /// On Windows, this function deadlocks when used in a synchronous command or event handlers, see [the Webview2 issue]. + /// You should use `async` commands and separate threads when creating webviews. + /// + /// # Examples + /// + /// - Create a webview in the setup hook: + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +tauri::Builder::default() + .setup(|app| { + let window = tauri::window::WindowBuilder::new(app, "label").build()?; + let webview_builder = tauri::webview::WebviewBuilder::new("label", tauri::WebviewUrl::App("index.html".into())); + let webview = window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap()); + Ok(()) + }); +``` + "#### + )] + /// + /// - Create a webview in a separate thread: + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +tauri::Builder::default() + .setup(|app| { + let handle = app.handle().clone(); + std::thread::spawn(move || { + let window = tauri::window::WindowBuilder::new(&handle, "label").build().unwrap(); + let webview_builder = tauri::webview::WebviewBuilder::new("label", tauri::WebviewUrl::App("index.html".into())); + window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap()); + }); + Ok(()) + }); +``` + "#### + )] + /// + /// - Create a webview in a command: + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +#[tauri::command] +async fn create_window(app: tauri::AppHandle) { + let window = tauri::window::WindowBuilder::new(&app, "label").build().unwrap(); + let webview_builder = tauri::webview::WebviewBuilder::new("label", tauri::WebviewUrl::External("https://tauri.app/".parse().unwrap())); + window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap()); +} +``` + "#### + )] + /// + /// [the Webview2 issue]: https://github.com/tauri-apps/wry/issues/583 + pub fn new>(label: L, url: WebviewUrl) -> Self { + Self { + label: label.into(), + webview_attributes: WebviewAttributes::new(url), + web_resource_request_handler: None, + navigation_handler: None, + new_window_handler: None, + on_page_load_handler: None, + document_title_changed_handler: None, + download_handler: None, + permission_request_handler: None, + } + } + + /// Initializes a webview builder from a [`WindowConfig`] from tauri.conf.json. + /// Keep in mind that you can't create 2 webviews with the same `label` so make sure + /// that the initial webview was closed or change the label of the new [`WebviewBuilder`]. + /// + /// # Known issues + /// + /// On Windows, this function deadlocks when used in a synchronous command or event handlers, see [the Webview2 issue]. + /// You should use `async` commands and separate threads when creating webviews. + /// + /// # Examples + /// + /// - Create a webview in a command: + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +#[tauri::command] +async fn create_window(app: tauri::AppHandle) { + let window = tauri::window::WindowBuilder::new(&app, "label").build().unwrap(); + let webview_builder = tauri::webview::WebviewBuilder::from_config(&app.config().app.windows.get(0).unwrap().clone()); + window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap()); +} +``` + "#### + )] + /// + /// [the Webview2 issue]: https://github.com/tauri-apps/wry/issues/583 + pub fn from_config(config: &WindowConfig) -> Self { + let mut config = config.to_owned(); + + if let Some(data_directory) = &config.data_directory { + let local_dir = dirs::data_local_dir(); + if local_dir.is_none() { + #[cfg(feature = "tracing")] + tracing::error!("failed to resolve data directory"); + } + let resolve_data_dir_res = local_dir + .and_then(|local_dir| { + SafePathBuf::new(data_directory.clone()) + .inspect_err(|_err| { + #[cfg(feature = "tracing")] + tracing::error!( + "data_directory `{}` is not a safe path, ignoring config. Validation error was: {_err}", + data_directory.display() + ); + }) + .map(|p| (local_dir, p)) + .ok() + }) + .and_then(|(local_dir, data_directory)| { + if data_directory.as_ref().is_relative() { + Some(local_dir.join(&config.label).join(data_directory.as_ref())) + } else { + #[cfg(feature = "tracing")] + tracing::error!( + "data_directory `{}` is not a relative path, ignoring config.", + data_directory.display() + ); + None + } + }); + if let Some(resolved_data_directory) = resolve_data_dir_res { + config.data_directory = Some(resolved_data_directory); + } + } + + Self { + label: config.label.clone(), + webview_attributes: WebviewAttributes::from(&config), + web_resource_request_handler: None, + navigation_handler: None, + new_window_handler: None, + on_page_load_handler: None, + document_title_changed_handler: None, + download_handler: None, + permission_request_handler: None, + } + } + + /// Defines a closure to be executed when the webview makes an HTTP request for a web resource, allowing you to modify the response. + /// + /// Currently only implemented for the `tauri` URI protocol. + /// + /// **NOTE:** Currently this is **not** executed when using external URLs such as a development server, + /// but it might be implemented in the future. **Always** check the request URL. + /// + /// # Examples + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust,no_run +use tauri::{ + utils::config::{Csp, CspDirectiveSources, WebviewUrl}, + window::WindowBuilder, + webview::WebviewBuilder, +}; +use http::header::HeaderValue; +use std::collections::HashMap; +tauri::Builder::default() + .setup(|app| { + let window = tauri::window::WindowBuilder::new(app, "label").build()?; + + let webview_builder = WebviewBuilder::new("core", WebviewUrl::App("index.html".into())) + .on_web_resource_request(|request, response| { + if request.uri().scheme_str() == Some("tauri") { + // if we have a CSP header, Tauri is loading an HTML file + // for this example, let's dynamically change the CSP + if let Some(csp) = response.headers_mut().get_mut("Content-Security-Policy") { + // use the tauri helper to parse the CSP policy to a map + let mut csp_map: HashMap = Csp::Policy(csp.to_str().unwrap().to_string()).into(); + csp_map.entry("script-src".to_string()).or_insert_with(Default::default).push("'unsafe-inline'"); + // use the tauri helper to get a CSP string from the map + let csp_string = Csp::from(csp_map).to_string(); + *csp = HeaderValue::from_str(&csp_string).unwrap(); + } + } + }); + + let webview = window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap())?; + + Ok(()) + }); +``` + "#### + )] + pub fn on_web_resource_request< + F: Fn(http::Request>, &mut http::Response>) + Send + Sync + 'static, + >( + mut self, + f: F, + ) -> Self { + self.web_resource_request_handler.replace(Box::new(f)); + self + } + + /// Defines a closure to be executed when the webview navigates to a URL. Returning `false` cancels the navigation. + /// + /// # Examples + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust,no_run +use tauri::{ + utils::config::{Csp, CspDirectiveSources, WebviewUrl}, + window::WindowBuilder, + webview::WebviewBuilder, +}; +use http::header::HeaderValue; +use std::collections::HashMap; +tauri::Builder::default() + .setup(|app| { + let window = tauri::window::WindowBuilder::new(app, "label").build()?; + + let webview_builder = WebviewBuilder::new("core", WebviewUrl::App("index.html".into())) + .on_navigation(|url| { + // allow the production URL or localhost on dev + url.scheme() == "tauri" || (cfg!(dev) && url.host_str() == Some("localhost")) + }); + + let webview = window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap())?; + Ok(()) + }); +``` + "#### + )] + pub fn on_navigation bool + Send + 'static>(mut self, f: F) -> Self { + self.navigation_handler.replace(Box::new(f)); + self + } + + /// Set a new window request handler to decide if incoming url is allowed to be opened. + /// + /// A new window is requested to be opened by the [window.open] API. + /// + /// The closure take the URL to open and the window features object and returns [`NewWindowResponse`] to determine whether the window should open. + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust,no_run +use tauri::{ + utils::config::{Csp, CspDirectiveSources, WebviewUrl}, + window::WindowBuilder, + webview::WebviewBuilder, +}; +use http::header::HeaderValue; +use std::collections::HashMap; +tauri::Builder::default() + .setup(|app| { + let window = tauri::window::WindowBuilder::new(app, "label").build()?; + + let app_ = app.handle().clone(); + let webview_builder = WebviewBuilder::new("core", WebviewUrl::App("index.html".into())) + .on_new_window(move |url, features| { + let builder = tauri::WebviewWindowBuilder::new( + &app_, + // note: add an ID counter or random label generator to support multiple opened windows at the same time + "opened-window", + tauri::WebviewUrl::External("about:blank".parse().unwrap()), + ) + .window_features(features) + .on_document_title_changed(|window, title| { + window.set_title(&title).unwrap(); + }) + .title(url.as_str()); + + let window = builder.build().unwrap(); + tauri::webview::NewWindowResponse::Create { window } + }); + + let webview = window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap())?; + Ok(()) + }); +``` + "#### + )] + /// + /// # Platform-specific + /// + /// - **Android / iOS**: Not supported. + /// + /// [window.open]: https://developer.mozilla.org/en-US/docs/Web/API/Window/open + pub fn on_new_window NewWindowResponse + Send + 'static>( + mut self, + f: F, + ) -> Self { + self.new_window_handler.replace(Box::new(f)); + self + } + + /// Defines a closure to be executed when document title change. + pub fn on_document_title_changed, String) + Send + 'static>( + mut self, + f: F, + ) -> Self { + self.document_title_changed_handler.replace(Box::new(f)); + self + } + + /// Set a download event handler to be notified when a download is requested or finished. + /// + /// Returning `false` prevents the download from happening on a [`DownloadEvent::Requested`] event. + /// + /// # Examples + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust,no_run +use tauri::{ + utils::config::{Csp, CspDirectiveSources, WebviewUrl}, + window::WindowBuilder, + webview::{DownloadEvent, WebviewBuilder}, +}; + +tauri::Builder::default() + .setup(|app| { + let window = WindowBuilder::new(app, "label").build()?; + let webview_builder = WebviewBuilder::new("core", WebviewUrl::App("index.html".into())) + .on_download(|webview, event| { + match event { + DownloadEvent::Requested { url, destination } => { + println!("downloading {}", url); + *destination = "/home/tauri/target/path".into(); + } + DownloadEvent::Finished { url, path, success } => { + println!("downloaded {} to {:?}, success: {}", url, path, success); + } + _ => (), + } + // let the download start + true + }); + + let webview = window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap())?; + Ok(()) + }); +``` + "#### + )] + pub fn on_download, DownloadEvent<'_>) -> bool + Send + Sync + 'static>( + mut self, + f: F, + ) -> Self { + self.download_handler.replace(Arc::new(f)); + self + } + + /// Defines a closure to be executed when a page load event is triggered. + /// The event can be either [`PageLoadEvent::Started`] if the page has started loading + /// or [`PageLoadEvent::Finished`] when the page finishes loading. + /// + /// # Examples + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust,no_run +use tauri::{ + utils::config::{Csp, CspDirectiveSources, WebviewUrl}, + window::WindowBuilder, + webview::{PageLoadEvent, WebviewBuilder}, +}; +use http::header::HeaderValue; +use std::collections::HashMap; +tauri::Builder::default() + .setup(|app| { + let window = tauri::window::WindowBuilder::new(app, "label").build()?; + let webview_builder = WebviewBuilder::new("core", WebviewUrl::App("index.html".into())) + .on_page_load(|webview, payload| { + match payload.event() { + PageLoadEvent::Started => { + println!("{} finished loading", payload.url()); + } + PageLoadEvent::Finished => { + println!("{} finished loading", payload.url()); + } + } + }); + let webview = window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap())?; + Ok(()) + }); +``` + "#### + )] + pub fn on_page_load, PageLoadPayload<'_>) + Send + Sync + 'static>( + mut self, + f: F, + ) -> Self { + self.on_page_load_handler.replace(Box::new(f)); + self + } + + /// Defines a closure to be executed when a permission is requested. + /// + /// The handler receives the [`PermissionKind`] and should return + /// the desired [`PermissionResponse`]. + /// + /// This is called before [`crate::Builder::on_permission_request`], + /// and skips it if the returned response is not [`crate::webview::PermissionResponse::Default`]. + /// + /// > [!NOTE] + /// > This handler only triggers for new permission requests. If the user has already + /// > allowed or denied a permission persistently within the webview, the browser + /// > will use the saved preference instead of calling this handler. + /// + /// ## Platform-specific: + /// + /// - **Windows**: Fully supported via WebView2's PermissionRequested event. + /// - **macOS / iOS**: Fully supported via WKUIDelegate's requestMediaCapturePermission. + /// - **Linux**: Fully supported via WebKitGTK's permission-request signal. + /// - **Android**: Supported via JNI bridge for geolocation, microphone, camera, + /// protected media, and MIDI requests. Android runtime permissions may still + /// trigger native OS prompts before access is granted. + /// + /// # Examples + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust,no_run +use tauri::webview::{WebviewBuilder, PermissionKind, PermissionResponse}; +tauri::Builder::default() + .setup(|app| { + let window = tauri::window::WindowBuilder::new(app, "label").build()?; + let webview_builder = WebviewBuilder::new("core", tauri::WebviewUrl::App("index.html".into())) + .on_permission_request(|webview, kind| { + match kind { + PermissionKind::Geolocation => PermissionResponse::Allow, + PermissionKind::Notifications => PermissionResponse::Allow, + _ => PermissionResponse::Default, + } + }); + let webview = window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap())?; + Ok(()) + }); +``` + "#### + )] + pub fn on_permission_request< + F: Fn(Webview, PermissionKind) -> PermissionResponse + Send + Sync + 'static, + >( + mut self, + f: F, + ) -> Self { + self.permission_request_handler.replace(Box::new(f)); + self + } + + pub(crate) fn into_pending_webview>( + mut self, + manager: &M, + window_label: &str, + ) -> crate::Result> { + let mut pending = PendingWebview::new(self.webview_attributes, self.label.clone())?; + pending.navigation_handler = self.navigation_handler.take(); + pending.new_window_handler = self.new_window_handler.take().map(|handler| { + Box::new( + move |url, features: NewWindowFeatures| match handler(url, features) { + NewWindowResponse::Allow => tauri_runtime::webview::NewWindowResponse::Allow, + #[cfg(mobile)] + NewWindowResponse::Create { window: _ } => { + tauri_runtime::webview::NewWindowResponse::Allow + } + #[cfg(desktop)] + NewWindowResponse::Create { window } => { + tauri_runtime::webview::NewWindowResponse::Create { + window_id: window.window.window.id, + } + } + NewWindowResponse::Deny => tauri_runtime::webview::NewWindowResponse::Deny, + }, + ) + as Box< + dyn Fn(Url, NewWindowFeatures) -> tauri_runtime::webview::NewWindowResponse + + Send + + 'static, + > + }); + + if let Some(document_title_changed_handler) = self.document_title_changed_handler.take() { + let label = pending.label.clone(); + let manager = manager.manager_owned(); + pending + .document_title_changed_handler + .replace(Box::new(move |title| { + if let Some(w) = manager.get_webview(&label) { + document_title_changed_handler(w, title); + } + })); + } + pending.web_resource_request_handler = self.web_resource_request_handler.take(); + + if let Some(download_handler) = self.download_handler.take() { + let label = pending.label.clone(); + let manager = manager.manager_owned(); + pending.download_handler.replace(Arc::new(move |event| { + if let Some(w) = manager.get_webview(&label) { + download_handler( + w, + match event { + tauri_runtime::webview::DownloadEvent::Requested { url, destination } => { + DownloadEvent::Requested { url, destination } + } + tauri_runtime::webview::DownloadEvent::Finished { url, path, success } => { + DownloadEvent::Finished { url, path, success } + } + }, + ) + } else { + false + } + })); + } + + let label_ = pending.label.clone(); + let manager_ = manager.manager_owned(); + pending + .on_page_load_handler + .replace(Box::new(move |url, event| { + if let Some(w) = manager_.get_webview(&label_) { + if let Some(handler) = self.on_page_load_handler.as_ref() { + handler(w, PageLoadPayload { url: &url, event }); + } + } + })); + + let label_ = pending.label.clone(); + let manager_ = manager.manager_owned(); + if let Some(handler) = self.permission_request_handler { + pending.permission_request_handler = Some(Box::new(move |kind| { + if let Some(w) = manager_.get_webview(&label_) { + handler(w, kind) + } else { + PermissionResponse::Default + } + })); + } + + manager + .manager() + .webview + .prepare_webview(manager, pending, window_label) + } + + /// Creates a new webview on the given window. + #[cfg(desktop)] + pub(crate) fn build( + self, + window: Window, + position: Position, + size: Size, + ) -> crate::Result> { + let app_manager = window.manager(); + + let mut pending = self.into_pending_webview(&window, window.label())?; + + pending.webview_attributes.bounds = Some(tauri_runtime::dpi::Rect { size, position }); + + let use_https_scheme = pending.webview_attributes.use_https_scheme; + + let webview = match &mut window.runtime() { + RuntimeOrDispatch::Dispatch(dispatcher) => dispatcher.create_webview(pending), + _ => unimplemented!(), + } + .map(|webview| { + app_manager + .webview + .attach_webview(window.clone(), webview, use_https_scheme) + })?; + + Ok(webview) + } +} + +/// Webview attributes. +impl WebviewBuilder { + /// Sets whether clicking an inactive window also clicks through to the webview. + #[must_use] + pub fn accept_first_mouse(mut self, accept: bool) -> Self { + self.webview_attributes.accept_first_mouse = accept; + self + } + + /// Adds the provided JavaScript to a list of scripts that should be run after the global object has been created, + /// but before the HTML document has been parsed and before any other script included by the HTML document is run. + /// + /// Since it runs on all top-level document navigations, + /// it's recommended to check the `window.location` to guard your script from running on unexpected origins. + /// + /// This is executed only on the main frame. + /// If you only want to run it in all frames, use [`Self::initialization_script_for_all_frames`] instead. + /// + /// ## Platform-specific + /// + /// - **Windows:** scripts are always added to subframes. + /// - **Android:** When [addDocumentStartJavaScript] is not supported, + /// we prepend initialization scripts to each HTML head (implementation only supported on custom protocol URLs). + /// For remote URLs, we use [onPageStarted] which is not guaranteed to run before other scripts. + /// + /// # Examples + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust +use tauri::{WindowBuilder, Runtime}; + +const INIT_SCRIPT: &str = r#" + if (window.location.origin === 'https://tauri.app') { + console.log("hello world from js init script"); + + window.__MY_CUSTOM_PROPERTY__ = { foo: 'bar' }; + } +"#; + +fn main() { + tauri::Builder::default() + .setup(|app| { + let window = tauri::window::WindowBuilder::new(app, "label").build()?; + let webview_builder = tauri::webview::WebviewBuilder::new("label", tauri::WebviewUrl::App("index.html".into())) + .initialization_script(INIT_SCRIPT); + let webview = window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap())?; + Ok(()) + }); +} +``` + "#### + )] + /// + /// [addDocumentStartJavaScript]: https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E) + /// [onPageStarted]: https://developer.android.com/reference/android/webkit/WebViewClient#onPageStarted(android.webkit.WebView,%20java.lang.String,%20android.graphics.Bitmap) + #[must_use] + pub fn initialization_script(mut self, script: impl Into) -> Self { + self + .webview_attributes + .initialization_scripts + .push(InitializationScript { + script: script.into(), + for_main_frame_only: true, + }); + self + } + + /// Adds the provided JavaScript to a list of scripts that should be run after the global object has been created, + /// but before the HTML document has been parsed and before any other script included by the HTML document is run. + /// + /// Since it runs on all top-level document navigations and also child frame page navigations, + /// it's recommended to check the `window.location` to guard your script from running on unexpected origins. + /// + /// This is executed on all frames (main frame and also sub frames). + /// If you only want to run the script in the main frame, use [`Self::initialization_script`] instead. + /// + /// ## Platform-specific + /// + /// - **Android:** When [addDocumentStartJavaScript] is not supported, + /// we prepend initialization scripts to each HTML head (implementation only supported on custom protocol URLs). + /// For remote URLs, we use [onPageStarted] which is not guaranteed to run before other scripts. + /// + /// # Examples + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust +use tauri::{WindowBuilder, Runtime}; + +const INIT_SCRIPT: &str = r#" + if (window.location.origin === 'https://tauri.app') { + console.log("hello world from js init script"); + + window.__MY_CUSTOM_PROPERTY__ = { foo: 'bar' }; + } +"#; + +fn main() { + tauri::Builder::default() + .setup(|app| { + let window = tauri::window::WindowBuilder::new(app, "label").build()?; + let webview_builder = tauri::webview::WebviewBuilder::new("label", tauri::WebviewUrl::App("index.html".into())) + .initialization_script_for_all_frames(INIT_SCRIPT); + let webview = window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap())?; + Ok(()) + }); +} +``` + "#### + )] + /// + /// [addDocumentStartJavaScript]: https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E) + /// [onPageStarted]: https://developer.android.com/reference/android/webkit/WebViewClient#onPageStarted(android.webkit.WebView,%20java.lang.String,%20android.graphics.Bitmap) + #[must_use] + pub fn initialization_script_for_all_frames(mut self, script: impl Into) -> Self { + self + .webview_attributes + .initialization_scripts + .push(InitializationScript { + script: script.into(), + for_main_frame_only: false, + }); + self + } + + /// Set the user agent for the webview + #[must_use] + pub fn user_agent(mut self, user_agent: &str) -> Self { + self.webview_attributes.user_agent = Some(user_agent.to_string()); + self + } + + /// Set additional arguments for the webview. + /// + /// ## Platform-specific + /// + /// - **macOS / Linux / Android / iOS**: Unsupported. + /// + /// ## Warning + /// + /// Webview instances with different browser arguments must also have different [data directories](Self::data_directory). + /// + /// By default wry passes `--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection` + /// so if you use this method, you also need to disable these components by yourself if you want. + #[must_use] + pub fn additional_browser_args(mut self, additional_args: &str) -> Self { + self.webview_attributes.additional_browser_args = Some(additional_args.to_string()); + self + } + + /// Data directory for the webview. + #[must_use] + pub fn data_directory(mut self, data_directory: PathBuf) -> Self { + self + .webview_attributes + .data_directory + .replace(data_directory); + self + } + + /// Disables the drag and drop handler used internally to generate [`DragDropEvent`](crate::DragDropEvent)s. + /// + /// This is required to use HTML5 drag and drop APIs on the frontend on Windows since we replace the drag drop handler of WebView2. + #[must_use] + pub fn disable_drag_drop_handler(mut self) -> Self { + self.webview_attributes.drag_drop_handler_enabled = false; + self + } + + /// Enables clipboard access for the page rendered on **Linux** and **Windows**. + /// + /// **macOS** doesn't provide such method and is always enabled by default, + /// but you still need to add menu item accelerators to use shortcuts. + #[must_use] + pub fn enable_clipboard_access(mut self) -> Self { + self.webview_attributes.clipboard = true; + self + } + + /// Enable or disable incognito mode for the WebView. + /// + /// ## Platform-specific: + /// + /// - **Windows**: Requires WebView2 Runtime version 101.0.1210.39 or higher, does nothing on older versions, + /// see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/archive?tabs=dotnetcsharp#10121039 + /// - **Android**: Unsupported. + /// - **macOS / iOS**: Uses the nonPersistent DataStore + #[must_use] + pub fn incognito(mut self, incognito: bool) -> Self { + self.webview_attributes.incognito = incognito; + self + } + + /// Set a proxy URL for the WebView for all network requests. + /// + /// Must be either a `http://` or a `socks5://` URL. + /// + /// ## Platform-specific + /// + /// - **macOS**: Requires the `macos-proxy` feature flag and only compiles for macOS 14+. + #[must_use] + pub fn proxy_url(mut self, url: Url) -> Self { + self.webview_attributes.proxy_url = Some(url); + self + } + + /// Enable or disable transparency for the WebView. + #[must_use] + pub fn transparent(mut self, transparent: bool) -> Self { + self.webview_attributes.transparent = transparent; + self + } + + /// Whether the webview should be focused or not. + #[must_use] + pub fn focused(mut self, focus: bool) -> Self { + self.webview_attributes.focus = focus; + self + } + + /// Sets the webview to automatically grow and shrink its size and position when the parent window resizes. + #[must_use] + pub fn auto_resize(mut self) -> Self { + self.webview_attributes.auto_resize = true; + self + } + + /// Whether page zooming by hotkeys and mousewheel should be enabled or not. + /// + /// ## Platform-specific: + /// + /// - **Windows**: Controls WebView2's [`IsZoomControlEnabled`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2settings?view=webview2-winrt-1.0.2420.47#iszoomcontrolenabled) setting. + /// - **MacOS / Linux**: Injects a polyfill that zooms in and out with `Ctrl/Cmd + [- = +]` hotkeys or mousewheel events, + /// 20% in each step, ranging from 20% to 1000%. Requires `core:webview:allow-set-webview-zoom` permission + /// + /// - **Android / iOS**: Unsupported. + #[must_use] + pub fn zoom_hotkeys_enabled(mut self, enabled: bool) -> Self { + self.webview_attributes.zoom_hotkeys_enabled = enabled; + self + } + + /// Whether browser extensions can be installed for the webview process + /// + /// ## Platform-specific: + /// + /// - **Windows**: Enables the WebView2 environment's [`AreBrowserExtensionsEnabled`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2environmentoptions?view=webview2-winrt-1.0.2739.15#arebrowserextensionsenabled) + /// - **MacOS / Linux / iOS / Android** - Unsupported. + #[must_use] + pub fn browser_extensions_enabled(mut self, enabled: bool) -> Self { + self.webview_attributes.browser_extensions_enabled = enabled; + self + } + + /// Set the path from which to load extensions from. Extensions stored in this path should be unpacked Chrome extensions on Windows, and compiled `.so` extensions on Linux. + /// + /// ## Platform-specific: + /// + /// - **Windows**: Browser extensions must first be enabled. See [`browser_extensions_enabled`](Self::browser_extensions_enabled) + /// - **MacOS / iOS / Android** - Unsupported. + #[must_use] + pub fn extensions_path(mut self, path: impl AsRef) -> Self { + self.webview_attributes.extensions_path = Some(path.as_ref().to_path_buf()); + self + } + + /// Initialize the WebView with a custom data store identifier. + /// Can be used as a replacement for data_directory not being available in WKWebView. + /// + /// - **macOS / iOS**: Available on macOS >= 14 and iOS >= 17 + /// - **Windows / Linux / Android**: Unsupported. + /// + /// Note: Enable incognito mode to use the `nonPersistent` DataStore. + #[must_use] + pub fn data_store_identifier(mut self, data_store_identifier: [u8; 16]) -> Self { + self.webview_attributes.data_store_identifier = Some(data_store_identifier); + self + } + + /// Sets whether the custom protocols should use `https://.localhost` instead of the default `http://.localhost` on Windows and Android. Defaults to `false`. + /// + /// ## Note + /// + /// Using a `https` scheme will NOT allow mixed content when trying to fetch `http` endpoints and therefore will not match the behavior of the `://localhost` protocols used on macOS and Linux. + /// + /// ## Warning + /// + /// Changing this value between releases will change the IndexedDB, cookies and localstorage location and your app will not be able to access the old data. + #[must_use] + pub fn use_https_scheme(mut self, enabled: bool) -> Self { + self.webview_attributes.use_https_scheme = enabled; + self + } + + /// Whether web inspector, which is usually called browser devtools, is enabled or not. Enabled by default. + /// + /// This API works in **debug** builds, but requires `devtools` feature flag to enable it in **release** builds. + /// + /// ## Platform-specific + /// + /// - macOS: This will call private functions on **macOS** + /// - Android: Open `chrome://inspect/#devices` in Chrome to get the devtools window. Wry's `WebView` devtools API isn't supported on Android. + /// - iOS: Open Safari > Develop > [Your Device Name] > [Your WebView] to get the devtools window. + #[must_use] + pub fn devtools(mut self, enabled: bool) -> Self { + self.webview_attributes.devtools.replace(enabled); + self + } + + /// Set the webview background color. + /// + /// ## Platform-specific: + /// + /// - **macOS / iOS**: Not implemented. + /// - **Windows**: On Windows 7, alpha channel is ignored. + /// - **Windows**: On Windows 8 and newer, if alpha channel is not `0`, it will be ignored. + #[must_use] + pub fn background_color(mut self, color: Color) -> Self { + self.webview_attributes.background_color = Some(color); + self + } + + /// Change the default background throttling behaviour. + /// + /// By default, browsers use a suspend policy that will throttle timers and even unload + /// the whole tab (view) to free resources after roughly 5 minutes when a view became + /// minimized or hidden. This will pause all tasks until the documents visibility state + /// changes back from hidden to visible by bringing the view back to the foreground. + /// + /// ## Platform-specific + /// + /// - **Linux / Windows / Android**: Unsupported. Workarounds like a pending WebLock transaction might suffice. + /// - **iOS**: Supported since version 17.0+. + /// - **macOS**: Supported since version 14.0+. + /// + /// see + #[must_use] + pub fn background_throttling(mut self, policy: BackgroundThrottlingPolicy) -> Self { + self.webview_attributes.background_throttling = Some(policy); + self + } + + /// Whether JavaScript should be disabled. + #[must_use] + pub fn disable_javascript(mut self) -> Self { + self.webview_attributes.javascript_disabled = true; + self + } + + /// Specifies the native scrollbar style to use with the webview. + /// CSS styles that modify the scrollbar are applied on top of the native appearance configured here. + /// + /// Defaults to [`ScrollBarStyle::Default`], which is the browser default. + /// + /// ## Platform-specific + /// + /// - **Windows**: + /// - [`ScrollBarStyle::FluentOverlay`] requires WebView2 Runtime version 125.0.2535.41 or higher, + /// and does nothing on older versions. + /// - This option must be given the same value for all webviews that target the same data directory. Use + /// [`WebviewBuilder::data_directory`] to change data directories if needed. + /// - **Linux / Android / iOS / macOS**: Unsupported. Only supports `Default` and performs no operation. + #[must_use] + pub fn scroll_bar_style(mut self, style: ScrollBarStyle) -> Self { + self.webview_attributes = self.webview_attributes.scroll_bar_style(style); + self + } + + /// Controls the WebView's browser-level general autofill behavior. + /// + /// **This option does not disable password or credit card autofill.** + /// + /// When set to `false`, the WebView will not automatically populate + /// general form fields using previously stored data such as addresses + /// or contact information. + /// + /// By default, this is `true`. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported. WebView2's autofill feature (called + /// "Suggestions") may not honor `autocomplete="off"` on input + /// elements in some cases. + /// - **Linux / Android / iOS / macOS**: Unsupported and performs no + /// operation. + #[must_use] + pub fn general_autofill_enabled(mut self, enabled: bool) -> Self { + self.webview_attributes = self.webview_attributes.general_autofill_enabled(enabled); + self + } + + /// Whether to show a link preview when long pressing on links. Available on macOS and iOS only. + /// + /// Default is true. + /// + /// See https://docs.rs/objc2-web-kit/latest/objc2_web_kit/struct.WKWebView.html#method.allowsLinkPreview + /// + /// ## Platform-specific + /// + /// - **Linux / Windows / Android:** Unsupported. + #[cfg(target_os = "macos")] + #[must_use] + pub fn allow_link_preview(mut self, allow_link_preview: bool) -> Self { + self.webview_attributes = self + .webview_attributes + .allow_link_preview(allow_link_preview); + self + } + /// Whether to limit navigations to App-Bound Domains. This is necessary to + /// enable Service Workers on iOS according to + /// [StackOverflow](https://stackoverflow.com/questions/49673399/service-workers-unavailable-in-wkwebview-in-ios-11-3/64155509#64155509). + /// + /// Default is false. + /// + /// Note: If you pass in `true` make sure to add localhost and any [`registrable + /// domains`](https://developer.mozilla.org/en-US/docs/Glossary/Registrable_domain) + /// used in this webview to tauri-src/Info.ios.plist: + /// + /// ```xml + /// + /// + /// WKAppBoundDomains + /// + /// localhost + /// aregistrabledomain.example + /// + /// + /// + /// ``` + /// + /// You must add `localhost` if any webview with this set to true opens a + /// local webpage, makes any localhost calls, or uses the isolation pattern + /// because Tauri uses the `localhost` domain for hosting the application + /// webpage, the IPC protocol, and the isolation pattern's iframe. + /// + /// Requests served through custom uri schemes are allowed so long as they use + /// a registrable domain specified in the `WKAppBoundDomains` array for all the + /// requests from the app, including requests for the `localhost` domain. + /// + /// In theory, you can whitelist an entire uri scheme by including the + /// protocol name followed by a colon. For example, to allow all requests + /// using a custom "stream" uri scheme (see [this tauri + /// example](https://github.com/tauri-apps/tauri/blob/dev/examples/streaming/main.rs)), + /// you could add `stream:` to the AppBoundDomains array. That said, I'm not + /// sure whether Apple would let your app through app review if you do + /// whitelist an entire protocol because this feature is not mentioned in + /// [their blog post on App-Bound + /// Domains](https://webkit.org/blog/10882/app-bound-domains/). + /// + /// See https://webkit.org/blog/10882/app-bound-domains/ and + /// https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/limitsnavigationstoappbounddomains + /// for the official documentation on App-Bound Domains. + /// + /// ## Platform-specific + /// + /// - **iOS**: Supported since version 14.0+. + /// - **Linux / Windows / Android / MacOS:** Unsupported. + #[must_use] + pub fn limit_navigations_to_app_bound_domains(mut self, limit_navigations: bool) -> Self { + self.webview_attributes = self + .webview_attributes + .limit_navigations_to_app_bound_domains(limit_navigations); + self + } + + /// Allows overriding the keyboard accessory view on iOS. + /// Returning `None` effectively removes the view. + /// + /// The closure parameter is the webview instance. + /// + /// The accessory view is the view that appears above the keyboard when a text input element is focused. + /// It usually displays a view with "Done", "Next" buttons. + /// + /// # Stability + /// + /// This relies on [`objc2_ui_kit`] which does not provide a stable API yet, so it can receive breaking changes in minor releases. + #[cfg(target_os = "ios")] + pub fn with_input_accessory_view_builder< + F: Fn(&objc2_ui_kit::UIView) -> Option> + + Send + + Sync + + 'static, + >( + mut self, + builder: F, + ) -> Self { + self + .webview_attributes + .input_accessory_view_builder + .replace(tauri_runtime::webview::InputAccessoryViewBuilder::new( + Box::new(builder), + )); + self + } + + /// Set the environment for the webview. + /// Useful if you need to share the same environment, for instance when using the [`Self::on_new_window`]. + #[cfg(all(feature = "wry", windows))] + pub fn with_environment( + mut self, + environment: webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Environment, + ) -> Self { + self.webview_attributes.environment.replace(environment); + self + } + + /// Creates a new webview sharing the same web process with the provided webview. + /// Useful if you need to link a webview to another, for instance when using the [`Self::on_new_window`]. + #[cfg(all( + feature = "wry", + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + ) + ))] + pub fn with_related_view(mut self, related_view: webkit2gtk::WebView) -> Self { + self.webview_attributes.related_view.replace(related_view); + self + } + + /// Set the webview configuration. + /// Useful if you need to share the use a predefined webview configuration, for instance when using the [`Self::on_new_window`]. + #[cfg(target_os = "macos")] + pub fn with_webview_configuration( + mut self, + webview_configuration: objc2::rc::Retained, + ) -> Self { + self + .webview_attributes + .webview_configuration + .replace(webview_configuration); + self + } +} + +/// Webview. +#[default_runtime(crate::Wry, wry)] +pub struct Webview { + pub(crate) window: Arc>>, + /// The webview created by the runtime. + pub(crate) webview: DetachedWebview, + /// The manager to associate this webview with. + pub(crate) manager: Arc>, + pub(crate) app_handle: AppHandle, + pub(crate) resources_table: Arc>, + use_https_scheme: bool, +} + +impl std::fmt::Debug for Webview { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Window") + .field("window", &self.window.lock().unwrap()) + .field("webview", &self.webview) + .field("use_https_scheme", &self.use_https_scheme) + .finish() + } +} + +impl Clone for Webview { + fn clone(&self) -> Self { + Self { + window: self.window.clone(), + webview: self.webview.clone(), + manager: self.manager.clone(), + app_handle: self.app_handle.clone(), + resources_table: self.resources_table.clone(), + use_https_scheme: self.use_https_scheme, + } + } +} + +impl Hash for Webview { + /// Only use the [`Webview`]'s label to represent its hash. + fn hash(&self, state: &mut H) { + self.webview.label.hash(state) + } +} + +impl Eq for Webview {} +impl PartialEq for Webview { + /// Only use the [`Webview`]'s label to compare equality. + fn eq(&self, other: &Self) -> bool { + self.webview.label.eq(&other.webview.label) + } +} + +/// Base webview functions. +impl Webview { + /// Whether this webview instance is still registered on the manager. + /// + /// Returns false once the webview is closed, even if a new webview with the same label exists. + pub(crate) fn is_registered(&self) -> bool { + self + .manager + .get_webview(self.label()) + // clones share the resources table, so it identifies the webview instance + .is_some_and(|w| Arc::ptr_eq(&w.resources_table, &self.resources_table)) + } + + /// Create a new webview that is attached to the window. + pub(crate) fn new( + window: Window, + webview: DetachedWebview, + use_https_scheme: bool, + ) -> Self { + Self { + manager: window.manager.clone(), + app_handle: window.app_handle.clone(), + window: Arc::new(Mutex::new(window)), + webview, + resources_table: Default::default(), + use_https_scheme, + } + } + + /// Initializes a webview builder with the given window label and URL to load on the webview. + /// + /// Data URLs are only supported with the `webview-data-url` feature flag. + #[cfg(feature = "unstable")] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] + pub fn builder>(label: L, url: WebviewUrl) -> WebviewBuilder { + WebviewBuilder::new(label.into(), url) + } + + /// Runs the given closure on the main thread. + pub fn run_on_main_thread(&self, f: F) -> crate::Result<()> { + self + .webview + .dispatcher + .run_on_main_thread(f) + .map_err(Into::into) + } + + /// The webview label. + pub fn label(&self) -> &str { + &self.webview.label + } + + /// Whether the webview was configured to use the HTTPS scheme or not. + pub(crate) fn use_https_scheme(&self) -> bool { + self.use_https_scheme + } + + /// Registers a webview event listener. + pub fn on_webview_event(&self, f: F) { + self + .webview + .dispatcher + .on_webview_event(move |event| f(&event.clone().into())); + } + + /// Resolves the given command scope for this webview on the currently loaded URL. + /// + /// If the command is not allowed, returns None. + /// + /// If the scope cannot be deserialized to the given type, an error is returned. + /// + /// In a command context this can be directly resolved from the command arguments via [CommandScope]: + /// + /// ``` + /// use tauri::ipc::CommandScope; + /// + /// #[derive(Debug, serde::Deserialize)] + /// struct ScopeType { + /// some_value: String, + /// } + /// #[tauri::command] + /// fn my_command(scope: CommandScope) { + /// // check scope + /// } + /// ``` + /// + /// # Examples + /// + /// ``` + /// use tauri::Manager; + /// + /// #[derive(Debug, serde::Deserialize)] + /// struct ScopeType { + /// some_value: String, + /// } + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let webview = app.get_webview_window("main").unwrap(); + /// let scope = webview.resolve_command_scope::("my-plugin", "read"); + /// Ok(()) + /// }); + /// ``` + pub fn resolve_command_scope( + &self, + plugin: &str, + command: &str, + ) -> crate::Result>> { + let current_url = self.url()?; + let is_local = self.is_local_url(¤t_url); + let origin = if is_local { + Origin::Local + } else { + Origin::Remote { url: current_url } + }; + + let cmd_name = format!("plugin:{plugin}|{command}"); + let resolved_access = self + .manager() + .runtime_authority + .lock() + .unwrap() + .resolve_access(&cmd_name, self.window().label(), self.label(), &origin); + + if let Some(access) = resolved_access { + let scope_ids = access + .iter() + .filter_map(|cmd| cmd.scope_id) + .collect::>(); + + let command_scope = CommandScope::resolve(self, scope_ids)?; + let global_scope = GlobalScope::resolve(self, plugin)?; + + Ok(Some(ResolvedScope { + global_scope, + command_scope, + })) + } else { + Ok(None) + } + } +} + +/// Desktop webview setters and actions. +#[cfg(desktop)] +impl Webview { + /// Opens the dialog to prints the contents of the webview. + /// Currently only supported on macOS on `wry`. + /// `window.print()` works on all platforms. + pub fn print(&self) -> crate::Result<()> { + self.webview.dispatcher.print().map_err(Into::into) + } + + /// Get the cursor position relative to the top-left hand corner of the desktop. + /// + /// Note that the top-left hand corner of the desktop is not necessarily the same as the screen. + /// If the user uses a desktop with multiple monitors, + /// the top-left hand corner of the desktop is the top-left hand corner of the main monitor on Windows and macOS + /// or the top-left of the leftmost monitor on X11. + /// + /// The coordinates can be negative if the top-left hand corner of the window is outside of the visible screen region. + pub fn cursor_position(&self) -> crate::Result> { + self.app_handle.cursor_position() + } + + /// Closes this webview. + pub fn close(&self) -> crate::Result<()> { + self.webview.dispatcher.close()?; + self.manager().on_webview_close(self.label()); + Ok(()) + } + + /// Resizes this webview. + pub fn set_bounds(&self, bounds: tauri_runtime::dpi::Rect) -> crate::Result<()> { + self + .webview + .dispatcher + .set_bounds(bounds) + .map_err(Into::into) + } + + /// Resizes this webview. + pub fn set_size>(&self, size: S) -> crate::Result<()> { + self + .webview + .dispatcher + .set_size(size.into()) + .map_err(Into::into) + } + + /// Sets this webviews's position. + pub fn set_position>(&self, position: Pos) -> crate::Result<()> { + self + .webview + .dispatcher + .set_position(position.into()) + .map_err(Into::into) + } + + /// Focus the webview. + pub fn set_focus(&self) -> crate::Result<()> { + self.webview.dispatcher.set_focus().map_err(Into::into) + } + + /// Hide the webview. + pub fn hide(&self) -> crate::Result<()> { + self.webview.dispatcher.hide().map_err(Into::into) + } + + /// Show the webview. + pub fn show(&self) -> crate::Result<()> { + self.webview.dispatcher.show().map_err(Into::into) + } + + /// Move the webview to the given window. + /// + /// The webview keeps its size and its position relative to the window client area, + /// so you usually want to call [`Self::set_position`] and [`Self::set_size`] afterwards + /// (or [`Self::set_auto_resize`] to let it follow the new parent window size). + /// + /// # Errors + /// + /// Returns [`Error::CannotReparentWebviewWindow`](crate::Error::CannotReparentWebviewWindow) + /// when the `unstable` Cargo feature is disabled and either this webview or the target window + /// belongs to a [`WebviewWindow`] - without that feature a window and its webview are a single + /// object that cannot be split apart. With the `unstable` feature enabled any webview can be + /// moved to any window. + /// + /// # Examples + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +use tauri::{LogicalPosition, LogicalSize, WebviewUrl}; + +tauri::Builder::default() + .setup(|app| { + let first = tauri::Window::builder(app, "first").build()?; + let second = tauri::Window::builder(app, "second").build()?; + + let webview = first.add_child( + tauri::webview::WebviewBuilder::new("child", WebviewUrl::App(Default::default())), + LogicalPosition::new(0., 0.), + LogicalSize::new(800., 600.), + )?; + + // move the webview to the second window + webview.reparent(&second)?; + + Ok(()) + }); +``` + "#### + )] + pub fn reparent(&self, window: &Window) -> crate::Result<()> { + #[cfg(not(feature = "unstable"))] + { + if self.window_ref().is_webview_window() || window.is_webview_window() { + return Err(crate::Error::CannotReparentWebviewWindow); + } + } + + *self.window.lock().unwrap() = window.clone(); + self.webview.dispatcher.reparent(window.window.id)?; + Ok(()) + } + + /// Sets whether the webview should automatically grow and shrink its size and position when the parent window resizes. + /// + /// Child webviews created with `Window::add_child` (`unstable` feature) have a fixed position and size + /// by default: resizing the window leaves them where they are. When auto resize is enabled, the webview bounds + /// are scaled with the window client area, keeping the same proportions it had when auto resize was enabled. + /// + /// The same can be set when creating the webview with `WebviewBuilder::auto_resize`. + /// + /// This is a no-op for [`WebviewWindow`] webviews, which always fill their window. + pub fn set_auto_resize(&self, auto_resize: bool) -> crate::Result<()> { + self + .webview + .dispatcher + .set_auto_resize(auto_resize) + .map_err(Into::into) + } + + /// Returns the bounds of the webviews's client area. + pub fn bounds(&self) -> crate::Result { + self.webview.dispatcher.bounds().map_err(Into::into) + } + + /// Returns the webview position. + /// + /// - For child webviews, returns the position of the top-left hand corner of the webviews's client area relative to the top-left hand corner of the parent window. + /// - For webview window, returns the inner position of the window. + pub fn position(&self) -> crate::Result> { + self.webview.dispatcher.position().map_err(Into::into) + } + + /// Returns the physical size of the webviews's client area. + pub fn size(&self) -> crate::Result> { + self.webview.dispatcher.size().map_err(Into::into) + } +} + +/// Webview APIs. +impl Webview { + /// The window that is hosting this webview. + pub fn window(&self) -> Window { + self.window.lock().unwrap().clone() + } + + /// A reference to the window that is hosting this webview. + pub fn window_ref(&self) -> MutexGuard<'_, Window> { + self.window.lock().unwrap() + } + + pub(crate) fn window_label(&self) -> String { + self.window_ref().label().to_string() + } + + /// Executes a closure, providing it with the webview handle that is specific to the current platform. + /// + /// The closure is executed on the main thread. + /// + /// Note that `webview2-com`, `webkit2gtk`, `objc2_web_kit` and similar crates may be updated in minor releases of Tauri. + /// Therefore it's recommended to pin Tauri to at least a minor version when you're using `with_webview`. + /// + /// # Examples + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust,no_run +use tauri::Manager; + +tauri::Builder::default() + .setup(|app| { + let main_webview = app.get_webview("main").unwrap(); + main_webview.with_webview(|webview| { + #[cfg(target_os = "linux")] + { + // see + // and + use webkit2gtk::WebViewExt; + webview.inner().set_zoom_level(4.); + } + + #[cfg(windows)] + unsafe { + // see https://docs.rs/webview2-com/0.19.1/webview2_com/Microsoft/Web/WebView2/Win32/struct.ICoreWebView2Controller.html + webview.controller().SetZoomFactor(4.).unwrap(); + } + + #[cfg(target_os = "macos")] + unsafe { + let view: &objc2_web_kit::WKWebView = &*webview.inner().cast(); + let controller: &objc2_web_kit::WKUserContentController = &*webview.controller().cast(); + let window: &objc2_app_kit::NSWindow = &*webview.ns_window().cast(); + + view.setPageZoom(4.); + controller.removeAllUserScripts(); + let bg_color = objc2_app_kit::NSColor::colorWithDeviceRed_green_blue_alpha(0.5, 0.2, 0.4, 1.); + window.setBackgroundColor(Some(&bg_color)); + } + + #[cfg(target_os = "android")] + { + use jni::objects::JValue; + webview.jni_handle().exec(|env, _, webview| { + env.call_method(webview, "zoomBy", "(F)V", &[JValue::Float(4.)]).unwrap(); + }) + } + }); + Ok(()) +}); +``` + "#### + )] + #[cfg(feature = "wry")] + #[cfg_attr(docsrs, doc(cfg(feature = "wry")))] + pub fn with_webview( + &self, + f: F, + ) -> crate::Result<()> { + self + .webview + .dispatcher + .with_webview(|w| f(PlatformWebview(*w.downcast().unwrap()))) + .map_err(Into::into) + } + + /// Returns the current url of the webview. + pub fn url(&self) -> crate::Result { + self + .webview + .dispatcher + .url() + .map(|url| url.parse().map_err(crate::Error::InvalidUrl))? + } + + /// Navigates the webview to the defined url. + /// + /// This is the Rust equivalent of setting `window.location` on the frontend and behaves like any other + /// top-level navigation: + /// + /// - the navigation handler defined with `WebviewBuilder::on_navigation` and the + /// [`Plugin::on_navigation`](crate::plugin::Plugin::on_navigation) hooks are called and can cancel it; + /// - the initialization scripts registered when the webview was created + /// (including Tauri's own IPC bridge) run again on the new document, so `window.__TAURI__` and the scripts + /// added with `WebviewBuilder::initialization_script` keep working after the navigation; + /// - [`Builder::on_page_load`](crate::Builder::on_page_load) is triggered for the new document; + /// - the ACL origin of the IPC changes with the URL: after navigating to a remote URL the webview can only + /// execute commands allowed by a capability whose `remote` context matches it. + /// + /// The navigation is asynchronous: this function returns as soon as the request is sent to the webview, + /// the page is not loaded yet when it returns. + pub fn navigate(&self, url: Url) -> crate::Result<()> { + self.webview.dispatcher.navigate(url).map_err(Into::into) + } + + /// Reloads the current page. + /// + /// Reloading re-runs the initialization scripts and triggers the navigation and page load hooks + /// for the same URL, exactly like [`Self::navigate`] to [`Self::url`] would. + /// In-memory state of the page (JavaScript variables, unsaved form data, channels created by the frontend) + /// is lost, but cookies and other browsing data are kept - use [`Self::clear_all_browsing_data`] to clear those. + pub fn reload(&self) -> crate::Result<()> { + self.webview.dispatcher.reload().map_err(Into::into) + } + + /// Converts a file path to a URL that can be loaded by this webview. + /// + /// This is the Rust equivalent of the JavaScript `convertFileSrc` function. + /// + /// The `protocol-asset` Cargo feature must be enabled and the file must be included in the + /// [`app.security.assetProtocol`](https://v2.tauri.app/reference/config/#assetprotocolconfig) + /// scope. The protocol origin must also be allowed by the relevant + /// [`app.security.csp`](https://v2.tauri.app/reference/config/#csp-1) directive, + /// e.g. `img-src 'self' asset: http://asset.localhost`. + /// + /// On Windows and Android the URL is `http://{protocol}.localhost/{path}` + /// (or `https://` if the webview was built with [`WebviewBuilder::use_https_scheme`]); + /// on macOS, Linux and iOS it is `{protocol}://localhost/{path}`. + /// + /// # Arguments + /// + /// * `path` - The file path to convert. + /// * `protocol` - The custom protocol to use. Defaults to `asset`; you only need to set this + /// when using a protocol registered with [`Builder::register_uri_scheme_protocol`](crate::Builder::register_uri_scheme_protocol). + /// + /// # Errors + /// + /// Returns [`Error::NonUtf8Path`](crate::Error::NonUtf8Path) if the path is not valid UTF-8, + /// since the asset protocol could not resolve such a URL back to the file. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri::Manager; + /// tauri::Builder::default() + /// .setup(|app| { + /// let webview = app.get_webview_window("main").unwrap(); + /// let video_path = app.path().app_data_dir()?.join("video.mp4"); + /// let url = webview.convert_file_src(&video_path, None)?; + /// webview.eval(format!("document.querySelector('video').src = '{url}'"))?; + /// Ok(()) + /// }); + /// ``` + pub fn convert_file_src>( + &self, + path: P, + protocol: Option<&str>, + ) -> crate::Result { + let path = dunce::simplified(path.as_ref()); + let path = path + .to_str() + .ok_or_else(|| crate::Error::NonUtf8Path(path.to_path_buf()))?; + Ok(format!( + "{}/{}", + crate::protocol::origin(protocol.unwrap_or("asset"), self.use_https_scheme), + percent_encoding::utf8_percent_encode(path, crate::protocol::ENCODE_URI_COMPONENT) + )) + } + + fn is_local_url(&self, current_url: &Url) -> bool { + let uses_https = current_url.scheme() == "https"; + + // if from `tauri://` custom protocol + ({ + let protocol_url = self.manager().tauri_protocol_url(uses_https); + current_url.scheme() == protocol_url.scheme() + && current_url.domain() == protocol_url.domain() + }) || + + // or if relative to `devUrl` or `frontendDist` + self + .manager() + .get_app_url(uses_https) + .make_relative(current_url) + .is_some() + + // or from a custom protocol registered by the user + || ({ + let scheme = current_url.scheme(); + let protocols = self.manager().webview.uri_scheme_protocols.lock().unwrap(); + + #[cfg(all(not(windows), not(target_os = "android")))] + let local = protocols.contains_key(scheme); + + // on window and android, custom protocols are `http://.path/to/route` + // so we check using the first part of the domain + #[cfg(any(windows, target_os = "android"))] + let local = { + let scheme = scheme == self.manager().tauri_protocol_url(uses_https).scheme(); + let protocol = current_url + .domain() + .and_then(|d| d.strip_suffix(".localhost")) + .map(|protocol| protocols.contains_key(protocol)) + .unwrap_or_default(); + + scheme && protocol + }; + + local + }) + } + + /// Handles this window receiving an [`InvokeRequest`]. + pub fn on_message(self, request: InvokeRequest, responder: Box>) { + let manager = self.manager_owned(); + let is_local = self.is_local_url(&request.url); + + // ensure the passed key matches what our manager should have injected + let expected = manager.invoke_key(); + if request.invoke_key != expected { + #[cfg(feature = "tracing")] + tracing::error!( + "__TAURI_INVOKE_KEY__ expected {expected} but received {}", + request.invoke_key + ); + + #[cfg(not(feature = "tracing"))] + eprintln!( + "__TAURI_INVOKE_KEY__ expected {expected} but received {}", + request.invoke_key + ); + + return; + } + + let resolver = InvokeResolver::new( + self.clone(), + Arc::new(Mutex::new(Some(Box::new( + move |webview: Webview, cmd, response, callback, error| { + responder(webview, cmd, response, callback, error); + }, + )))), + request.cmd.clone(), + request.callback, + request.error, + ); + + #[cfg(mobile)] + let app_handle = self.app_handle.clone(); + + let message = InvokeMessage::new(self, request.cmd.to_string(), request.body, request.headers); + + let acl_origin = if is_local { + Origin::Local + } else { + Origin::Remote { + url: request.url.clone(), + } + }; + let (resolved_acl, has_app_acl_manifest) = { + let runtime_authority = manager.runtime_authority.lock().unwrap(); + let acl = runtime_authority.resolve_access( + &request.cmd, + message.webview.window_ref().label(), + message.webview.label(), + &acl_origin, + ); + (acl, runtime_authority.has_app_manifest()) + }; + + let mut invoke = Invoke { + message, + resolver: resolver.clone(), + acl: resolved_acl, + }; + + let plugin_command = request.cmd.strip_prefix("plugin:").map(|raw_command| { + let mut tokens = raw_command.split('|'); + // safe to unwrap: split always has a least one item + let plugin = tokens.next().unwrap(); + let command = tokens.next().map(|c| c.to_string()).unwrap_or_default(); + (plugin, command) + }); + + // Check ACL on plugin commands, when the app defined its ACL manifest, + // or when the request comes from a non-local (remote) origin. This + // ensures remote content can never reach custom commands unless an + // explicit `remote` capability has been configured for them. + if (plugin_command.is_some() || has_app_acl_manifest || !is_local) + // TODO: Remove this special check in v3 + && request.cmd != crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND + && invoke.acl.is_none() + { + #[cfg(debug_assertions)] + { + let (key, command_name) = plugin_command + .clone() + .unwrap_or_else(|| (tauri_utils::acl::APP_ACL_KEY, request.cmd.clone())); + invoke.resolver.reject( + manager + .runtime_authority + .lock() + .unwrap() + .resolve_access_message( + key, + &command_name, + invoke.message.webview.window().label(), + invoke.message.webview.label(), + &acl_origin, + ), + ); + } + #[cfg(not(debug_assertions))] + invoke + .resolver + .reject(format!("Command {} not allowed by ACL", request.cmd)); + return; + } + + if let Some((plugin, command_name)) = plugin_command { + invoke.message.command = command_name; + + #[cfg(desktop)] + let command = invoke.message.command.clone(); + + #[cfg(mobile)] + let message = invoke.message.clone(); + + #[allow(unused_mut)] + let mut handled = manager.run_plugin_invoke_handler(plugin, invoke); + + if handled { + return; + } + + #[cfg(mobile)] + { + fn load_channels(payload: &serde_json::Value, webview: &Webview) { + use std::str::FromStr; + + if let serde_json::Value::Object(map) = payload { + for v in map.values() { + if let serde_json::Value::String(s) = v { + let _ = crate::ipc::JavaScriptChannelId::from_str(s) + .map(|id| id.channel_on::(webview.clone())); + } + } + } + } + + let payload = message.payload.into_json(); + // initialize channels + load_channels(&payload, &message.webview); + + let resolver_ = resolver.clone(); + if let Err(e) = crate::plugin::mobile::run_command( + plugin, + &app_handle, + heck::AsLowerCamelCase(message.command).to_string(), + payload, + move |response| match response { + Ok(r) => resolver_.resolve(r), + Err(e) => resolver_.reject(e), + }, + ) { + resolver.reject(e.to_string()); + } + } + + #[cfg(desktop)] + resolver.reject(format!("Command {command} not found")); + } else { + let command = invoke.message.command.clone(); + let handled = manager.run_invoke_handler(invoke); + if !handled { + resolver.reject(format!("Command {command} not found")); + } + } + } + + /// Evaluates JavaScript on this window. + pub fn eval(&self, js: impl Into) -> crate::Result<()> { + self + .webview + .dispatcher + .eval_script(js.into()) + .map_err(Into::into) + } + + /// Evaluate JavaScript with callback function on this webview. + /// The evaluation result will be serialized into a JSON string and passed to the callback function. + /// + /// Exception is ignored because of the limitation on Windows. You can catch it yourself and return as string as a workaround. + pub fn eval_with_callback( + &self, + js: impl Into, + callback: impl Fn(String) + Send + 'static, + ) -> crate::Result<()> { + self + .webview + .dispatcher + .eval_script_with_callback(js.into(), callback) + .map_err(Into::into) + } + + /// Register a JS event listener and return its identifier. + pub(crate) fn listen_js( + &self, + event: EventName<&str>, + target: EventTarget, + handler: CallbackFn, + ) -> crate::Result { + let listeners = self.manager().listeners(); + + let id = listeners.next_event_id(); + + self.eval(crate::event::listen_js_script( + listeners.listeners_object_name(), + &serde_json::to_string(&target)?, + event, + id, + handler, + ))?; + + listeners.listen_js(event, self.label(), target, id); + + Ok(id) + } + + /// Unregister a JS event listener. + pub(crate) fn unlisten_js(&self, event: EventName<&str>, id: EventId) -> crate::Result<()> { + let listeners = self.manager().listeners(); + + listeners.unlisten_js(event, id); + + Ok(()) + } + + pub(crate) fn emit_js(&self, emit_args: &EmitArgs, ids: &[u32]) -> crate::Result<()> { + self.eval(crate::event::emit_js_script( + self.manager().listeners().function_name(), + emit_args, + &serde_json::to_string(ids)?, + )?)?; + Ok(()) + } + + /// Opens the developer tools window (Web Inspector). + /// The devtools is only enabled on debug builds or with the `devtools` feature flag. + /// + /// ## Platform-specific + /// + /// - **macOS:** Only supported on macOS 10.15+. + /// This is a private API on macOS, so you cannot use this if your application will be published on the App Store. + /// + /// # Examples + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust,no_run +use tauri::Manager; +tauri::Builder::default() + .setup(|app| { + #[cfg(debug_assertions)] + app.get_webview("main").unwrap().open_devtools(); + Ok(()) + }); +``` + "#### + )] + #[cfg(any(debug_assertions, feature = "devtools"))] + #[cfg_attr(docsrs, doc(cfg(any(debug_assertions, feature = "devtools"))))] + pub fn open_devtools(&self) { + self.webview.dispatcher.open_devtools(); + } + + /// Closes the developer tools window (Web Inspector). + /// The devtools is only enabled on debug builds or with the `devtools` feature flag. + /// + /// ## Platform-specific + /// + /// - **macOS:** Only supported on macOS 10.15+. + /// This is a private API on macOS, so you cannot use this if your application will be published on the App Store. + /// - **Windows:** Unsupported. + /// + /// # Examples + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust,no_run +use tauri::Manager; +tauri::Builder::default() + .setup(|app| { + #[cfg(debug_assertions)] + { + let webview = app.get_webview("main").unwrap(); + webview.open_devtools(); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_secs(10)); + webview.close_devtools(); + }); + } + Ok(()) + }); +``` + "#### + )] + #[cfg(any(debug_assertions, feature = "devtools"))] + #[cfg_attr(docsrs, doc(cfg(any(debug_assertions, feature = "devtools"))))] + pub fn close_devtools(&self) { + self.webview.dispatcher.close_devtools(); + } + + /// Checks if the developer tools window (Web Inspector) is opened. + /// The devtools is only enabled on debug builds or with the `devtools` feature flag. + /// + /// ## Platform-specific + /// + /// - **macOS:** Only supported on macOS 10.15+. + /// This is a private API on macOS, so you cannot use this if your application will be published on the App Store. + /// - **Windows:** Unsupported. + /// + /// # Examples + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust,no_run +use tauri::Manager; +tauri::Builder::default() + .setup(|app| { + #[cfg(debug_assertions)] + { + let webview = app.get_webview("main").unwrap(); + if !webview.is_devtools_open() { + webview.open_devtools(); + } + } + Ok(()) + }); +``` + "#### + )] + #[cfg(any(debug_assertions, feature = "devtools"))] + #[cfg_attr(docsrs, doc(cfg(any(debug_assertions, feature = "devtools"))))] + pub fn is_devtools_open(&self) -> bool { + self + .webview + .dispatcher + .is_devtools_open() + .unwrap_or_default() + } + + /// Set the webview zoom level + /// + /// ## Platform-specific: + /// + /// - **Android**: Not supported. + /// - **macOS**: available on macOS 11+ only. + /// - **iOS**: available on iOS 14+ only. + pub fn set_zoom(&self, scale_factor: f64) -> crate::Result<()> { + self + .webview + .dispatcher + .set_zoom(scale_factor) + .map_err(Into::into) + } + + /// Specify the webview background color. + /// + /// ## Platform-specific: + /// + /// - **macOS / iOS**: Not implemented. + /// - **Windows**: + /// - On Windows 7, transparency is not supported and the alpha value will be ignored. + /// - On Windows higher than 7: translucent colors are not supported so any alpha value other than `0` will be replaced by `255` + pub fn set_background_color(&self, color: Option) -> crate::Result<()> { + self + .webview + .dispatcher + .set_background_color(color) + .map_err(Into::into) + } + + /// Clear all browsing data for this webview. + /// + /// This deletes every website data type the underlying webview knows about: cookies, local storage, + /// session storage, IndexedDB databases, the HTTP cache and service workers. It does not reload the + /// current page, so the document that is currently loaded keeps its in-memory state until it navigates + /// or you call [`Self::reload`]. + /// + /// The deletion is asynchronous on every platform: the data is not necessarily gone when this function returns. + /// + /// ## Platform-specific + /// + /// - **Linux**: the data is cleared for the whole WebKit web context, which is shared by every webview + /// using the same data directory, not only this one. + /// - **macOS / iOS**: clears the [`WKWebsiteDataStore`] of this webview. Webviews created with a custom + /// `WebviewBuilder::data_store_identifier` have their own store, so only that store is affected. + /// - **Windows**: implemented with `ICoreWebView2Profile2::ClearBrowsingDataAll`, so an error is returned + /// if the installed WebView2 runtime is too old to provide it. + /// + /// [`WKWebsiteDataStore`]: https://developer.apple.com/documentation/webkit/wkwebsitedatastore + pub fn clear_all_browsing_data(&self) -> crate::Result<()> { + self + .webview + .dispatcher + .clear_all_browsing_data() + .map_err(Into::into) + } + + /// Returns all cookies in the runtime's cookie store including HTTP-only and secure cookies. + /// + /// Note that cookies will only be returned for URLs with an http or https scheme. + /// Cookies set through javascript for local files + /// (such as those served from the tauri://) protocol are not currently supported. + /// + /// # Stability + /// + /// See [Self::cookies]. + /// + /// # Known issues + /// + /// See [Self::cookies]. + pub fn cookies_for_url(&self, url: Url) -> crate::Result>> { + self + .webview + .dispatcher + .cookies_for_url(url) + .map_err(Into::into) + } + + /// Returns all cookies in the runtime's cookie store for all URLs including HTTP-only and secure cookies. + /// + /// Note that cookies will only be returned for URLs with an http or https scheme. + /// Cookies set through javascript for local files + /// (such as those served from the tauri://) protocol are not currently supported. + /// + /// # Stability + /// + /// The return value of this function leverages [`tauri_runtime::Cookie`] which re-exports the cookie crate. + /// This dependency might receive updates in minor Tauri releases. + /// + /// # Known issues + /// + /// On Windows, this function deadlocks when used in a synchronous command or event handlers, see [the Webview2 issue]. + /// You should use `async` commands and separate threads when reading cookies. + /// + /// ## Platform-specific + /// + /// - **Android**: Unsupported, always returns an empty [`Vec`]. + /// + /// [the Webview2 issue]: https://github.com/tauri-apps/wry/issues/583 + pub fn cookies(&self) -> crate::Result>> { + self.webview.dispatcher.cookies().map_err(Into::into) + } + + /// Set a cookie for the webview. + /// + /// # Stability + /// + /// See [Self::cookies]. + pub fn set_cookie(&self, cookie: Cookie<'_>) -> crate::Result<()> { + self + .webview + .dispatcher + .set_cookie(cookie) + .map_err(Into::into) + } + + /// Delete a cookie for the webview. + /// + /// # Stability + /// + /// See [Self::cookies]. + pub fn delete_cookie(&self, cookie: Cookie<'_>) -> crate::Result<()> { + self + .webview + .dispatcher + .delete_cookie(cookie) + .map_err(Into::into) + } +} + +impl Listener for Webview { + /// Listen to an event on this webview. + /// + /// # Examples + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +use tauri::{Manager, Listener}; + +tauri::Builder::default() + .setup(|app| { + let webview = app.get_webview("main").unwrap(); + webview.listen("component-loaded", move |event| { + println!("webview just loaded a component"); + }); + + Ok(()) + }); +``` + "#### + )] + fn listen(&self, event: impl Into, handler: F) -> EventId + where + F: Fn(Event) + Send + 'static, + { + let event = EventName::new(event.into()).unwrap(); + self.manager.listen( + event, + EventTarget::Webview { + label: self.label().to_string(), + }, + handler, + ) + } + + /// Listen to an event on this webview only once. + /// + /// See [`Self::listen`] for more information. + fn once(&self, event: impl Into, handler: F) -> EventId + where + F: FnOnce(Event) + Send + 'static, + { + let event = EventName::new(event.into()).unwrap(); + self.manager.once( + event, + EventTarget::Webview { + label: self.label().to_string(), + }, + handler, + ) + } + + /// Unlisten to an event on this webview. + /// + /// # Examples + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +use tauri::{Manager, Listener}; + +tauri::Builder::default() + .setup(|app| { + let webview = app.get_webview("main").unwrap(); + let webview_ = webview.clone(); + let handler = webview.listen("component-loaded", move |event| { + println!("webview just loaded a component"); + + // we no longer need to listen to the event + // we also could have used `webview.once` instead + webview_.unlisten(event.id()); + }); + + // stop listening to the event when you do not need it anymore + webview.unlisten(handler); + + Ok(()) + }); +``` + "#### + )] + fn unlisten(&self, id: EventId) { + self.manager.unlisten(id) + } +} + +impl Emitter for Webview {} + +impl Manager for Webview { + fn resources_table(&self) -> MutexGuard<'_, ResourceTable> { + self + .resources_table + .lock() + .expect("poisoned window resources table") + } +} + +impl ManagerBase for Webview { + fn manager(&self) -> &AppManager { + &self.manager + } + + fn manager_owned(&self) -> Arc> { + self.manager.clone() + } + + fn runtime(&self) -> RuntimeOrDispatch<'_, R> { + self.app_handle.runtime() + } + + fn managed_app_handle(&self) -> &AppHandle { + &self.app_handle + } + + #[cfg(target_os = "android")] + fn activity_name(&self) -> Option> { + Some(self.window().activity_name()) + } + + #[cfg(target_os = "ios")] + fn scene_identifier(&self) -> Option> { + Some(self.window().scene_identifier()) + } +} + +impl<'de, R: Runtime> CommandArg<'de, R> for Webview { + /// Grabs the [`Webview`] from the [`CommandItem`]. This will never fail. + fn from_command(command: CommandItem<'de, R>) -> Result { + Ok(command.message.webview()) + } +} + +/// Resolved scope that can be obtained via [`Webview::resolve_command_scope`]. +pub struct ResolvedScope { + command_scope: CommandScope, + global_scope: GlobalScope, +} + +impl ResolvedScope { + /// The global plugin scope. + pub fn global_scope(&self) -> &GlobalScope { + &self.global_scope + } + + /// The command-specific scope. + pub fn command_scope(&self) -> &CommandScope { + &self.command_scope + } +} + +#[cfg(test)] +mod tests { + use url::Url; + + #[test] + fn no_permission_handler_preserves_runtime_default() { + use crate::test::{mock_builder, mock_context, noop_assets}; + + let app = mock_builder().build(mock_context(noop_assets())).unwrap(); + let pending = super::WebviewBuilder::new("test", crate::WebviewUrl::default()) + .into_pending_webview(&app, "test") + .unwrap(); + + assert!(pending.permission_request_handler.is_none()); + } + + fn test_webview_window() -> crate::WebviewWindow { + use crate::test::{mock_builder, mock_context, noop_assets}; + + // Create a mock app with proper context + let app = mock_builder().build(mock_context(noop_assets())).unwrap(); + + // Create a webview window + crate::WebviewWindowBuilder::new(&app, "test", crate::WebviewUrl::default()) + .build() + .unwrap() + } + + #[test] + fn webview_is_send_sync() { + crate::test_utils::assert_send::(); + crate::test_utils::assert_sync::(); + } + + #[test] + fn convert_file_src_matches_js() { + use crate::test::{mock_builder, mock_context, noop_assets}; + + let app = mock_builder().build(mock_context(noop_assets())).unwrap(); + let http = crate::WebviewWindowBuilder::new(&app, "http", crate::WebviewUrl::default()) + .build() + .unwrap(); + let https = crate::WebviewWindowBuilder::new(&app, "https", crate::WebviewUrl::default()) + .use_https_scheme(true) + .build() + .unwrap(); + assert!(!http.webview.use_https_scheme()); + assert!(https.webview.use_https_scheme()); + + // `encoded` is what `encodeURIComponent(path)` returns in JS + #[cfg(windows)] + let (path, encoded) = ( + r"C:\Users\me\my-file (1).mp4", + "C%3A%5CUsers%5Cme%5Cmy-file%20(1).mp4", + ); + #[cfg(not(windows))] + let (path, encoded) = ( + "/home/me/my-file (1).mp4", + "%2Fhome%2Fme%2Fmy-file%20(1).mp4", + ); + + let convert = + |w: &crate::WebviewWindow<_>, protocol| w.convert_file_src(path, protocol).unwrap(); + + #[cfg(any(windows, target_os = "android"))] + { + assert_eq!( + convert(&http, None), + format!("http://asset.localhost/{encoded}") + ); + assert_eq!( + convert(&https, None), + format!("https://asset.localhost/{encoded}") + ); + assert_eq!( + convert(&http, Some("custom")), + format!("http://custom.localhost/{encoded}") + ); + } + #[cfg(not(any(windows, target_os = "android")))] + { + assert_eq!(convert(&http, None), format!("asset://localhost/{encoded}")); + assert_eq!( + convert(&https, None), + format!("asset://localhost/{encoded}") + ); + assert_eq!( + convert(&http, Some("custom")), + format!("custom://localhost/{encoded}") + ); + } + } + + #[cfg(unix)] + #[test] + fn convert_file_src_rejects_non_utf8_path() { + use std::os::unix::ffi::OsStrExt; + + let webview = test_webview_window(); + let path = std::path::Path::new(std::ffi::OsStr::from_bytes(b"/tmp/\xff.mp4")); + assert!(matches!( + webview.convert_file_src(path, None), + Err(crate::Error::NonUtf8Path(_)) + )); + } + + #[test] + fn tauri_protocol_is_local() { + let webview = test_webview_window().webview; + + #[cfg(all(not(windows), not(target_os = "android")))] + assert!(webview.is_local_url(&Url::parse("tauri://localhost/").unwrap())); + + #[cfg(any(windows, target_os = "android"))] + assert!(webview.is_local_url(&Url::parse("https://tauri.localhost/").unwrap())); + } + + // On Windows/Android, custom protocols are served as `https://.localhost/`. + // We ensure only `.localhost` domains are accepted to prevent a subdomain being able to + // impersonate a protocol name. + #[cfg(any(windows, target_os = "android"))] + #[test] + fn windows_custom_protocol_rejects_spoofed_domain() { + use crate::test::{mock_builder, mock_context, noop_assets}; + + let app = mock_builder() + .register_uri_scheme_protocol("myproto", |_, _| { + http::Response::builder().body(Vec::new()).unwrap() + }) + .build(mock_context(noop_assets())) + .unwrap(); + let webview = crate::WebviewWindowBuilder::new(&app, "test", crate::WebviewUrl::default()) + .build() + .unwrap() + .webview; + + let url = |s| Url::parse(s).unwrap(); + + // Legitimate Windows custom protocol URL + assert!(webview.is_local_url(&url("https://myproto.localhost/"))); + + // Attacker domain that starts with a registered protocol name — must NOT be local. + assert!(!webview.is_local_url(&url("https://myproto.evil.com/"))); + + // Subdomain of .localhost with unregistered name — must NOT be local + assert!(!webview.is_local_url(&url("https://notregistered.localhost/"))); + } + + /// Custom (non-plugin) commands must be rejected when the IPC request + /// originates from a remote URL, even when no `AppManifest` has been + /// configured. Only local (bundled) origins should be able to reach + /// custom commands. + #[test] + fn remote_origin_blocked_for_custom_commands_without_app_manifest() { + use crate::test::{INVOKE_KEY, mock_builder, mock_context, noop_assets}; + use crate::webview::InvokeRequest; + + let app = mock_builder().build(mock_context(noop_assets())).unwrap(); + + let webview = crate::WebviewWindowBuilder::new(&app, "main", Default::default()) + .build() + .unwrap(); + + // Request from a remote origin for a custom (non-plugin) command + // - should be rejected even without an AppManifest. + let remote_result = crate::test::get_ipc_response( + &webview, + InvokeRequest { + cmd: "any_custom_command".into(), + callback: crate::ipc::CallbackFn(0), + error: crate::ipc::CallbackFn(1), + url: "https://evil.com".parse().unwrap(), + body: crate::ipc::InvokeBody::default(), + headers: Default::default(), + invoke_key: INVOKE_KEY.to_string(), + }, + ); + assert!( + remote_result.is_err(), + "custom command should be rejected from a remote origin" + ); + + // Same command from the local origin - should NOT be rejected by the + // remote-origin guard (it may still fail because the command doesn't + // exist, but the error message will be different). + let local_result = crate::test::get_ipc_response( + &webview, + InvokeRequest { + cmd: "any_custom_command".into(), + callback: crate::ipc::CallbackFn(0), + error: crate::ipc::CallbackFn(1), + url: "tauri://localhost".parse().unwrap(), + body: crate::ipc::InvokeBody::default(), + headers: Default::default(), + invoke_key: INVOKE_KEY.to_string(), + }, + ); + // The local request should either succeed or fail for a reason OTHER + // than "not allowed from remote context". + if let Err(e) = &local_result { + let msg = e.to_string(); + assert!( + !msg.contains("not allowed from remote context"), + "local origin should not be blocked by the remote-origin guard, got: {msg}" + ); + } + } + + #[cfg(target_os = "macos")] + #[test] + fn test_webview_window_has_set_simple_fullscreen_method() { + let webview_window = test_webview_window(); + + // This should compile if set_simple_fullscreen exists + let result = webview_window.set_simple_fullscreen(true); + + // We expect this to work without panicking + assert!(result.is_ok(), "set_simple_fullscreen should succeed"); + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/webview/plugin.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/webview/plugin.rs new file mode 100644 index 000000000..a5c10a0d1 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/webview/plugin.rs @@ -0,0 +1,255 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! The tauri plugin to create and manipulate windows from JS. + +use crate::{ + AppHandle, Runtime, WebviewWindowBuilder, command, + plugin::{Builder, TauriPlugin}, + sealed::ManagerBase, + utils::config::WindowConfig, +}; + +#[derive(serde::Serialize)] +struct WebviewRef { + window_label: String, + label: String, +} + +#[command(root = "crate")] +async fn get_all_webviews(app: AppHandle) -> Vec { + app + .manager() + .webviews() + .values() + .map(|webview| WebviewRef { + window_label: webview.window_ref().label().into(), + label: webview.label().into(), + }) + .collect() +} + +#[command(root = "crate")] +async fn create_webview_window( + app: AppHandle, + options: WindowConfig, +) -> crate::Result<()> { + WebviewWindowBuilder::from_config(&app, &options)?.build()?; + Ok(()) +} + +#[cfg(desktop)] +mod desktop_commands { + use super::*; + use crate::{ + Webview, command, + runtime::dpi::{Position, Size}, + utils::config::Color, + }; + + fn get_webview( + webview: Webview, + label: Option, + ) -> crate::Result> { + match label { + Some(l) if !l.is_empty() => webview + .manager() + .get_webview(&l) + .ok_or(crate::Error::WebviewNotFound), + _ => Ok(webview), + } + } + + macro_rules! getter { + ($cmd: ident, $ret: ty) => { + getter!($cmd, $cmd, $ret) + }; + ($fn: ident, $cmd: ident, $ret: ty) => { + #[command(root = "crate")] + pub async fn $fn( + webview: Webview, + label: Option, + ) -> crate::Result<$ret> { + get_webview(webview, label)?.$cmd().map_err(Into::into) + } + }; + } + + macro_rules! setter { + ($cmd: ident) => { + setter!($cmd, $cmd); + }; + ($fn: ident, $cmd: ident) => { + #[command(root = "crate")] + pub async fn $fn( + webview: Webview, + label: Option, + ) -> crate::Result<()> { + get_webview(webview, label)?.$cmd().map_err(Into::into) + } + }; + ($fn: ident, $cmd: ident, $input: ty) => { + #[command(root = "crate")] + pub async fn $fn( + webview: Webview, + label: Option, + value: $input, + ) -> crate::Result<()> { + get_webview(webview, label)?.$cmd(value).map_err(Into::into) + } + }; + } + + // TODO + getter!( + webview_position, + position, + tauri_runtime::dpi::PhysicalPosition + ); + getter!(webview_size, size, tauri_runtime::dpi::PhysicalSize); + //getter!(is_focused, bool); + + setter!(print); + setter!(webview_close, close); + setter!(set_webview_size, set_size, Size); + setter!(set_webview_position, set_position, Position); + setter!(set_webview_focus, set_focus); + setter!(set_webview_auto_resize, set_auto_resize, bool); + setter!(webview_hide, hide); + setter!(webview_show, show); + setter!(set_webview_zoom, set_zoom, f64); + setter!( + set_webview_background_color, + set_background_color, + Option + ); + setter!(clear_all_browsing_data, clear_all_browsing_data); + + #[cfg(not(feature = "unstable"))] + #[command(root = "crate")] + pub async fn create_webview() -> crate::Result<()> { + Err(crate::Error::UnstableFeatureNotSupported) + } + + #[cfg(feature = "unstable")] + #[command(root = "crate")] + pub async fn create_webview( + app: crate::AppHandle, + window_label: String, + options: WindowConfig, + ) -> crate::Result<()> { + use anyhow::Context; + + let window = app + .manager() + .get_window(&window_label) + .ok_or(crate::Error::WindowNotFound)?; + + let x = options.x.context("missing parameter `options.x`")?; + let y = options.y.context("missing parameter `options.y`")?; + let width = options.width; + let height = options.height; + + let builder = crate::webview::WebviewBuilder::from_config(&options); + + window.add_child( + builder, + tauri_runtime::dpi::LogicalPosition::new(x, y), + tauri_runtime::dpi::LogicalSize::new(width, height), + )?; + + Ok(()) + } + + #[command(root = "crate")] + pub async fn reparent( + webview: crate::Webview, + label: Option, + window: String, + ) -> crate::Result<()> { + let webview = get_webview(webview, label)?; + if let Some(window) = webview.manager.get_window(&window) { + webview.reparent(&window)?; + } + Ok(()) + } + + #[cfg(any(debug_assertions, feature = "devtools"))] + #[command(root = "crate")] + pub async fn internal_toggle_devtools( + webview: crate::Webview, + label: Option, + ) -> crate::Result<()> { + let webview = get_webview(webview, label)?; + if webview.is_devtools_open() { + webview.close_devtools(); + } else { + webview.open_devtools(); + } + Ok(()) + } +} + +/// Initializes the plugin. +pub fn init() -> TauriPlugin { + #[allow(unused_mut)] + let mut init_script = String::new(); + // window.print works on Linux/Windows; need to use the API on macOS + #[cfg(any(target_os = "macos", target_os = "ios"))] + { + init_script.push_str(include_str!("./scripts/print.js")); + } + + #[cfg(any(debug_assertions, feature = "devtools"))] + { + use serialize_to_javascript::{DefaultTemplate, Template, default_template}; + + #[derive(Template)] + #[default_template("./scripts/toggle-devtools.js")] + struct Devtools<'a> { + os_name: &'a str, + } + + init_script.push_str( + &Devtools { + os_name: std::env::consts::OS, + } + .render_default(&Default::default()) + .unwrap() + .into_string(), + ); + } + + let mut builder = Builder::new("webview"); + if !init_script.is_empty() { + builder = builder.js_init_script(init_script); + } + + builder + .invoke_handler(crate::generate_handler![ + #![plugin(webview)] + create_webview_window, + get_all_webviews, + #[cfg(desktop)] desktop_commands::create_webview, + // getters + #[cfg(desktop)] desktop_commands::webview_position, + #[cfg(desktop)] desktop_commands::webview_size, + // setters + #[cfg(desktop)] desktop_commands::webview_close, + #[cfg(desktop)] desktop_commands::set_webview_size, + #[cfg(desktop)] desktop_commands::set_webview_position, + #[cfg(desktop)] desktop_commands::set_webview_focus, + #[cfg(desktop)] desktop_commands::set_webview_auto_resize, + #[cfg(desktop)] desktop_commands::set_webview_background_color, + #[cfg(desktop)] desktop_commands::set_webview_zoom, + #[cfg(desktop)] desktop_commands::webview_hide, + #[cfg(desktop)] desktop_commands::webview_show, + #[cfg(desktop)] desktop_commands::print, + #[cfg(desktop)] desktop_commands::clear_all_browsing_data, + #[cfg(desktop)] desktop_commands::reparent, + #[cfg(all(desktop, any(debug_assertions, feature = "devtools")))] + desktop_commands::internal_toggle_devtools, + ]) + .build() +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/webview/scripts/print.js b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/webview/scripts/print.js new file mode 100644 index 000000000..df5b92a1b --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/webview/scripts/print.js @@ -0,0 +1,7 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +window.print = function () { + return window.__TAURI_INTERNALS__.invoke('plugin:webview|print') +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/webview/scripts/toggle-devtools.js b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/webview/scripts/toggle-devtools.js new file mode 100644 index 000000000..5997187a1 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/webview/scripts/toggle-devtools.js @@ -0,0 +1,31 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +;(function () { + function toggleDevtoolsHotkey() { + const osName = __TEMPLATE_os_name__ + + const isHotkey = + osName === 'macos' + ? (event) => event.metaKey && event.altKey && event.code === 'KeyI' + : (event) => event.ctrlKey && event.shiftKey && event.code === 'KeyI' + + document.addEventListener('keydown', (event) => { + if (isHotkey(event)) { + window.__TAURI_INTERNALS__.invoke( + 'plugin:webview|internal_toggle_devtools' + ) + } + }) + } + + if ( + document.readyState === 'complete' + || document.readyState === 'interactive' + ) { + toggleDevtoolsHotkey() + } else { + window.addEventListener('DOMContentLoaded', toggleDevtoolsHotkey, true) + } +})() diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/webview/scripts/zoom-hotkey.js b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/webview/scripts/zoom-hotkey.js new file mode 100644 index 000000000..bc2622440 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/webview/scripts/zoom-hotkey.js @@ -0,0 +1,43 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +const OS_NAME = __TEMPLATE_os_name__ + +let zoomLevel = 1 + +const MAX_ZOOM_LEVEL = 10 +const MIN_ZOOM_LEVEL = 0.2 + +window.addEventListener('keydown', (event) => { + if (OS_NAME === 'macos' ? event.metaKey : event.ctrlKey) { + if (event.key === '-') { + zoomLevel -= 0.2 + } else if (event.key === '=' || event.key === '+') { + zoomLevel += 0.2 + } else if (event.key === '0') { + zoomLevel = 1 + } else { + return + } + zoomLevel = Math.min(Math.max(zoomLevel, MIN_ZOOM_LEVEL), MAX_ZOOM_LEVEL) + window.__TAURI_INTERNALS__.invoke('plugin:webview|set_webview_zoom', { + value: zoomLevel + }) + } +}) + +window.addEventListener('mousewheel', (event) => { + if (event.ctrlKey) { + event.preventDefault() + if (event.deltaY < 0) { + zoomLevel += 0.2 + } else { + zoomLevel -= 0.2 + } + zoomLevel = Math.min(Math.max(zoomLevel, MIN_ZOOM_LEVEL), MAX_ZOOM_LEVEL) + window.__TAURI_INTERNALS__.invoke('plugin:webview|set_webview_zoom', { + value: zoomLevel + }) + } +}) diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/webview/webview_window.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/webview/webview_window.rs new file mode 100644 index 000000000..7e9873a1d --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/webview/webview_window.rs @@ -0,0 +1,2874 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! [`Window`] that hosts a single [`Webview`]. + +use std::{ + borrow::Cow, + path::{Path, PathBuf}, + sync::{Arc, MutexGuard}, +}; + +use crate::{ + Emitter, EventName, Listener, ResourceTable, Window, + event::EventTarget, + ipc::ScopeObject, + runtime::dpi::{PhysicalPosition, PhysicalSize, Position, Size}, + webview::{NewWindowResponse, ScrollBarStyle}, + window::Monitor, +}; +#[cfg(desktop)] +use crate::{ + image::Image, + menu::{ContextMenu, Menu}, + runtime::{UserAttentionType, window::CursorIcon}, +}; +use tauri_runtime::webview::NewWindowFeatures; +use tauri_utils::config::{BackgroundThrottlingPolicy, Color, WebviewUrl, WindowConfig}; +use url::Url; + +use crate::{ + AppHandle, Event, EventId, Manager, Runtime, Webview, WindowEvent, + ipc::{CommandArg, CommandItem, InvokeError, OwnedInvokeResponder}, + manager::AppManager, + sealed::{ManagerBase, RuntimeOrDispatch}, + webview::{Cookie, PageLoadPayload, WebviewBuilder, WebviewEvent}, + window::WindowBuilder, +}; + +use tauri_macros::default_runtime; + +#[cfg(windows)] +use windows::Win32::Foundation::HWND; + +use super::{DownloadEvent, ResolvedScope}; + +/// A builder for [`WebviewWindow`], a window that hosts a single webview. +pub struct WebviewWindowBuilder<'a, R: Runtime, M: Manager> { + window_builder: WindowBuilder<'a, R, M>, + webview_builder: WebviewBuilder, +} + +impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { + /// Initializes a webview window builder with the given window label. + /// + /// # Known issues + /// + /// On Windows, this function deadlocks when used in a synchronous command and event handlers, see [the Webview2 issue]. + /// You should use `async` commands and separate threads when creating windows. + /// + /// # Examples + /// + /// - Create a window in the setup hook: + /// + /// ``` + /// tauri::Builder::default() + /// .setup(|app| { + /// let webview_window = tauri::WebviewWindowBuilder::new(app, "label", tauri::WebviewUrl::App("index.html".into())) + /// .build()?; + /// Ok(()) + /// }); + /// ``` + /// + /// - Create a window in a separate thread: + /// + /// ``` + /// tauri::Builder::default() + /// .setup(|app| { + /// let handle = app.handle().clone(); + /// std::thread::spawn(move || { + /// let webview_window = tauri::WebviewWindowBuilder::new(&handle, "label", tauri::WebviewUrl::App("index.html".into())) + /// .build() + /// .unwrap(); + /// }); + /// Ok(()) + /// }); + /// ``` + /// + /// - Create a window in a command: + /// + /// ``` + /// #[tauri::command] + /// async fn create_window(app: tauri::AppHandle) { + /// let webview_window = tauri::WebviewWindowBuilder::new(&app, "label", tauri::WebviewUrl::App("index.html".into())) + /// .build() + /// .unwrap(); + /// } + /// ``` + /// + /// [the Webview2 issue]: https://github.com/tauri-apps/wry/issues/583 + pub fn new>(manager: &'a M, label: L, url: WebviewUrl) -> Self { + let label = label.into(); + Self { + window_builder: WindowBuilder::new(manager, &label), + webview_builder: WebviewBuilder::new(&label, url), + } + } + + /// Initializes a webview window builder from a [`WindowConfig`] from tauri.conf.json. + /// Keep in mind that you can't create 2 windows with the same `label` so make sure + /// that the initial window was closed or change the label of the cloned [`WindowConfig`]. + /// + /// # Known issues + /// + /// On Windows, this function deadlocks when used in a synchronous command or event handlers, see [the Webview2 issue]. + /// You should use `async` commands and separate threads when creating windows. + /// + /// # Examples + /// + /// - Create a window in a command: + /// + /// ``` + /// #[tauri::command] + /// async fn reopen_window(app: tauri::AppHandle) { + /// let webview_window = tauri::WebviewWindowBuilder::from_config(&app, &app.config().app.windows.get(0).unwrap()) + /// .unwrap() + /// .build() + /// .unwrap(); + /// } + /// ``` + /// + /// - Create a window in a command from a config with a specific label, and change its label so multiple instances can exist: + /// + /// ``` + /// #[tauri::command] + /// async fn open_window_multiple(app: tauri::AppHandle) { + /// let mut conf = app.config().app.windows.iter().find(|c| c.label == "template-for-multiwindow").unwrap().clone(); + /// // This should be a unique label for all windows. For example, we can use a random suffix: + /// let mut buf = [0u8; 1]; + /// assert_eq!(getrandom::fill(&mut buf), Ok(())); + /// conf.label = format!("my-multiwindow-{}", buf[0]); + /// let webview_window = tauri::WebviewWindowBuilder::from_config(&app, &conf) + /// .unwrap() + /// .build() + /// .unwrap(); + /// } + /// ``` + /// + /// [the Webview2 issue]: https://github.com/tauri-apps/wry/issues/583 + pub fn from_config(manager: &'a M, config: &WindowConfig) -> crate::Result { + Ok(Self { + window_builder: WindowBuilder::from_config(manager, config)?, + webview_builder: WebviewBuilder::from_config(config), + }) + } + + /// Registers a global menu event listener. + /// + /// Note that this handler is called for any menu event, + /// whether it is coming from this window, another window or from the tray icon menu. + /// + /// Also note that this handler will not be called if + /// the window used to register it was closed. + /// + /// # Examples + /// ``` + /// use tauri::menu::{Menu, Submenu, MenuItem}; + /// tauri::Builder::default() + /// .setup(|app| { + /// let handle = app.handle(); + /// let save_menu_item = MenuItem::new(handle, "Save", true, None::<&str>)?; + /// let menu = Menu::with_items(handle, &[ + /// &Submenu::with_items(handle, "File", true, &[ + /// &save_menu_item, + /// ])?, + /// ])?; + /// let webview_window = tauri::WebviewWindowBuilder::new(app, "editor", tauri::WebviewUrl::App("index.html".into())) + /// .menu(menu) + /// .on_menu_event(move |window, event| { + /// if event.id == save_menu_item.id() { + /// // save menu item + /// } + /// }) + /// .build() + /// .unwrap(); + /// + /// Ok(()) + /// }); + /// ``` + #[cfg(desktop)] + pub fn on_menu_event, crate::menu::MenuEvent) + Send + Sync + 'static>( + mut self, + f: F, + ) -> Self { + self.window_builder = self.window_builder.on_menu_event(f); + self + } + + /// Defines a closure to be executed when the webview makes an HTTP request for a web resource, allowing you to modify the response. + /// + /// Currently only implemented for the `tauri` URI protocol. + /// + /// **NOTE:** Currently this is **not** executed when using external URLs such as a development server, + /// but it might be implemented in the future. **Always** check the request URL. + /// + /// # Examples + /// ```rust,no_run + /// use tauri::{ + /// utils::config::{Csp, CspDirectiveSources, WebviewUrl}, + /// webview::WebviewWindowBuilder, + /// }; + /// use http::header::HeaderValue; + /// use std::collections::HashMap; + /// tauri::Builder::default() + /// .setup(|app| { + /// let webview_window = WebviewWindowBuilder::new(app, "core", WebviewUrl::App("index.html".into())) + /// .on_web_resource_request(|request, response| { + /// if request.uri().scheme_str() == Some("tauri") { + /// // if we have a CSP header, Tauri is loading an HTML file + /// // for this example, let's dynamically change the CSP + /// if let Some(csp) = response.headers_mut().get_mut("Content-Security-Policy") { + /// // use the tauri helper to parse the CSP policy to a map + /// let mut csp_map: HashMap = Csp::Policy(csp.to_str().unwrap().to_string()).into(); + /// csp_map.entry("script-src".to_string()).or_insert_with(Default::default).push("'unsafe-inline'"); + /// // use the tauri helper to get a CSP string from the map + /// let csp_string = Csp::from(csp_map).to_string(); + /// *csp = HeaderValue::from_str(&csp_string).unwrap(); + /// } + /// } + /// }) + /// .build()?; + /// Ok(()) + /// }); + /// ``` + pub fn on_web_resource_request< + F: Fn(http::Request>, &mut http::Response>) + Send + Sync + 'static, + >( + mut self, + f: F, + ) -> Self { + self.webview_builder = self.webview_builder.on_web_resource_request(f); + self + } + + /// Defines a closure to be executed when the webview navigates to a URL. Returning `false` cancels the navigation. + /// + /// # Examples + /// ```rust,no_run + /// use tauri::{ + /// utils::config::{Csp, CspDirectiveSources, WebviewUrl}, + /// webview::WebviewWindowBuilder, + /// }; + /// use http::header::HeaderValue; + /// use std::collections::HashMap; + /// tauri::Builder::default() + /// .setup(|app| { + /// let webview_window = WebviewWindowBuilder::new(app, "core", WebviewUrl::App("index.html".into())) + /// .on_navigation(|url| { + /// // allow the production URL or localhost on dev + /// url.scheme() == "tauri" || (cfg!(dev) && url.host_str() == Some("localhost")) + /// }) + /// .build()?; + /// Ok(()) + /// }); + /// ``` + pub fn on_navigation bool + Send + 'static>(mut self, f: F) -> Self { + self.webview_builder = self.webview_builder.on_navigation(f); + self + } + + /// Set a new window request handler to decide if incoming url is allowed to be opened. + /// + /// A new window is requested to be opened by the [window.open] API. + /// + /// The closure take the URL to open and the window features object and returns [`NewWindowResponse`] to determine whether the window should open. + /// + /// # Examples + /// ```rust,no_run + /// use tauri::{ + /// utils::config::WebviewUrl, + /// webview::WebviewWindowBuilder, + /// }; + /// use http::header::HeaderValue; + /// use std::collections::HashMap; + /// tauri::Builder::default() + /// .setup(|app| { + /// let app_ = app.handle().clone(); + /// let webview_window = WebviewWindowBuilder::new(app, "core", WebviewUrl::App("index.html".into())) + /// .on_new_window(move |url, features| { + /// let builder = tauri::WebviewWindowBuilder::new( + /// &app_, + /// // note: add an ID counter or random label generator to support multiple opened windows at the same time + /// "opened-window", + /// tauri::WebviewUrl::External("about:blank".parse().unwrap()), + /// ) + /// .window_features(features) + /// .on_document_title_changed(|window, title| { + /// window.set_title(&title).unwrap(); + /// }) + /// .title(url.as_str()); + /// + /// let window = builder.build().unwrap(); + /// tauri::webview::NewWindowResponse::Create { window } + /// }) + /// .build()?; + /// Ok(()) + /// }); + /// ``` + /// + /// # Platform-specific + /// + /// - **Android / iOS**: Not supported. + /// + /// [window.open]: https://developer.mozilla.org/en-US/docs/Web/API/Window/open + pub fn on_new_window NewWindowResponse + Send + 'static>( + mut self, + f: F, + ) -> Self { + self.webview_builder = self.webview_builder.on_new_window(f); + self + } + + /// Defines a closure to be executed when the document title changes. + /// + /// Note that it may run before or after the navigation event. + pub fn on_document_title_changed, String) + Send + 'static>( + mut self, + f: F, + ) -> Self { + self.webview_builder = self + .webview_builder + .on_document_title_changed(move |webview, url| { + f( + WebviewWindow { + window: webview.window(), + webview, + }, + url, + ) + }); + self + } + + /// Set a download event handler to be notified when a download is requested or finished. + /// + /// Returning `false` prevents the download from happening on a [`DownloadEvent::Requested`] event. + /// + /// # Examples + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust,no_run +use tauri::{ + utils::config::{Csp, CspDirectiveSources, WebviewUrl}, + webview::{DownloadEvent, WebviewWindowBuilder}, +}; + +tauri::Builder::default() + .setup(|app| { + let handle = app.handle(); + let webview_window = WebviewWindowBuilder::new(handle, "core", WebviewUrl::App("index.html".into())) + .on_download(|webview, event| { + match event { + DownloadEvent::Requested { url, destination } => { + println!("downloading {}", url); + *destination = "/home/tauri/target/path".into(); + } + DownloadEvent::Finished { url, path, success } => { + println!("downloaded {} to {:?}, success: {}", url, path, success); + } + _ => (), + } + // let the download start + true + }) + .build()?; + + Ok(()) + }); +``` + "#### + )] + pub fn on_download, DownloadEvent<'_>) -> bool + Send + Sync + 'static>( + mut self, + f: F, + ) -> Self { + self.webview_builder.download_handler.replace(Arc::new(f)); + self + } + + /// Defines a closure to be executed when a page load event is triggered. + /// The event can be either [`tauri_runtime::webview::PageLoadEvent::Started`] if the page has started loading + /// or [`tauri_runtime::webview::PageLoadEvent::Finished`] when the page finishes loading. + /// + /// # Examples + /// ```rust,no_run + /// use tauri::{ + /// utils::config::{Csp, CspDirectiveSources, WebviewUrl}, + /// webview::{PageLoadEvent, WebviewWindowBuilder}, + /// }; + /// use http::header::HeaderValue; + /// use std::collections::HashMap; + /// tauri::Builder::default() + /// .setup(|app| { + /// let webview_window = WebviewWindowBuilder::new(app, "core", WebviewUrl::App("index.html".into())) + /// .on_page_load(|window, payload| { + /// match payload.event() { + /// PageLoadEvent::Started => { + /// println!("{} finished loading", payload.url()); + /// } + /// PageLoadEvent::Finished => { + /// println!("{} finished loading", payload.url()); + /// } + /// } + /// }) + /// .build()?; + /// Ok(()) + /// }); + /// ``` + pub fn on_page_load, PageLoadPayload<'_>) + Send + Sync + 'static>( + mut self, + f: F, + ) -> Self { + self.webview_builder = self.webview_builder.on_page_load(move |webview, payload| { + f( + WebviewWindow { + window: webview.window(), + webview, + }, + payload, + ) + }); + self + } + + /// Defines a closure to be executed when a permission is requested. + /// + /// The handler receives the [`crate::webview::PermissionKind`] and should return + /// the desired [`crate::webview::PermissionResponse`]. + /// + /// > [!NOTE] + /// > This handler only triggers for new permission requests. If the user has already + /// > allowed or denied a permission persistently within the webview, the browser + /// > will use the saved preference instead of calling this handler. + /// + /// ## Platform-specific: + /// + /// - **Windows**: Fully supported via WebView2's PermissionRequested event. + /// - **macOS / iOS**: Fully supported via WKUIDelegate's requestMediaCapturePermission. + /// - **Linux**: Fully supported via WebKitGTK's permission-request signal. + /// - **Android**: Supported via JNI bridge for geolocation, microphone, camera, + /// protected media, and MIDI requests. Android runtime permissions may still + /// trigger native OS prompts before access is granted. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri::{ + /// webview::{PermissionKind, PermissionResponse, WebviewWindowBuilder}, + /// WebviewUrl, + /// }; + /// tauri::Builder::default() + /// .setup(|app| { + /// WebviewWindowBuilder::new(app, "core", WebviewUrl::App("index.html".into())) + /// .on_permission_request(|_, kind| match kind { + /// PermissionKind::Geolocation => PermissionResponse::Allow, + /// PermissionKind::Notifications => PermissionResponse::Allow, + /// _ => PermissionResponse::Default, + /// }) + /// .build()?; + /// Ok(()) + /// }); + /// ``` + pub fn on_permission_request< + F: Fn(Webview, crate::webview::PermissionKind) -> crate::webview::PermissionResponse + + Send + + Sync + + 'static, + >( + mut self, + f: F, + ) -> Self { + self.webview_builder = self.webview_builder.on_permission_request(f); + self + } + + /// Creates a new window. + pub fn build(self) -> crate::Result> { + let (window, webview) = self.window_builder.with_webview(self.webview_builder)?; + Ok(WebviewWindow { window, webview }) + } +} + +/// Desktop APIs. +#[cfg(desktop)] +impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { + /// Sets the menu for the window. + #[must_use] + pub fn menu(mut self, menu: crate::menu::Menu) -> Self { + self.window_builder = self.window_builder.menu(menu); + self + } + + /// Show window in the center of the screen. + #[must_use] + pub fn center(mut self) -> Self { + self.window_builder = self.window_builder.center(); + self + } + + /// Prevent the window from overflowing the working area (e.g. monitor size - taskbar size) + /// on creation, which means the window size will be limited to `monitor size - taskbar size` + /// + /// **NOTE**: The overflow check is only performed on window creation, resizes can still overflow + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[must_use] + pub fn prevent_overflow(mut self) -> Self { + self.window_builder = self.window_builder.prevent_overflow(); + self + } + + /// Prevent the window from overflowing the working area (e.g. monitor size - taskbar size) + /// on creation with a margin, which means the window size will be limited to `monitor size - taskbar size - margin size` + /// + /// **NOTE**: The overflow check is only performed on window creation, resizes can still overflow + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[must_use] + pub fn prevent_overflow_with_margin(mut self, margin: impl Into) -> Self { + self.window_builder = self.window_builder.prevent_overflow_with_margin(margin); + self + } + + /// Whether the window's native maximize button is enabled or not. + /// If resizable is set to false, this setting is ignored. + /// + /// ## Platform-specific + /// + /// - **macOS:** Disables the "zoom" button in the window titlebar, which is also used to enter fullscreen mode. + /// - **Linux / iOS / Android:** Unsupported. + #[must_use] + pub fn maximizable(mut self, maximizable: bool) -> Self { + self.window_builder = self.window_builder.maximizable(maximizable); + self + } + + /// Whether the window's native minimize button is enabled or not. + /// + /// ## Platform-specific + /// + /// - **Linux / iOS / Android:** Unsupported. + #[must_use] + pub fn minimizable(mut self, minimizable: bool) -> Self { + self.window_builder = self.window_builder.minimizable(minimizable); + self + } + + /// Whether the window's native close button is enabled or not. + /// + /// ## Platform-specific + /// + /// - **Linux:** "GTK+ will do its best to convince the window manager not to show a close button. + /// Depending on the system, this function may not have any effect when called on a window that is already visible" + /// - **iOS / Android:** Unsupported. + #[must_use] + pub fn closable(mut self, closable: bool) -> Self { + self.window_builder = self.window_builder.closable(closable); + self + } + + /// Whether to start the window in fullscreen or not. + #[must_use] + pub fn fullscreen(mut self, fullscreen: bool) -> Self { + self.window_builder = self.window_builder.fullscreen(fullscreen); + self + } + + /// Whether the window will be initially focused or not. + #[must_use] + pub fn focused(mut self, focused: bool) -> Self { + self.window_builder = self.window_builder.focused(focused); + self.webview_builder = self.webview_builder.focused(focused); + self + } + + /// Whether the window should be maximized upon creation. + #[must_use] + pub fn maximized(mut self, maximized: bool) -> Self { + self.window_builder = self.window_builder.maximized(maximized); + self + } + + /// Whether the window should have borders and bars. + #[must_use] + pub fn decorations(mut self, decorations: bool) -> Self { + self.window_builder = self.window_builder.decorations(decorations); + self + } + + /// Whether the window should always be below other windows. + #[must_use] + pub fn always_on_bottom(mut self, always_on_bottom: bool) -> Self { + self.window_builder = self.window_builder.always_on_bottom(always_on_bottom); + self + } + + /// Whether the window should always be on top of other windows. + #[must_use] + pub fn always_on_top(mut self, always_on_top: bool) -> Self { + self.window_builder = self.window_builder.always_on_top(always_on_top); + self + } + + /// Whether the window will be visible on all workspaces or virtual desktops. + #[must_use] + pub fn visible_on_all_workspaces(mut self, visible_on_all_workspaces: bool) -> Self { + self.window_builder = self + .window_builder + .visible_on_all_workspaces(visible_on_all_workspaces); + self + } + + /// Sets the window icon. + pub fn icon(mut self, icon: Image<'a>) -> crate::Result { + self.window_builder = self.window_builder.icon(icon)?; + Ok(self) + } + + /// Sets whether or not the window icon should be hidden from the taskbar. + /// + /// ## Platform-specific + /// + /// - **macOS**: Unsupported. + #[must_use] + pub fn skip_taskbar(mut self, skip: bool) -> Self { + self.window_builder = self.window_builder.skip_taskbar(skip); + self + } + + /// Sets custom name for Windows' window class. **Windows only**. + #[must_use] + pub fn window_classname>(mut self, classname: S) -> Self { + self.window_builder = self.window_builder.window_classname(classname); + self + } + + /// This sets `WS_EX_NOREDIRECTIONBITMAP`. + /// + /// This can avoid the white flash that may appear before the webview content is rendered + /// when using a transparent window. **Windows only**. + #[must_use] + pub fn no_redirection_bitmap(mut self, enable: bool) -> Self { + self.window_builder = self.window_builder.no_redirection_bitmap(enable); + self + } + + /// Sets whether or not the window has shadow. + /// + /// ## Platform-specific + /// + /// - **Windows:** + /// - `false` has no effect on decorated window, shadows are always ON. + /// - `true` will make undecorated window have a 1px white border, + /// and on Windows 11, it will have a rounded corners. + /// - **Linux:** Unsupported. + #[must_use] + pub fn shadow(mut self, enable: bool) -> Self { + self.window_builder = self.window_builder.shadow(enable); + self + } + + /// Sets a parent to the window to be created. + /// + /// ## Platform-specific + /// + /// - **Windows**: This sets the passed parent as an owner window to the window to be created. + /// From [MSDN owned windows docs](https://docs.microsoft.com/en-us/windows/win32/winmsg/window-features#owned-windows): + /// - An owned window is always above its owner in the z-order. + /// - The system automatically destroys an owned window when its owner is destroyed. + /// - An owned window is hidden when its owner is minimized. + /// - **Linux**: This makes the new window transient for parent, see + /// - **macOS**: This adds the window as a child of parent, see + pub fn parent(mut self, parent: &WebviewWindow) -> crate::Result { + self.window_builder = self.window_builder.parent(&parent.window)?; + Ok(self) + } + + /// Set an owner to the window to be created. + /// + /// From MSDN: + /// - An owned window is always above its owner in the z-order. + /// - The system automatically destroys an owned window when its owner is destroyed. + /// - An owned window is hidden when its owner is minimized. + /// + /// For more information, see + #[cfg(windows)] + pub fn owner(mut self, owner: &WebviewWindow) -> crate::Result { + self.window_builder = self.window_builder.owner(&owner.window)?; + Ok(self) + } + + /// Set an owner to the window to be created. + /// + /// From MSDN: + /// - An owned window is always above its owner in the z-order. + /// - The system automatically destroys an owned window when its owner is destroyed. + /// - An owned window is hidden when its owner is minimized. + /// + /// For more information, see + #[cfg(windows)] + #[must_use] + pub fn owner_raw(mut self, owner: HWND) -> Self { + self.window_builder = self.window_builder.owner_raw(owner); + self + } + + /// Sets a parent to the window to be created. + /// + /// A child window has the WS_CHILD style and is confined to the client area of its parent window. + /// + /// For more information, see + #[cfg(windows)] + #[must_use] + pub fn parent_raw(mut self, parent: HWND) -> Self { + self.window_builder = self.window_builder.parent_raw(parent); + self + } + + /// Sets a parent to the window to be created. + /// + /// See + #[cfg(target_os = "macos")] + #[must_use] + pub fn parent_raw(mut self, parent: *mut std::ffi::c_void) -> Self { + self.window_builder = self.window_builder.parent_raw(parent); + self + } + + /// Sets the window to be created transient for parent. + /// + /// See + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + pub fn transient_for(mut self, parent: &WebviewWindow) -> crate::Result { + self.window_builder = self.window_builder.transient_for(&parent.window)?; + Ok(self) + } + + /// Sets the window to be created transient for parent. + /// + /// See + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + #[must_use] + pub fn transient_for_raw(mut self, parent: &impl gtk::glib::IsA) -> Self { + self.window_builder = self.window_builder.transient_for_raw(parent); + self + } + + /// Enables or disables drag and drop support of this window. + /// + /// Note: this is a different config from [`Self::disable_drag_drop_handler`] + #[cfg(windows)] + #[must_use] + pub fn drag_and_drop(mut self, enabled: bool) -> Self { + self.window_builder = self.window_builder.drag_and_drop(enabled); + self + } + + /// Sets the [`crate::TitleBarStyle`]. + #[cfg(target_os = "macos")] + #[must_use] + pub fn title_bar_style(mut self, style: crate::TitleBarStyle) -> Self { + self.window_builder = self.window_builder.title_bar_style(style); + self + } + + /// Change the position of the window controls on macOS. + /// + /// Requires titleBarStyle: Overlay and decorations: true. + #[cfg(target_os = "macos")] + #[must_use] + pub fn traffic_light_position>(mut self, position: P) -> Self { + self.webview_builder.webview_attributes = self + .webview_builder + .webview_attributes + .traffic_light_position(position.into()); + self + } + + /// Whether to show a link preview when long pressing on links. Available on macOS and iOS only. + /// + /// Default is true. + /// + /// See https://docs.rs/objc2-web-kit/latest/objc2_web_kit/struct.WKWebView.html#method.allowsLinkPreview + /// + /// ## Platform-specific + /// + /// - **Linux / Windows / Android:** Unsupported. + #[cfg(target_os = "macos")] + #[must_use] + pub fn allow_link_preview(mut self, allow_link_preview: bool) -> Self { + self.webview_builder = self.webview_builder.allow_link_preview(allow_link_preview); + self + } + + /// Hide the window title. + #[cfg(target_os = "macos")] + #[must_use] + pub fn hidden_title(mut self, hidden: bool) -> Self { + self.window_builder = self.window_builder.hidden_title(hidden); + self + } + + /// Defines the window [tabbing identifier] for macOS. + /// + /// Windows with matching tabbing identifiers will be grouped together. + /// If the tabbing identifier is not set, automatic tabbing will be disabled. + /// + /// [tabbing identifier]: + #[cfg(target_os = "macos")] + #[must_use] + pub fn tabbing_identifier(mut self, identifier: &str) -> Self { + self.window_builder = self.window_builder.tabbing_identifier(identifier); + self + } + + /// Sets window effects. + /// + /// Requires the window to be transparent. + /// + /// ## Platform-specific: + /// + /// - **Windows**: If using decorations or shadows, you may want to try this workaround + /// - **Linux**: Unsupported + pub fn effects(mut self, effects: crate::utils::config::WindowEffectsConfig) -> Self { + self.window_builder = self.window_builder.effects(effects); + self + } +} + +/// Window APIs. +impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { + /// The initial position of the window in logical pixels. + #[must_use] + pub fn position(mut self, x: f64, y: f64) -> Self { + self.window_builder = self.window_builder.position(x, y); + self + } + + /// Window size in logical pixels. + #[must_use] + pub fn inner_size(mut self, width: f64, height: f64) -> Self { + self.window_builder = self.window_builder.inner_size(width, height); + self + } + + /// Window min inner size in logical pixels. + #[must_use] + pub fn min_inner_size(mut self, min_width: f64, min_height: f64) -> Self { + self.window_builder = self.window_builder.min_inner_size(min_width, min_height); + self + } + + /// Window max inner size in logical pixels. + #[must_use] + pub fn max_inner_size(mut self, max_width: f64, max_height: f64) -> Self { + self.window_builder = self.window_builder.max_inner_size(max_width, max_height); + self + } + + /// Window inner size constraints. + #[must_use] + pub fn inner_size_constraints( + mut self, + constraints: tauri_runtime::window::WindowSizeConstraints, + ) -> Self { + self.window_builder = self.window_builder.inner_size_constraints(constraints); + self + } + + /// Whether the window is resizable or not. + /// When resizable is set to false, native window's maximize button is automatically disabled. + #[must_use] + pub fn resizable(mut self, resizable: bool) -> Self { + self.window_builder = self.window_builder.resizable(resizable); + self + } + + /// The title of the window in the title bar. + #[must_use] + pub fn title>(mut self, title: S) -> Self { + self.window_builder = self.window_builder.title(title); + self + } + + /// Sets the window to be initially focused. + #[must_use] + #[deprecated( + since = "1.2.0", + note = "The window is automatically focused by default. This function Will be removed in 3.0.0. Use `focused` instead." + )] + pub fn focus(mut self) -> Self { + self.window_builder = self.window_builder.focused(true); + self.webview_builder = self.webview_builder.focused(true); + self + } + + /// Whether the window will be focusable or not. + #[must_use] + pub fn focusable(mut self, focusable: bool) -> Self { + self.window_builder = self.window_builder.focusable(focusable); + self + } + + /// Whether the window should be immediately visible upon creation. + #[must_use] + pub fn visible(mut self, visible: bool) -> Self { + self.window_builder = self.window_builder.visible(visible); + self + } + + /// Forces a theme or uses the system settings if None was provided. + /// + /// ## Platform-specific + /// + /// - **macOS**: Only supported on macOS 10.14+. + #[must_use] + pub fn theme(mut self, theme: Option) -> Self { + self.window_builder = self.window_builder.theme(theme); + self + } + + /// Prevents the window contents from being captured by other apps. + #[must_use] + pub fn content_protected(mut self, protected: bool) -> Self { + self.window_builder = self.window_builder.content_protected(protected); + self + } +} + +/// Webview attributes. +impl> WebviewWindowBuilder<'_, R, M> { + /// Sets whether clicking an inactive window also clicks through to the webview. + #[must_use] + pub fn accept_first_mouse(mut self, accept: bool) -> Self { + self.webview_builder = self.webview_builder.accept_first_mouse(accept); + self + } + + /// Adds the provided JavaScript to a list of scripts that should be run after the global object has been created, + /// but before the HTML document has been parsed and before any other script included by the HTML document is run. + /// + /// Since it runs on all top-level document navigations, + /// it's recommended to check the `window.location` to guard your script from running on unexpected origins. + /// + /// This is executed only on the main frame. + /// If you only want to run it in all frames, use [Self::initialization_script_for_all_frames] instead. + /// + /// ## Platform-specific + /// + /// - **Windows:** scripts are always added to subframes. + /// - **Android:** When [addDocumentStartJavaScript] is not supported, + /// we prepend initialization scripts to each HTML head (implementation only supported on custom protocol URLs). + /// For remote URLs, we use [onPageStarted] which is not guaranteed to run before other scripts. + /// + /// # Examples + /// + /// ```rust + /// const INIT_SCRIPT: &str = r#" + /// if (window.location.origin === 'https://tauri.app') { + /// console.log("hello world from js init script"); + /// + /// window.__MY_CUSTOM_PROPERTY__ = { foo: 'bar' }; + /// } + /// "#; + /// + /// fn main() { + /// tauri::Builder::default() + /// .setup(|app| { + /// let webview = tauri::WebviewWindowBuilder::new(app, "label", tauri::WebviewUrl::App("index.html".into())) + /// .initialization_script(INIT_SCRIPT) + /// .build()?; + /// Ok(()) + /// }); + /// } + /// ``` + /// + /// [addDocumentStartJavaScript]: https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E) + /// [onPageStarted]: https://developer.android.com/reference/android/webkit/WebViewClient#onPageStarted(android.webkit.WebView,%20java.lang.String,%20android.graphics.Bitmap) + #[must_use] + pub fn initialization_script(mut self, script: impl Into) -> Self { + self.webview_builder = self.webview_builder.initialization_script(script); + self + } + + /// Adds the provided JavaScript to a list of scripts that should be run after the global object has been created, + /// but before the HTML document has been parsed and before any other script included by the HTML document is run. + /// + /// Since it runs on all top-level document navigations and also child frame page navigations, + /// it's recommended to check the `window.location` to guard your script from running on unexpected origins. + /// + /// This is executed on all frames (main frame and also sub frames). + /// If you only want to run the script in the main frame, use [Self::initialization_script] instead. + /// + /// ## Platform-specific + /// + /// - **Android:** When [addDocumentStartJavaScript] is not supported, + /// we prepend initialization scripts to each HTML head (implementation only supported on custom protocol URLs). + /// For remote URLs, we use [onPageStarted] which is not guaranteed to run before other scripts. + /// + /// # Examples + /// + /// ```rust + /// const INIT_SCRIPT: &str = r#" + /// if (window.location.origin === 'https://tauri.app') { + /// console.log("hello world from js init script"); + /// + /// window.__MY_CUSTOM_PROPERTY__ = { foo: 'bar' }; + /// } + /// "#; + /// + /// fn main() { + /// tauri::Builder::default() + /// .setup(|app| { + /// let webview = tauri::WebviewWindowBuilder::new(app, "label", tauri::WebviewUrl::App("index.html".into())) + /// .initialization_script_for_all_frames(INIT_SCRIPT) + /// .build()?; + /// Ok(()) + /// }); + /// } + /// ``` + /// + /// [addDocumentStartJavaScript]: https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E) + /// [onPageStarted]: https://developer.android.com/reference/android/webkit/WebViewClient#onPageStarted(android.webkit.WebView,%20java.lang.String,%20android.graphics.Bitmap) + #[must_use] + pub fn initialization_script_for_all_frames(mut self, script: impl Into) -> Self { + self.webview_builder = self + .webview_builder + .initialization_script_for_all_frames(script); + self + } + + /// Set the user agent for the webview + #[must_use] + pub fn user_agent(mut self, user_agent: &str) -> Self { + self.webview_builder = self.webview_builder.user_agent(user_agent); + self + } + + /// Set additional arguments for the webview. + /// + /// ## Platform-specific + /// + /// - **macOS / Linux / Android / iOS**: Unsupported. + /// + /// ## Warning + /// + /// Webview instances with different browser arguments must also have different [data directories](Self::data_directory). + /// + /// By default wry passes `--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection` + /// so if you use this method, you also need to disable these components by yourself if you want. + #[must_use] + pub fn additional_browser_args(mut self, additional_args: &str) -> Self { + self.webview_builder = self + .webview_builder + .additional_browser_args(additional_args); + self + } + + /// Data directory for the webview. + #[must_use] + pub fn data_directory(mut self, data_directory: PathBuf) -> Self { + self.webview_builder = self.webview_builder.data_directory(data_directory); + self + } + + /// Disables the webview drag and drop handler used internally to generate [`DragDropEvent`](crate::DragDropEvent)s. + /// + /// This is required to use HTML5 drag and drop APIs on the frontend on Windows since we replace the drag drop handler of WebView2. + #[must_use] + pub fn disable_drag_drop_handler(mut self) -> Self { + self.webview_builder = self.webview_builder.disable_drag_drop_handler(); + self + } + + /// Enables clipboard access for the page rendered on **Linux** and **Windows**. + /// + /// **macOS** doesn't provide such method and is always enabled by default, + /// but you still need to add menu item accelerators to use shortcuts. + #[must_use] + pub fn enable_clipboard_access(mut self) -> Self { + self.webview_builder = self.webview_builder.enable_clipboard_access(); + self + } + + /// Enable or disable incognito mode for the WebView.. + /// + /// ## Platform-specific: + /// + /// **Android**: Unsupported. + #[must_use] + pub fn incognito(mut self, incognito: bool) -> Self { + self.webview_builder = self.webview_builder.incognito(incognito); + self + } + + /// Sets the webview to automatically grow and shrink its size and position when the parent window resizes. + #[must_use] + pub fn auto_resize(mut self) -> Self { + self.webview_builder = self.webview_builder.auto_resize(); + self + } + + /// Set a proxy URL for the WebView for all network requests. + /// + /// Must be either a `http://` or a `socks5://` URL. + #[must_use] + pub fn proxy_url(mut self, url: Url) -> Self { + self.webview_builder = self.webview_builder.proxy_url(url); + self + } + + /// Whether the window should be transparent. If this is true, writing colors + /// with alpha values different than `1.0` will produce a transparent window. + /// + /// On Windows, using `no_redirection_bitmap` can help avoid a white flash when + /// creating a transparent window. + #[must_use] + pub fn transparent(mut self, transparent: bool) -> Self { + #[cfg(desktop)] + { + self.window_builder = self.window_builder.transparent(transparent); + } + self.webview_builder = self.webview_builder.transparent(transparent); + self + } + + /// Whether page zooming by hotkeys and mousewheel should be enabled or not. + /// + /// ## Platform-specific: + /// + /// - **Windows**: Controls WebView2's [`IsZoomControlEnabled`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2settings?view=webview2-winrt-1.0.2420.47#iszoomcontrolenabled) setting. + /// - **MacOS / Linux**: Injects a polyfill that zooms in and out with `Ctrl/Cmd + [- = +]` hotkeys or mousewheel events, + /// 20% in each step, ranging from 20% to 1000%. Requires `core:webview:allow-set-webview-zoom` permission + /// + /// - **Android / iOS**: Unsupported. + #[must_use] + pub fn zoom_hotkeys_enabled(mut self, enabled: bool) -> Self { + self.webview_builder = self.webview_builder.zoom_hotkeys_enabled(enabled); + self + } + + /// Whether browser extensions can be installed for the webview process + /// + /// ## Platform-specific: + /// + /// - **Windows**: Enables the WebView2 environment's [`AreBrowserExtensionsEnabled`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2environmentoptions?view=webview2-winrt-1.0.2739.15#arebrowserextensionsenabled) + /// - **MacOS / Linux / iOS / Android** - Unsupported. + #[must_use] + pub fn browser_extensions_enabled(mut self, enabled: bool) -> Self { + self.webview_builder = self.webview_builder.browser_extensions_enabled(enabled); + self + } + + /// Set the path from which to load extensions from. Extensions stored in this path should be unpacked Chrome extensions on Windows, and compiled `.so` extensions on Linux. + /// + /// ## Platform-specific: + /// + /// - **Windows**: Browser extensions must first be enabled. See [`browser_extensions_enabled`](Self::browser_extensions_enabled) + /// - **MacOS / iOS / Android** - Unsupported. + #[must_use] + pub fn extensions_path(mut self, path: impl AsRef) -> Self { + self.webview_builder = self.webview_builder.extensions_path(path); + self + } + + /// Initialize the WebView with a custom data store identifier. + /// Can be used as a replacement for data_directory not being available in WKWebView. + /// + /// - **macOS / iOS**: Available on macOS >= 14 and iOS >= 17 + /// - **Windows / Linux / Android**: Unsupported. + #[must_use] + pub fn data_store_identifier(mut self, data_store_identifier: [u8; 16]) -> Self { + self.webview_builder = self + .webview_builder + .data_store_identifier(data_store_identifier); + self + } + + /// Sets whether the custom protocols should use `https://.localhost` instead of the default `http://.localhost` on Windows and Android. Defaults to `false`. + /// + /// ## Note + /// + /// Using a `https` scheme will NOT allow mixed content when trying to fetch `http` endpoints and therefore will not match the behavior of the `://localhost` protocols used on macOS and Linux. + /// + /// ## Warning + /// + /// Changing this value between releases will change the IndexedDB, cookies and localstorage location and your app will not be able to access the old data. + #[must_use] + pub fn use_https_scheme(mut self, enabled: bool) -> Self { + self.webview_builder = self.webview_builder.use_https_scheme(enabled); + self + } + + /// Whether web inspector, which is usually called browser devtools, is enabled or not. Enabled by default. + /// + /// This API works in **debug** builds, but requires `devtools` feature flag to enable it in **release** builds. + /// + /// ## Platform-specific + /// + /// - macOS: This will call private functions on **macOS**. + /// - Android: Open `chrome://inspect/#devices` in Chrome to get the devtools window. Wry's `WebView` devtools API isn't supported on Android. + /// - iOS: Open Safari > Develop > [Your Device Name] > [Your WebView] to get the devtools window. + #[must_use] + pub fn devtools(mut self, enabled: bool) -> Self { + self.webview_builder = self.webview_builder.devtools(enabled); + self + } + + /// Set the window and webview background color. + /// + /// ## Platform-specific: + /// + /// - **Android / iOS:** Unsupported for the window layer. + /// - **macOS / iOS**: Not implemented for the webview layer. + /// - **Windows**: + /// - alpha channel is ignored for the window layer. + /// - On Windows 7, alpha channel is ignored for the webview layer. + /// - On Windows 8 and newer, if alpha channel is not `0`, it will be ignored. + #[must_use] + pub fn background_color(mut self, color: Color) -> Self { + self.window_builder = self.window_builder.background_color(color); + self.webview_builder = self.webview_builder.background_color(color); + self + } + + /// Change the default background throttling behaviour. + /// + /// By default, browsers use a suspend policy that will throttle timers and even unload + /// the whole tab (view) to free resources after roughly 5 minutes when a view became + /// minimized or hidden. This will pause all tasks until the documents visibility state + /// changes back from hidden to visible by bringing the view back to the foreground. + /// + /// ## Platform-specific + /// + /// - **Linux / Windows / Android**: Unsupported. Workarounds like a pending WebLock transaction might suffice. + /// - **iOS**: Supported since version 17.0+. + /// - **macOS**: Supported since version 14.0+. + /// + /// see + #[must_use] + pub fn background_throttling(mut self, policy: BackgroundThrottlingPolicy) -> Self { + self.webview_builder = self.webview_builder.background_throttling(policy); + self + } + + /// Whether JavaScript should be disabled. + #[must_use] + pub fn disable_javascript(mut self) -> Self { + self.webview_builder = self.webview_builder.disable_javascript(); + self + } + + /// Specifies the native scrollbar style to use with the webview. + /// CSS styles that modifier the scrollbar are applied on top of the native appearance configured here. + /// + /// Defaults to [`ScrollBarStyle::Default`], which is the browser default. + /// + /// ## Platform-specific + /// + /// - **Windows**: + /// - [`ScrollBarStyle::FluentOverlay`] requires WebView2 Runtime version 125.0.2535.41 or higher, + /// and does nothing on older versions. + /// - This option must be given the same value for all webviews that target the same data directory. Use + /// [`WebviewWindowBuilder::data_directory`] to change data directories if needed. + /// - **Linux / Android / iOS / macOS**: Unsupported. Only supports `Default` and performs no operation. + #[must_use] + pub fn scroll_bar_style(mut self, style: ScrollBarStyle) -> Self { + self.webview_builder = self.webview_builder.scroll_bar_style(style); + self + } + + /// Controls the WebView's browser-level general autofill behavior. + /// + /// **This option does not disable password or credit card autofill.** + /// + /// When set to `false`, the WebView will not automatically populate + /// general form fields using previously stored data such as addresses + /// or contact information. + /// + /// By default, this is `true`. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported. WebView2's autofill feature (called + /// "Suggestions") may not honor `autocomplete="off"` on input + /// elements in some cases. + /// - **Linux / Android / iOS / macOS**: Unsupported and performs no + /// operation. + #[must_use] + pub fn general_autofill_enabled(mut self, enabled: bool) -> Self { + self.webview_builder = self.webview_builder.general_autofill_enabled(enabled); + self + } + + /// Allows overriding the keyboard accessory view on iOS. + /// Returning `None` effectively removes the view. + /// + /// The closure parameter is the webview instance. + /// + /// The accessory view is the view that appears above the keyboard when a text input element is focused. + /// It usually displays a view with "Done", "Next" buttons. + /// + /// # Examples + /// + /// ``` + /// tauri::Builder::default() + /// .setup(|app| { + /// let mut builder = tauri::WebviewWindowBuilder::new(app, "label", tauri::WebviewUrl::App("index.html".into())); + /// #[cfg(target_os = "ios")] + /// { + /// window_builder = window_builder.with_input_accessory_view_builder(|_webview| unsafe { + /// let mtm = objc2::MainThreadMarker::new_unchecked(); + /// let button = objc2_ui_kit::UIButton::buttonWithType(objc2_ui_kit::UIButtonType(1), mtm); + /// button.setTitle_forState( + /// Some(&objc2_foundation::NSString::from_str("Tauri")), + /// objc2_ui_kit::UIControlState(0), + /// ); + /// Some(button.downcast().unwrap()) + /// }); + /// } + /// let webview = builder.build()?; + /// Ok(()) + /// }); + /// ``` + /// + /// # Stability + /// + /// This relies on [`objc2_ui_kit`] which does not provide a stable API yet, so it can receive breaking changes in minor releases. + #[cfg(target_os = "ios")] + pub fn with_input_accessory_view_builder< + F: Fn(&objc2_ui_kit::UIView) -> Option> + + Send + + Sync + + 'static, + >( + mut self, + builder: F, + ) -> Self { + self.webview_builder = self + .webview_builder + .with_input_accessory_view_builder(builder); + self + } + + /// Whether to limit navigations to App-Bound Domains. This is necessary to + /// enable Service Workers on iOS according to + /// [StackOverflow](https://stackoverflow.com/questions/49673399/service-workers-unavailable-in-wkwebview-in-ios-11-3/64155509#64155509). + /// + /// Default is false. + /// + /// Note: If you pass in `true` make sure to add localhost and any [`registrable + /// domains`](https://developer.mozilla.org/en-US/docs/Glossary/Registrable_domain) + /// used in this webview to tauri-src/Info.ios.plist: + /// + /// ```xml + /// + /// + /// WKAppBoundDomains + /// + /// localhost + /// aregistrabledomain.example + /// + /// + /// + /// ``` + /// + /// You must add `localhost` if any webview with this set to true opens a + /// local webpage, makes any localhost calls, or uses the isolation pattern + /// because Tauri uses the `localhost` domain for hosting the application + /// webpage, the IPC protocol, and the isolation pattern's iframe. + /// + /// Requests served through custom uri schemes are allowed so long as they use + /// a registrable domain specified in the `WKAppBoundDomains` array for all the + /// requests from the app, including requests for the `localhost` domain. + /// + /// In theory, you can whitelist an entire uri scheme by including the + /// protocol name followed by a colon. For example, to allow all requests + /// using a custom "stream" uri scheme (see [this tauri + /// example](https://github.com/tauri-apps/tauri/blob/dev/examples/streaming/main.rs)), + /// you could add `stream:` to the AppBoundDomains array. That said, I'm not + /// sure whether Apple would let your app through app review if you do + /// whitelist an entire protocol because this feature is not mentioned in + /// [their blog post on App-Bound + /// Domains](https://webkit.org/blog/10882/app-bound-domains/). + /// + /// See https://webkit.org/blog/10882/app-bound-domains/ and + /// https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/limitsnavigationstoappbounddomains + /// for the official documentation on App-Bound Domains. + /// + /// ## Platform-specific + /// + /// - **iOS**: Supported since version 14.0+. + /// - **Linux / Windows / Android / MacOS:** Unsupported. + pub fn limit_navigations_to_app_bound_domains(mut self, limit_navigations: bool) -> Self { + self.webview_builder = self + .webview_builder + .limit_navigations_to_app_bound_domains(limit_navigations); + self + } + + /// Set the environment for the webview. + /// Useful if you need to share the same environment, for instance when using the [`Self::on_new_window`]. + #[cfg(all(feature = "wry", windows))] + pub fn with_environment( + mut self, + environment: webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Environment, + ) -> Self { + self.webview_builder = self.webview_builder.with_environment(environment); + self + } + + /// Creates a new webview sharing the same web process with the provided webview. + /// Useful if you need to link a webview to another, for instance when using the [`Self::on_new_window`]. + #[cfg(all( + feature = "wry", + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + ) + ))] + pub fn with_related_view(mut self, related_view: webkit2gtk::WebView) -> Self { + self.webview_builder = self.webview_builder.with_related_view(related_view); + self + } + + /// Set the webview configuration. + /// Useful if you need to share the same webview configuration, for instance when using the [`Self::on_new_window`]. + #[cfg(target_os = "macos")] + pub fn with_webview_configuration( + mut self, + webview_configuration: objc2::rc::Retained, + ) -> Self { + self.webview_builder = self + .webview_builder + .with_webview_configuration(webview_configuration); + self + } + + /// Set the window features. + /// Useful if you need to share the same window features, for instance when using the [`Self::on_new_window`]. + #[cfg(any( + target_os = "macos", + windows, + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + pub fn window_features(mut self, features: NewWindowFeatures) -> Self { + if let Some(position) = features.position() { + self.window_builder = self.window_builder.position(position.x, position.y); + } + + if let Some(size) = features.size() { + self.window_builder = self.window_builder.inner_size(size.width, size.height); + } + + #[cfg(target_os = "macos")] + { + self.webview_builder = self + .webview_builder + .with_webview_configuration(features.opener().target_configuration.clone()); + } + + #[cfg(all(feature = "wry", windows))] + { + self.webview_builder = self + .webview_builder + .with_environment(features.opener().environment.clone()); + } + + #[cfg(all( + feature = "wry", + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + ) + ))] + { + self.webview_builder = self + .webview_builder + .with_related_view(features.opener().webview.clone()); + } + self + } +} + +// Android specific APIs +#[cfg(target_os = "android")] +impl> WebviewWindowBuilder<'_, R, M> { + /// The name of the activity to create for this webview window. + pub fn activity_name>(mut self, class_name: S) -> Self { + self.window_builder = self.window_builder.activity_name(class_name); + self + } + + /// Sets the name of the activity that is creating this webview window. + /// + /// This is important to determine which stack the activity will belong to. + pub fn created_by_activity_name>(mut self, class_name: S) -> Self { + self.window_builder = self.window_builder.created_by_activity_name(class_name); + self + } +} + +/// iOS specific APIs +#[cfg(target_os = "ios")] +impl> WebviewWindowBuilder<'_, R, M> { + /// Sets the identifier of the scene that is requesting the new scene, + /// establishing a relationship between the two scenes. + /// + /// By default the system uses the foreground scene. + #[cfg(target_os = "ios")] + pub fn requested_by_scene_identifier(mut self, identifier: String) -> Self { + self.window_builder = self + .window_builder + .requested_by_scene_identifier(identifier); + self + } +} + +/// A type that wraps a [`Window`] together with a [`Webview`]. +#[default_runtime(crate::Wry, wry)] +#[derive(Debug)] +pub struct WebviewWindow { + pub(crate) window: Window, + pub(crate) webview: Webview, +} + +impl AsRef> for WebviewWindow { + fn as_ref(&self) -> &Webview { + &self.webview + } +} + +impl Clone for WebviewWindow { + fn clone(&self) -> Self { + Self { + window: self.window.clone(), + webview: self.webview.clone(), + } + } +} + +impl Eq for WebviewWindow {} +impl PartialEq for WebviewWindow { + /// Only use the [`Webview`]'s label to compare equality. + fn eq(&self, other: &Self) -> bool { + self.webview.eq(&other.webview) + } +} + +impl raw_window_handle::HasWindowHandle for WebviewWindow { + fn window_handle( + &self, + ) -> std::result::Result, raw_window_handle::HandleError> { + Ok(unsafe { + raw_window_handle::WindowHandle::borrow_raw(self.window.window_handle()?.as_raw()) + }) + } +} + +impl raw_window_handle::HasDisplayHandle for WebviewWindow { + fn display_handle( + &self, + ) -> std::result::Result, raw_window_handle::HandleError> { + self.webview.app_handle.display_handle() + } +} + +impl<'de, R: Runtime> CommandArg<'de, R> for WebviewWindow { + /// Grabs the [`Window`] from the [`CommandItem`]. This will never fail. + fn from_command(command: CommandItem<'de, R>) -> Result { + let webview = command.message.webview(); + let window = webview.window(); + if window.is_webview_window() { + return Ok(Self { window, webview }); + } + + Err(InvokeError::from("current webview is not a WebviewWindow")) + } +} + +/// Base webview window functions. +impl WebviewWindow { + /// Initializes a [`WebviewWindowBuilder`] with the given window label and webview URL. + /// + /// Data URLs are only supported with the `webview-data-url` feature flag. + pub fn builder, L: Into>( + manager: &M, + label: L, + url: WebviewUrl, + ) -> WebviewWindowBuilder<'_, R, M> { + WebviewWindowBuilder::new(manager, label, url) + } + + /// Runs the given closure on the main thread. + pub fn run_on_main_thread(&self, f: F) -> crate::Result<()> { + self.webview.run_on_main_thread(f) + } + + /// The webview label. + pub fn label(&self) -> &str { + self.webview.label() + } + + /// Registers a window event listener. + pub fn on_window_event(&self, f: F) { + self.window.on_window_event(f); + } + + /// Registers a webview event listener. + pub fn on_webview_event(&self, f: F) { + self.webview.on_webview_event(f); + } + + /// Resolves the given command scope for this webview on the currently loaded URL. + /// + /// If the command is not allowed, returns None. + /// + /// If the scope cannot be deserialized to the given type, an error is returned. + /// + /// In a command context this can be directly resolved from the command arguments via [crate::ipc::CommandScope]: + /// + /// ``` + /// use tauri::ipc::CommandScope; + /// + /// #[derive(Debug, serde::Deserialize)] + /// struct ScopeType { + /// some_value: String, + /// } + /// #[tauri::command] + /// fn my_command(scope: CommandScope) { + /// // check scope + /// } + /// ``` + /// + /// # Examples + /// + /// ``` + /// use tauri::Manager; + /// + /// #[derive(Debug, serde::Deserialize)] + /// struct ScopeType { + /// some_value: String, + /// } + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let webview = app.get_webview_window("main").unwrap(); + /// let scope = webview.resolve_command_scope::("my-plugin", "read"); + /// Ok(()) + /// }); + /// ``` + pub fn resolve_command_scope( + &self, + plugin: &str, + command: &str, + ) -> crate::Result>> { + self.webview.resolve_command_scope(plugin, command) + } +} + +/// Menu APIs +#[cfg(desktop)] +impl WebviewWindow { + /// Registers a global menu event listener. + /// + /// Note that this handler is called for any menu event, + /// whether it is coming from this window, another window or from the tray icon menu. + /// + /// Also note that this handler will not be called if + /// the window used to register it was closed. + /// + /// # Examples + /// + /// ``` + /// use tauri::menu::{Menu, Submenu, MenuItem}; + /// use tauri::{WebviewWindowBuilder, WebviewUrl}; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let handle = app.handle(); + /// let save_menu_item = MenuItem::new(handle, "Save", true, None::<&str>)?; + /// let menu = Menu::with_items(handle, &[ + /// &Submenu::with_items(handle, "File", true, &[ + /// &save_menu_item, + /// ])?, + /// ])?; + /// let webview_window = WebviewWindowBuilder::new(app, "editor", WebviewUrl::default()) + /// .menu(menu) + /// .build() + /// .unwrap(); + /// + /// webview_window.on_menu_event(move |window, event| { + /// if event.id == save_menu_item.id() { + /// // save menu item + /// } + /// }); + /// + /// Ok(()) + /// }); + /// ``` + pub fn on_menu_event, crate::menu::MenuEvent) + Send + Sync + 'static>( + &self, + f: F, + ) { + self.window.on_menu_event(f) + } + + /// Returns this window menu. + pub fn menu(&self) -> Option> { + self.window.menu() + } + + /// Sets the window menu and returns the previous one. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Unsupported. The menu on macOS is app-wide and not specific to one + /// window, if you need to set it, use [`AppHandle::set_menu`] instead. + #[cfg_attr(target_os = "macos", allow(unused_variables))] + pub fn set_menu(&self, menu: Menu) -> crate::Result>> { + self.window.set_menu(menu) + } + + /// Removes the window menu and returns it. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Unsupported. The menu on macOS is app-wide and not specific to one + /// window, if you need to remove it, use [`AppHandle::remove_menu`] instead. + pub fn remove_menu(&self) -> crate::Result>> { + self.window.remove_menu() + } + + /// Hides the window menu. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Unsupported. + pub fn hide_menu(&self) -> crate::Result<()> { + self.window.hide_menu() + } + + /// Shows the window menu. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Unsupported. + pub fn show_menu(&self) -> crate::Result<()> { + self.window.show_menu() + } + + /// Shows the window menu. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Unsupported. + pub fn is_menu_visible(&self) -> crate::Result { + self.window.is_menu_visible() + } + + /// Shows the specified menu as a context menu at the cursor position. + pub fn popup_menu(&self, menu: &M) -> crate::Result<()> { + self.window.popup_menu(menu) + } + + /// Shows the specified menu as a context menu at the specified position. + /// + /// The position is relative to the window's top-left corner. + pub fn popup_menu_at>( + &self, + menu: &M, + position: P, + ) -> crate::Result<()> { + self.window.popup_menu_at(menu, position) + } +} + +/// Window getters. +impl WebviewWindow { + /// Returns the scale factor that can be used to map logical pixels to physical pixels, and vice versa. + pub fn scale_factor(&self) -> crate::Result { + self.window.scale_factor() + } + + /// Returns the position of the top-left hand corner of the window's client area relative to the top-left hand corner of the desktop. + pub fn inner_position(&self) -> crate::Result> { + self.window.inner_position() + } + + /// Returns the position of the top-left hand corner of the window relative to the top-left hand corner of the desktop. + pub fn outer_position(&self) -> crate::Result> { + self.window.outer_position() + } + + /// Returns the physical size of the window's client area. + /// + /// The client area is the content of the window, excluding the title bar and borders. + pub fn inner_size(&self) -> crate::Result> { + self.window.inner_size() + } + + /// Returns the physical size of the entire window. + /// + /// These dimensions include the title bar and borders. If you don't want that (and you usually don't), use inner_size instead. + pub fn outer_size(&self) -> crate::Result> { + self.window.outer_size() + } + + /// Gets the window's current fullscreen state. + pub fn is_fullscreen(&self) -> crate::Result { + self.window.is_fullscreen() + } + + /// Gets the window's current minimized state. + pub fn is_minimized(&self) -> crate::Result { + self.window.is_minimized() + } + + /// Gets the window's current maximized state. + pub fn is_maximized(&self) -> crate::Result { + self.window.is_maximized() + } + + /// Gets the window's current focus state. + pub fn is_focused(&self) -> crate::Result { + self.window.is_focused() + } + + /// Gets the window's current decoration state. + pub fn is_decorated(&self) -> crate::Result { + self.window.is_decorated() + } + + /// Gets the window's current resizable state. + pub fn is_resizable(&self) -> crate::Result { + self.window.is_resizable() + } + + /// Whether the window is enabled or disabled. + pub fn is_enabled(&self) -> crate::Result { + self.webview.window().is_enabled() + } + + /// Determines if this window should always be on top of other windows. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + pub fn is_always_on_top(&self) -> crate::Result { + self.webview.window().is_always_on_top() + } + + /// Gets the window's native maximize button state + /// + /// ## Platform-specific + /// + /// - **Linux / iOS / Android:** Unsupported. + pub fn is_maximizable(&self) -> crate::Result { + self.window.is_maximizable() + } + + /// Gets the window's native minimize button state + /// + /// ## Platform-specific + /// + /// - **Linux / iOS / Android:** Unsupported. + pub fn is_minimizable(&self) -> crate::Result { + self.window.is_minimizable() + } + + /// Gets the window's native close button state + /// + /// ## Platform-specific + /// + /// - **Linux / iOS / Android:** Unsupported. + pub fn is_closable(&self) -> crate::Result { + self.window.is_closable() + } + + /// Gets the window's current visibility state. + pub fn is_visible(&self) -> crate::Result { + self.window.is_visible() + } + + /// Gets the window's current title. + pub fn title(&self) -> crate::Result { + self.window.title() + } + + /// Returns the monitor on which the window currently resides. + /// + /// Returns None if current monitor can't be detected. + pub fn current_monitor(&self) -> crate::Result> { + self.window.current_monitor() + } + + /// Returns the primary monitor of the system. + /// + /// Returns None if it can't identify any monitor as a primary one. + pub fn primary_monitor(&self) -> crate::Result> { + self.window.primary_monitor() + } + + /// Returns the monitor that contains the given point. + pub fn monitor_from_point(&self, x: f64, y: f64) -> crate::Result> { + self.window.monitor_from_point(x, y) + } + + /// Returns the list of all the monitors available on the system. + pub fn available_monitors(&self) -> crate::Result> { + self.window.available_monitors() + } + + /// Returns the native handle that is used by this window. + #[cfg(target_os = "macos")] + pub fn ns_window(&self) -> crate::Result<*mut std::ffi::c_void> { + self.window.ns_window() + } + + /// Returns the pointer to the content view of this window. + #[cfg(target_os = "macos")] + pub fn ns_view(&self) -> crate::Result<*mut std::ffi::c_void> { + self.window.ns_view() + } + + /// Returns the native handle that is used by this window. + #[cfg(windows)] + pub fn hwnd(&self) -> crate::Result { + self.window.hwnd() + } + + /// Returns the `ApplicationWindow` from gtk crate that is used by this window. + /// + /// Note that this type can only be used on the main thread. + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + pub fn gtk_window(&self) -> crate::Result { + self.window.gtk_window() + } + + /// Returns the vertical [`gtk::Box`] that is added by default as the sole child of this window. + /// + /// Note that this type can only be used on the main thread. + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + pub fn default_vbox(&self) -> crate::Result { + self.window.default_vbox() + } + + /// Returns the name of the Android activity associated with this window. + #[cfg(target_os = "android")] + pub fn activity_name(&self) -> crate::Result { + self.window.activity_name() + } + + /// Returns the current window theme. + /// + /// ## Platform-specific + /// + /// - **macOS**: Only supported on macOS 10.14+. + pub fn theme(&self) -> crate::Result { + self.window.theme() + } +} + +/// Desktop window getters. +#[cfg(desktop)] +impl WebviewWindow { + /// Get the cursor position relative to the top-left hand corner of the desktop. + /// + /// Note that the top-left hand corner of the desktop is not necessarily the same as the screen. + /// If the user uses a desktop with multiple monitors, + /// the top-left hand corner of the desktop is the top-left hand corner of the main monitor on Windows and macOS + /// or the top-left of the leftmost monitor on X11. + /// + /// The coordinates can be negative if the top-left hand corner of the window is outside of the visible screen region. + pub fn cursor_position(&self) -> crate::Result> { + self.webview.cursor_position() + } +} + +/// Desktop window setters and actions. +#[cfg(desktop)] +impl WebviewWindow { + /// Centers the window. + pub fn center(&self) -> crate::Result<()> { + self.window.center() + } + + /// Requests user attention to the window, this has no effect if the application + /// is already focused. How requesting for user attention manifests is platform dependent, + /// see `UserAttentionType` for details. + /// + /// Providing `None` will unset the request for user attention. Unsetting the request for + /// user attention might not be done automatically by the WM when the window receives input. + /// + /// ## Platform-specific + /// + /// - **macOS:** `None` has no effect. + /// - **Linux:** Urgency levels have the same effect. + pub fn request_user_attention( + &self, + request_type: Option, + ) -> crate::Result<()> { + self.window.request_user_attention(request_type) + } + + /// Determines if this window's native maximize button should be enabled. + /// If resizable is set to false, this setting is ignored. + /// + /// ## Platform-specific + /// + /// - **macOS:** Disables the "zoom" button in the window titlebar, which is also used to enter fullscreen mode. + /// - **Linux / iOS / Android:** Unsupported. + pub fn set_maximizable(&self, maximizable: bool) -> crate::Result<()> { + self.window.set_maximizable(maximizable) + } + + /// Determines if this window's native minimize button should be enabled. + /// + /// ## Platform-specific + /// + /// - **Linux / iOS / Android:** Unsupported. + pub fn set_minimizable(&self, minimizable: bool) -> crate::Result<()> { + self.window.set_minimizable(minimizable) + } + + /// Determines if this window's native close button should be enabled. + /// + /// ## Platform-specific + /// + /// - **Linux:** "GTK+ will do its best to convince the window manager not to show a close button. + /// Depending on the system, this function may not have any effect when called on a window that is already visible" + /// - **iOS / Android:** Unsupported. + pub fn set_closable(&self, closable: bool) -> crate::Result<()> { + self.window.set_closable(closable) + } + + /// Maximizes this window. + pub fn maximize(&self) -> crate::Result<()> { + self.window.maximize() + } + + /// Un-maximizes this window. + pub fn unmaximize(&self) -> crate::Result<()> { + self.window.unmaximize() + } + + /// Minimizes this window. + pub fn minimize(&self) -> crate::Result<()> { + self.window.minimize() + } + + /// Un-minimizes this window. + pub fn unminimize(&self) -> crate::Result<()> { + self.window.unminimize() + } + + /// Determines if this window should be [decorated]. + /// + /// [decorated]: https://en.wikipedia.org/wiki/Window_(computing)#Window_decoration + pub fn set_decorations(&self, decorations: bool) -> crate::Result<()> { + self.window.set_decorations(decorations) + } + + /// Determines if this window should have shadow. + /// + /// ## Platform-specific + /// + /// - **Windows:** + /// - `false` has no effect on decorated window, shadow are always ON. + /// - `true` will make undecorated window have a 1px white border, + /// and on Windows 11, it will have a rounded corners. + /// - **Linux:** Unsupported. + pub fn set_shadow(&self, enable: bool) -> crate::Result<()> { + self.window.set_shadow(enable) + } + + /// Sets window effects, pass [`None`] to clear any effects applied if possible. + /// + /// Requires the window to be transparent. + /// + /// See [`crate::window::EffectsBuilder`] for a convenient builder for [`crate::utils::config::WindowEffectsConfig`]. + /// + /// + /// ```rust,no_run + /// use tauri::{Manager, window::{Color, Effect, EffectState, EffectsBuilder}}; + /// tauri::Builder::default() + /// .setup(|app| { + /// let webview_window = app.get_webview_window("main").unwrap(); + /// webview_window.set_effects( + /// EffectsBuilder::new() + /// .effect(Effect::Popover) + /// .state(EffectState::Active) + /// .radius(5.) + /// .color(Color(0, 0, 0, 255)) + /// .build(), + /// )?; + /// Ok(()) + /// }); + /// ``` + /// + /// ## Platform-specific: + /// + /// - **Windows**: If using decorations or shadows, you may want to try this workaround + /// - **Linux**: Unsupported + pub fn set_effects>>( + &self, + effects: E, + ) -> crate::Result<()> { + self.window.set_effects(effects) + } + + /// Determines if this window should always be below other windows. + pub fn set_always_on_bottom(&self, always_on_bottom: bool) -> crate::Result<()> { + self.window.set_always_on_bottom(always_on_bottom) + } + + /// Determines if this window should always be on top of other windows. + pub fn set_always_on_top(&self, always_on_top: bool) -> crate::Result<()> { + self.window.set_always_on_top(always_on_top) + } + + /// Sets whether the window should be visible on all workspaces or virtual desktops. + pub fn set_visible_on_all_workspaces( + &self, + visible_on_all_workspaces: bool, + ) -> crate::Result<()> { + self + .window + .set_visible_on_all_workspaces(visible_on_all_workspaces) + } + + /// Determines if this window should be fullscreen. + pub fn set_fullscreen(&self, fullscreen: bool) -> crate::Result<()> { + self.window.set_fullscreen(fullscreen) + } + + /// Sets the window as fullscreen on the monitor that contains the given physical position, + /// such as a [`Monitor::position`](crate::Monitor::position). + /// + /// Does nothing if no monitor contains the position. + pub fn set_fullscreen_on_monitor(&self, position: PhysicalPosition) -> crate::Result<()> { + self.window.set_fullscreen_on_monitor(position) + } + + /// Toggles a fullscreen mode that doesn't require a new macOS space. + /// Returns a boolean indicating whether the transition was successful (this won't work if the window was already in the native fullscreen). + /// + /// This is how fullscreen used to work on macOS in versions before Lion. + /// And allows the user to have a fullscreen window without using another space or taking control over the entire monitor. + /// + /// ## Platform-specific + /// + /// - **macOS:** Uses native simple fullscreen mode. + /// - **Other platforms:** Falls back to [`Self::set_fullscreen`]. + pub fn set_simple_fullscreen(&self, enable: bool) -> crate::Result<()> { + self.window.set_simple_fullscreen(enable) + } + + /// Sets this window' icon. + pub fn set_icon(&self, icon: Image<'_>) -> crate::Result<()> { + self.window.set_icon(icon) + } + + /// Whether to hide the window icon from the taskbar or not. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub fn set_skip_taskbar(&self, skip: bool) -> crate::Result<()> { + self.window.set_skip_taskbar(skip) + } + + /// Grabs the cursor, preventing it from leaving the window. + /// + /// There's no guarantee that the cursor will be hidden. You should + /// hide it by yourself if you want so. + /// + /// ## Platform-specific + /// + /// - **Linux:** Unsupported. + /// - **macOS:** This locks the cursor in a fixed location, which looks visually awkward. + pub fn set_cursor_grab(&self, grab: bool) -> crate::Result<()> { + self.window.set_cursor_grab(grab) + } + + /// Modifies the cursor's visibility. + /// + /// If `false`, this will hide the cursor. If `true`, this will show the cursor. + /// + /// ## Platform-specific + /// + /// - **Windows:** The cursor is only hidden within the confines of the window. + /// - **macOS:** The cursor is hidden as long as the window has input focus, even if the cursor is + /// outside of the window. + pub fn set_cursor_visible(&self, visible: bool) -> crate::Result<()> { + self.window.set_cursor_visible(visible) + } + + /// Modifies the cursor icon of the window. + pub fn set_cursor_icon(&self, icon: CursorIcon) -> crate::Result<()> { + self.window.set_cursor_icon(icon) + } + + /// Changes the position of the cursor in window coordinates. + pub fn set_cursor_position>(&self, position: Pos) -> crate::Result<()> { + self.window.set_cursor_position(position) + } + + /// Ignores the window cursor events. + pub fn set_ignore_cursor_events(&self, ignore: bool) -> crate::Result<()> { + self.window.set_ignore_cursor_events(ignore) + } + + /// Starts dragging the window. + pub fn start_dragging(&self) -> crate::Result<()> { + self.window.start_dragging() + } + + /// Sets the overlay icon on the taskbar **Windows only**. Using `None` will remove the icon + /// + /// The overlay icon can be unique for each window. + #[cfg(target_os = "windows")] + #[cfg_attr(docsrs, doc(cfg(target_os = "windows")))] + pub fn set_overlay_icon(&self, icon: Option>) -> crate::Result<()> { + self.window.set_overlay_icon(icon) + } + + /// Sets the taskbar badge count. Using `0` or `None` will remove the badge + /// + /// ## Platform-specific + /// - **Windows:** Unsupported, use [`WebviewWindow::set_overlay_icon`] instead. + /// - **iOS:** iOS expects i32, the value will be clamped to i32::MIN, i32::MAX. + /// - **Android:** Unsupported. + pub fn set_badge_count(&self, count: Option) -> crate::Result<()> { + self.window.set_badge_count(count) + } + + /// Sets the taskbar badge label **macOS only**. Using `None` will remove the badge + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + pub fn set_badge_label(&self, label: Option) -> crate::Result<()> { + self.window.set_badge_label(label) + } + + /// Sets the taskbar progress state. + /// + /// ## Platform-specific + /// + /// - **Linux / macOS**: Progress bar is app-wide and not specific to this window. + /// - **Linux**: Only supported desktop environments with `libunity` (e.g. GNOME). + /// - **iOS / Android:** Unsupported. + pub fn set_progress_bar( + &self, + progress_state: crate::window::ProgressBarState, + ) -> crate::Result<()> { + self.window.set_progress_bar(progress_state) + } + + /// Sets the title bar style. **macOS only**. + pub fn set_title_bar_style(&self, style: tauri_utils::TitleBarStyle) -> crate::Result<()> { + self.window.set_title_bar_style(style) + } +} + +/// Desktop window setters and actions. +impl WebviewWindow { + /// Determines if this window should be resizable. + /// When resizable is set to false, native window's maximize button is automatically disabled. + pub fn set_resizable(&self, resizable: bool) -> crate::Result<()> { + self.window.set_resizable(resizable) + } + + /// Enable or disable the window. + pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> { + self.webview.window().set_enabled(enabled) + } + + /// Set this window's title. + pub fn set_title(&self, title: &str) -> crate::Result<()> { + self.window.set_title(title) + } + + /// Show this window. + pub fn show(&self) -> crate::Result<()> { + self.window.show() + } + + /// Hide this window. + pub fn hide(&self) -> crate::Result<()> { + self.window.hide() + } + + /// Closes this window. It emits [`crate::WindowEvent::CloseRequested`] first like a user-initiated close request so you can intercept it. + pub fn close(&self) -> crate::Result<()> { + self.window.close() + } + + /// Destroys this window. Similar to [`Self::close`] but does not emit any events and force close the window instead. + pub fn destroy(&self) -> crate::Result<()> { + self.window.destroy() + } + + /// Prevents the window contents from being captured by other apps. + pub fn set_content_protected(&self, protected: bool) -> crate::Result<()> { + self.window.set_content_protected(protected) + } + + /// Resizes this window. + pub fn set_size>(&self, size: S) -> crate::Result<()> { + self.window.set_size(size.into()) + } + + /// Sets this window's minimum inner size. + pub fn set_min_size>(&self, size: Option) -> crate::Result<()> { + self.window.set_min_size(size.map(|s| s.into())) + } + + /// Sets this window's maximum inner size. + pub fn set_max_size>(&self, size: Option) -> crate::Result<()> { + self.window.set_max_size(size.map(|s| s.into())) + } + + /// Sets this window's minimum inner width. + pub fn set_size_constraints( + &self, + constraints: tauri_runtime::window::WindowSizeConstraints, + ) -> crate::Result<()> { + self.window.set_size_constraints(constraints) + } + + /// Sets this window's position. + pub fn set_position>(&self, position: Pos) -> crate::Result<()> { + self.window.set_position(position) + } + + /// Bring the window to front and focus. + pub fn set_focus(&self) -> crate::Result<()> { + self.window.set_focus() + } + + /// Sets whether the window can be focused. + /// + /// ## Platform-specific + /// + /// - **macOS**: If the window is already focused, it is not possible to unfocus it after calling `set_focusable(false)`. + /// In this case, you might consider calling [`Window::set_focus`] but it will move the window to the back i.e. at the bottom in terms of z-order. + pub fn set_focusable(&self, focusable: bool) -> crate::Result<()> { + self.window.set_focusable(focusable) + } + + /// Sets the window background color. + /// + /// ## Platform-specific: + /// + /// - **iOS / Android:** Unsupported. + /// - **macOS**: Not implemented for the webview layer.. + /// - **Windows**: + /// - alpha channel is ignored for the window layer. + /// - On Windows 7, transparency is not supported and the alpha value will be ignored for the webview layer.. + /// - On Windows 8 and newer: translucent colors are not supported so any alpha value other than `0` will be replaced by `255` for the webview layer. + pub fn set_background_color(&self, color: Option) -> crate::Result<()> { + self.window.set_background_color(color)?; + self.webview.set_background_color(color) + } + + /// Sets the theme for this window. + /// + /// ## Platform-specific + /// + /// - **Linux / macOS**: Theme is app-wide and not specific to this window. + /// - **iOS / Android:** Unsupported. + pub fn set_theme(&self, theme: Option) -> crate::Result<()> { + self.window.set_theme(theme) + } +} + +/// Desktop webview APIs. +#[cfg(desktop)] +impl WebviewWindow { + /// Opens the dialog to prints the contents of the webview. + /// Currently only supported on macOS on `wry`. + /// `window.print()` works on all platforms. + pub fn print(&self) -> crate::Result<()> { + self.webview.print() + } +} + +/// Webview APIs. +impl WebviewWindow { + /// Executes a closure, providing it with the webview handle that is specific to the current platform. + /// + /// The closure is executed on the main thread. + /// + /// Note that `webview2-com`, `webkit2gtk`, `objc2_web_kit` and similar crates may be updated in minor releases of Tauri. + /// Therefore it's recommended to pin Tauri to at least a minor version when you're using `with_webview`. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri::Manager; + /// + /// fn main() { + /// tauri::Builder::default() + /// .setup(|app| { + /// let main_webview = app.get_webview_window("main").unwrap(); + /// main_webview.with_webview(|webview| { + /// #[cfg(target_os = "linux")] + /// { + /// // see + /// // and + /// use webkit2gtk::WebViewExt; + /// webview.inner().set_zoom_level(4.); + /// } + /// + /// #[cfg(windows)] + /// unsafe { + /// // see + /// webview.controller().SetZoomFactor(4.).unwrap(); + /// } + /// + /// #[cfg(target_os = "macos")] + /// unsafe { + /// let view: &objc2_web_kit::WKWebView = &*webview.inner().cast(); + /// let controller: &objc2_web_kit::WKUserContentController = &*webview.controller().cast(); + /// let window: &objc2_app_kit::NSWindow = &*webview.ns_window().cast(); + /// + /// view.setPageZoom(4.); + /// controller.removeAllUserScripts(); + /// let bg_color = objc2_app_kit::NSColor::colorWithDeviceRed_green_blue_alpha(0.5, 0.2, 0.4, 1.); + /// window.setBackgroundColor(Some(&bg_color)); + /// } + /// + /// #[cfg(target_os = "android")] + /// { + /// use jni::objects::JValue; + /// webview.jni_handle().exec(|env, _, webview| { + /// env.call_method(webview, "zoomBy", "(F)V", &[JValue::Float(4.)]).unwrap(); + /// }) + /// } + /// }); + /// Ok(()) + /// }); + /// } + /// ``` + #[allow(clippy::needless_doctest_main)] // To avoid a large diff + #[cfg(feature = "wry")] + #[cfg_attr(docsrs, doc(cfg(feature = "wry")))] + pub fn with_webview( + &self, + f: F, + ) -> crate::Result<()> { + self.webview.with_webview(f) + } + + /// Returns the current url of the webview. + pub fn url(&self) -> crate::Result { + self.webview.url() + } + + /// Navigates the webview to the defined url. + pub fn navigate(&self, url: Url) -> crate::Result<()> { + self.webview.navigate(url) + } + + /// Reloads the current page. + pub fn reload(&self) -> crate::Result<()> { + self.webview.reload() + } + + /// Converts a file path to a URL that can be loaded by this webview. + /// + /// This is the Rust equivalent of the JavaScript `convertFileSrc` function. + /// + /// The `protocol-asset` Cargo feature must be enabled and the file must be included in the + /// [`app.security.assetProtocol`](https://v2.tauri.app/reference/config/#assetprotocolconfig) + /// scope. The protocol origin must also be allowed by the relevant + /// [`app.security.csp`](https://v2.tauri.app/reference/config/#csp-1) directive, + /// e.g. `img-src 'self' asset: http://asset.localhost`. + /// + /// On Windows and Android the URL is `http://{protocol}.localhost/{path}` + /// (or `https://` if the webview was built with [`WebviewWindowBuilder::use_https_scheme`]); + /// on macOS, Linux and iOS it is `{protocol}://localhost/{path}`. + /// + /// # Arguments + /// + /// * `path` - The file path to convert. + /// * `protocol` - The custom protocol to use. Defaults to `asset`; you only need to set this + /// when using a protocol registered with [`Builder::register_uri_scheme_protocol`](crate::Builder::register_uri_scheme_protocol). + /// + /// # Errors + /// + /// Returns [`Error::NonUtf8Path`](crate::Error::NonUtf8Path) if the path is not valid UTF-8, + /// since the asset protocol could not resolve such a URL back to the file. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri::Manager; + /// tauri::Builder::default() + /// .setup(|app| { + /// let webview = app.get_webview_window("main").unwrap(); + /// let video_path = app.path().app_data_dir()?.join("video.mp4"); + /// let url = webview.convert_file_src(&video_path, None)?; + /// webview.eval(format!("document.querySelector('video').src = '{url}'"))?; + /// Ok(()) + /// }); + /// ``` + pub fn convert_file_src>( + &self, + path: P, + protocol: Option<&str>, + ) -> crate::Result { + self.webview.convert_file_src(path, protocol) + } + + /// Handles this window receiving an [`crate::webview::InvokeRequest`]. + pub fn on_message( + self, + request: crate::webview::InvokeRequest, + responder: Box>, + ) { + self.webview.on_message(request, responder) + } + + /// Evaluates JavaScript on this window. + pub fn eval(&self, js: impl Into) -> crate::Result<()> { + self.webview.eval(js) + } + + /// Evaluate JavaScript with callback function on this webview. + /// The evaluation result will be serialized into a JSON string and passed to the callback function. + /// + /// Exception is ignored because of the limitation on Windows. You can catch it yourself and return as string as a workaround. + pub fn eval_with_callback( + &self, + js: impl Into, + callback: impl Fn(String) + Send + 'static, + ) -> crate::Result<()> { + self.webview.eval_with_callback(js, callback) + } + + /// Opens the developer tools window (Web Inspector). + /// The devtools is only enabled on debug builds or with the `devtools` feature flag. + /// + /// ## Platform-specific + /// + /// - **macOS:** Only supported on macOS 10.15+. + /// This is a private API on macOS, so you cannot use this if your application will be published on the App Store. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri::Manager; + /// tauri::Builder::default() + /// .setup(|app| { + /// #[cfg(debug_assertions)] + /// app.get_webview_window("main").unwrap().open_devtools(); + /// Ok(()) + /// }); + /// ``` + #[cfg(any(debug_assertions, feature = "devtools"))] + #[cfg_attr(docsrs, doc(cfg(any(debug_assertions, feature = "devtools"))))] + pub fn open_devtools(&self) { + self.webview.open_devtools(); + } + + /// Closes the developer tools window (Web Inspector). + /// The devtools is only enabled on debug builds or with the `devtools` feature flag. + /// + /// ## Platform-specific + /// + /// - **macOS:** Only supported on macOS 10.15+. + /// This is a private API on macOS, so you cannot use this if your application will be published on the App Store. + /// - **Windows:** Unsupported. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri::Manager; + /// tauri::Builder::default() + /// .setup(|app| { + /// #[cfg(debug_assertions)] + /// { + /// let webview = app.get_webview_window("main").unwrap(); + /// webview.open_devtools(); + /// std::thread::spawn(move || { + /// std::thread::sleep(std::time::Duration::from_secs(10)); + /// webview.close_devtools(); + /// }); + /// } + /// Ok(()) + /// }); + /// ``` + #[cfg(any(debug_assertions, feature = "devtools"))] + #[cfg_attr(docsrs, doc(cfg(any(debug_assertions, feature = "devtools"))))] + pub fn close_devtools(&self) { + self.webview.close_devtools(); + } + + /// Checks if the developer tools window (Web Inspector) is opened. + /// The devtools is only enabled on debug builds or with the `devtools` feature flag. + /// + /// ## Platform-specific + /// + /// - **macOS:** Only supported on macOS 10.15+. + /// This is a private API on macOS, so you cannot use this if your application will be published on the App Store. + /// - **Windows:** Unsupported. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri::Manager; + /// tauri::Builder::default() + /// .setup(|app| { + /// #[cfg(debug_assertions)] + /// { + /// let webview = app.get_webview_window("main").unwrap(); + /// if !webview.is_devtools_open() { + /// webview.open_devtools(); + /// } + /// } + /// Ok(()) + /// }); + /// ``` + #[cfg(any(debug_assertions, feature = "devtools"))] + #[cfg_attr(docsrs, doc(cfg(any(debug_assertions, feature = "devtools"))))] + pub fn is_devtools_open(&self) -> bool { + self.webview.is_devtools_open() + } + + /// Set the webview zoom level + /// + /// ## Platform-specific: + /// + /// - **Android**: Not supported. + /// - **macOS**: available on macOS 11+ only. + /// - **iOS**: available on iOS 14+ only. + pub fn set_zoom(&self, scale_factor: f64) -> crate::Result<()> { + self.webview.set_zoom(scale_factor) + } + + /// Clear all browsing data for this webview window. + pub fn clear_all_browsing_data(&self) -> crate::Result<()> { + self.webview.clear_all_browsing_data() + } + + /// Returns all cookies in the runtime's cookie store including HTTP-only and secure cookies. + /// + /// Note that cookies will only be returned for URLs with an http or https scheme. + /// Cookies set through javascript for local files + /// (such as those served from the tauri://) protocol are not currently supported. + /// + /// # Stability + /// + /// See [Self::cookies]. + /// + /// # Known issues + /// + /// See [Self::cookies]. + pub fn cookies_for_url(&self, url: Url) -> crate::Result>> { + self.webview.cookies_for_url(url) + } + + /// Returns all cookies in the runtime's cookie store for all URLs including HTTP-only and secure cookies. + /// + /// Note that cookies will only be returned for URLs with an http or https scheme. + /// Cookies set through javascript for local files + /// (such as those served from the tauri://) protocol are not currently supported. + /// + /// # Stability + /// + /// The return value of this function leverages [`tauri_runtime::Cookie`] which re-exports the cookie crate. + /// This dependency might receive updates in minor Tauri releases. + /// + /// # Known issues + /// + /// On Windows, this function deadlocks when used in a synchronous command or event handlers, see [the Webview2 issue]. + /// You should use `async` commands and separate threads when reading cookies. + /// + /// ## Platform-specific + /// + /// - **Android**: Unsupported, always returns an empty [`Vec`]. + /// + /// [the Webview2 issue]: https://github.com/tauri-apps/wry/issues/583 + pub fn cookies(&self) -> crate::Result>> { + self.webview.cookies() + } + + /// Set a cookie for the webview. + /// + /// # Stability + /// + /// See [Self::cookies]. + pub fn set_cookie(&self, cookie: Cookie<'_>) -> crate::Result<()> { + self.webview.set_cookie(cookie) + } + + /// Delete a cookie for the webview. + /// + /// # Stability + /// + /// See [Self::cookies]. + pub fn delete_cookie(&self, cookie: Cookie<'_>) -> crate::Result<()> { + self.webview.delete_cookie(cookie) + } +} + +impl Listener for WebviewWindow { + /// Listen to an event on this webview window. + /// + /// # Examples + /// + /// ``` + /// use tauri::{Manager, Listener}; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let webview_window = app.get_webview_window("main").unwrap(); + /// webview_window.listen("component-loaded", move |event| { + /// println!("window just loaded a component"); + /// }); + /// + /// Ok(()) + /// }); + /// ``` + fn listen(&self, event: impl Into, handler: F) -> EventId + where + F: Fn(Event) + Send + 'static, + { + let event = EventName::new(event.into()).unwrap(); + self.manager().listen( + event, + EventTarget::WebviewWindow { + label: self.label().to_string(), + }, + handler, + ) + } + + /// Listen to an event on this window webview only once. + /// + /// See [`Self::listen`] for more information. + fn once(&self, event: impl Into, handler: F) -> EventId + where + F: FnOnce(Event) + Send + 'static, + { + let event = EventName::new(event.into()).unwrap(); + self.manager().once( + event, + EventTarget::WebviewWindow { + label: self.label().to_string(), + }, + handler, + ) + } + + /// Unlisten to an event on this webview window. + /// + /// # Examples + /// ``` + /// use tauri::{Manager, Listener}; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let webview_window = app.get_webview_window("main").unwrap(); + /// let webview_window_ = webview_window.clone(); + /// let handler = webview_window.listen("component-loaded", move |event| { + /// println!("webview_window just loaded a component"); + /// + /// // we no longer need to listen to the event + /// // we also could have used `webview_window.once` instead + /// webview_window_.unlisten(event.id()); + /// }); + /// + /// // stop listening to the event when you do not need it anymore + /// webview_window.unlisten(handler); + /// + /// Ok(()) + /// }); + /// ``` + fn unlisten(&self, id: EventId) { + self.manager().unlisten(id) + } +} + +impl Emitter for WebviewWindow {} + +impl Manager for WebviewWindow { + fn resources_table(&self) -> MutexGuard<'_, ResourceTable> { + self + .webview + .resources_table + .lock() + .expect("poisoned window resources table") + } +} + +impl ManagerBase for WebviewWindow { + fn manager(&self) -> &AppManager { + self.webview.manager() + } + + fn manager_owned(&self) -> Arc> { + self.webview.manager_owned() + } + + fn runtime(&self) -> RuntimeOrDispatch<'_, R> { + self.window.runtime() + } + + fn managed_app_handle(&self) -> &AppHandle { + self.webview.managed_app_handle() + } + + #[cfg(target_os = "android")] + fn activity_name(&self) -> Option> { + Some(self.window.activity_name()) + } + + #[cfg(target_os = "ios")] + fn scene_identifier(&self) -> Option> { + Some(self.window.scene_identifier()) + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/window/mod.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/window/mod.rs new file mode 100644 index 000000000..56715440f --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/window/mod.rs @@ -0,0 +1,2620 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! The Tauri window types and functions. + +pub(crate) mod plugin; + +use tauri_runtime::{ + dpi::{PhysicalPosition, PhysicalRect, PhysicalSize}, + webview::PendingWebview, +}; +pub use tauri_utils::{WindowEffect as Effect, WindowEffectState as EffectState, config::Color}; + +#[cfg(desktop)] +pub use crate::runtime::ProgressBarStatus; + +#[cfg(desktop)] +use crate::{ + CursorIcon, + image::Image, + menu::{ContextMenu, Menu, MenuId}, + runtime::UserAttentionType, +}; +use crate::{ + Emitter, EventLoopMessage, EventName, Listener, Manager, ResourceTable, Runtime, Theme, Webview, + WindowEvent, + app::AppHandle, + event::{Event, EventId, EventTarget}, + ipc::{CommandArg, CommandItem, InvokeError}, + manager::{AppManager, EmitPayload}, + runtime::{ + RuntimeHandle, WindowDispatch, + dpi::{Position, Size}, + monitor::Monitor as RuntimeMonitor, + window::{DetachedWindow, PendingWindow, WindowBuilder as _}, + }, + sealed::{ManagerBase, RuntimeOrDispatch}, + utils::config::{WindowConfig, WindowEffectsConfig}, + webview::WebviewBuilder, +}; + +use serde::Serialize; +#[cfg(windows)] +use windows::Win32::Foundation::HWND; + +use tauri_macros::default_runtime; + +use std::{ + fmt, + hash::{Hash, Hasher}, + sync::{Arc, Mutex, MutexGuard}, +}; + +/// Monitor descriptor. +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Monitor { + pub(crate) name: Option, + pub(crate) size: PhysicalSize, + pub(crate) position: PhysicalPosition, + pub(crate) work_area: PhysicalRect, + pub(crate) scale_factor: f64, +} + +impl From for Monitor { + fn from(monitor: RuntimeMonitor) -> Self { + Self { + name: monitor.name, + size: monitor.size, + position: monitor.position, + work_area: monitor.work_area, + scale_factor: monitor.scale_factor, + } + } +} + +impl Monitor { + /// Returns a human-readable name of the monitor. + /// Returns None if the monitor doesn't exist anymore. + pub fn name(&self) -> Option<&String> { + self.name.as_ref() + } + + /// Returns the monitor's resolution. + pub fn size(&self) -> &PhysicalSize { + &self.size + } + + /// Returns the top-left corner position of the monitor relative to the larger full screen area. + pub fn position(&self) -> &PhysicalPosition { + &self.position + } + + /// Returns the monitor's work_area. + pub fn work_area(&self) -> &PhysicalRect { + &self.work_area + } + + /// Returns the scale factor that can be used to map logical pixels to physical pixels, and vice versa. + pub fn scale_factor(&self) -> f64 { + self.scale_factor + } +} + +macro_rules! unstable_struct { + (#[doc = $doc:expr] $($tokens:tt)*) => { + #[cfg(feature = "unstable")] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] + #[doc = $doc] + pub $($tokens)* + + #[cfg(not(feature = "unstable"))] + pub(crate) $($tokens)* + } +} + +unstable_struct!( + #[doc = "A builder for a window managed by Tauri."] + struct WindowBuilder<'a, R: Runtime, M: Manager> { + manager: &'a M, + pub(crate) label: String, + pub(crate) window_builder: + >::WindowBuilder, + #[cfg(desktop)] + pub(crate) menu: Option>, + #[cfg(desktop)] + on_menu_event: Option>>, + window_effects: Option, + #[cfg(target_os = "android")] + created_by_activity_name_set: bool, + #[cfg(target_os = "ios")] + requested_by_scene_identifier_set: bool, + } +); + +impl> fmt::Debug for WindowBuilder<'_, R, M> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("WindowBuilder") + .field("label", &self.label) + .field("window_builder", &self.window_builder) + .finish() + } +} + +#[cfg_attr(not(feature = "unstable"), allow(dead_code))] +impl<'a, R: Runtime, M: Manager> WindowBuilder<'a, R, M> { + /// Initializes a window builder with the given window label. + /// + /// # Known issues + /// + /// On Windows, this function deadlocks when used in a synchronous command or event handlers, see [the Webview2 issue]. + /// You should use `async` commands and separate threads when creating windows. + /// + /// # Examples + /// + /// - Create a window in the setup hook: + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +tauri::Builder::default() + .setup(|app| { + let window = tauri::window::WindowBuilder::new(app, "label") + .build()?; + Ok(()) + }); +``` + "#### + )] + /// - Create a window in a separate thread: + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +tauri::Builder::default() + .setup(|app| { + let handle = app.handle().clone(); + std::thread::spawn(move || { + let window = tauri::window::WindowBuilder::new(&handle, "label") + .build() + .unwrap(); + }); + Ok(()) + }); +``` + "#### + )] + /// + /// - Create a window in a command: + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +#[tauri::command] +async fn create_window(app: tauri::AppHandle) { + let window = tauri::window::WindowBuilder::new(&app, "label") + .build() + .unwrap(); +} +``` + "#### + )] + /// + /// [the Webview2 issue]: https://github.com/tauri-apps/wry/issues/583 + pub fn new>(manager: &'a M, label: L) -> Self { + Self { + manager, + label: label.into(), + window_builder: >::WindowBuilder::new( + ), + #[cfg(desktop)] + menu: None, + #[cfg(desktop)] + on_menu_event: None, + window_effects: None, + #[cfg(target_os = "android")] + created_by_activity_name_set: false, + #[cfg(target_os = "ios")] + requested_by_scene_identifier_set: false, + } + } + + /// Initializes a window builder from a [`WindowConfig`] from tauri.conf.json. + /// Keep in mind that you can't create 2 windows with the same `label` so make sure + /// that the initial window was closed or change the label of the new [`WindowBuilder`]. + /// + /// # Known issues + /// + /// On Windows, this function deadlocks when used in a synchronous command or event handlers, see [the Webview2 issue]. + /// You should use `async` commands and separate threads when creating windows. + /// + /// # Examples + /// + /// - Create a window in a command: + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +#[tauri::command] +async fn reopen_window(app: tauri::AppHandle) { + let window = tauri::window::WindowBuilder::from_config(&app, &app.config().app.windows.get(0).unwrap().clone()) + .unwrap() + .build() + .unwrap(); +} +``` + "#### + )] + /// + /// [the Webview2 issue]: https://github.com/tauri-apps/wry/issues/583 + pub fn from_config(manager: &'a M, config: &WindowConfig) -> crate::Result { + #[cfg_attr(not(windows), allow(unused_mut))] + let mut builder = Self { + #[cfg(target_os = "android")] + created_by_activity_name_set: config.created_by_activity_name.is_some(), + #[cfg(target_os = "ios")] + requested_by_scene_identifier_set: config.requested_by_scene_identifier.is_some(), + manager, + label: config.label.clone(), + window_effects: config.window_effects.clone(), + window_builder: + >::WindowBuilder::with_config( + config, + ), + #[cfg(desktop)] + menu: None, + #[cfg(desktop)] + on_menu_event: None, + }; + + #[cfg(desktop)] + if let Some(parent) = &config.parent { + let window = manager + .manager() + .get_window(parent) + .ok_or(crate::Error::WindowNotFound)?; + builder = builder.parent(&window)?; + } + + Ok(builder) + } + + /// Registers a global menu event listener. + /// + /// Note that this handler is called for any menu event, + /// whether it is coming from this window, another window or from the tray icon menu. + /// + /// Also note that this handler will not be called if + /// the window used to register it was closed. + /// + /// # Examples + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +use tauri::menu::{Menu, Submenu, MenuItem}; +tauri::Builder::default() + .setup(|app| { + let handle = app.handle(); + let save_menu_item = MenuItem::new(handle, "Save", true, None::<&str>)?; + let menu = Menu::with_items(handle, &[ + &Submenu::with_items(handle, "File", true, &[ + &save_menu_item, + ])?, + ])?; + let window = tauri::window::WindowBuilder::new(app, "editor") + .menu(menu) + .on_menu_event(move |window, event| { + if event.id == save_menu_item.id() { + // save menu item + } + }) + .build() + .unwrap(); + /// + Ok(()) + }); +```"#### + )] + #[cfg(desktop)] + pub fn on_menu_event, crate::menu::MenuEvent) + Send + Sync + 'static>( + mut self, + f: F, + ) -> Self { + self.on_menu_event.replace(Box::new(f)); + self + } + + /// Creates this window with a webview with it. + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "webview::create", skip_all) + )] + pub(crate) fn with_webview( + self, + webview: WebviewBuilder, + ) -> crate::Result<(Window, Webview)> { + let pending_webview = webview.into_pending_webview(self.manager, &self.label)?; + let window = self.build_internal(Some(pending_webview))?; + + let webview = window.webviews().first().unwrap().clone(); + + Ok((window, webview)) + } + + /// Creates a new window. + pub fn build(self) -> crate::Result> { + self.build_internal(None) + } + + /// Creates a new window with an optional webview. + fn build_internal( + // mutable on mobile + #[allow(unused_mut)] mut self, + webview: Option>, + ) -> crate::Result> { + #[cfg(desktop)] + let theme = self.window_builder.get_theme(); + + #[cfg(target_os = "android")] + if !self.created_by_activity_name_set { + if let Some(manager_window_activity_name) = self.manager.activity_name() { + self.window_builder = self + .window_builder + .created_by_activity_name(manager_window_activity_name?); + } + } + + #[cfg(target_os = "ios")] + if !self.requested_by_scene_identifier_set { + if let Some(manager_window_scene_identifier) = self.manager.scene_identifier() { + self.window_builder = self + .window_builder + .requested_by_scene_identifier(manager_window_scene_identifier?); + } + } + + let mut pending = PendingWindow::new(self.window_builder, self.label)?; + if let Some(webview) = webview { + pending.set_webview(webview); + } + + let app_manager = self.manager.manager(); + + let pending = app_manager.window.prepare_window(pending)?; + + #[cfg(desktop)] + let window_menu = { + let is_app_wide = self.menu.is_none(); + self + .menu + .or_else(|| self.manager.app_handle().menu()) + .map(|menu| WindowMenu { is_app_wide, menu }) + }; + + #[cfg(desktop)] + let handler = app_manager + .menu + .prepare_window_menu_creation_handler(window_menu.as_ref(), theme); + #[cfg(not(desktop))] + #[allow(clippy::type_complexity)] + let handler: Option) + Send>> = None; + + let window = match &mut self.manager.runtime() { + RuntimeOrDispatch::Runtime(runtime) => runtime.create_window(pending, handler), + RuntimeOrDispatch::RuntimeHandle(handle) => handle.create_window(pending, handler), + RuntimeOrDispatch::Dispatch(dispatcher) => dispatcher.create_window(pending, handler), + } + .map(|detached_window| { + let window = app_manager.window.attach_window( + self.manager.app_handle().clone(), + detached_window.clone(), + #[cfg(desktop)] + window_menu, + ); + + if let Some(webview) = detached_window.webview { + app_manager.webview.attach_webview( + window.clone(), + webview.webview, + webview.use_https_scheme, + ); + } + + window + })?; + + #[cfg(desktop)] + if let Some(handler) = self.on_menu_event { + window.on_menu_event(handler); + } + + let app_manager = self.manager.manager_owned(); + let window_label = window.label().to_string(); + let window_ = window.clone(); + // run on the main thread to fix a deadlock on webview.eval if the tracing feature is enabled + let _ = window.run_on_main_thread(move || { + if let Some(effects) = self.window_effects { + _ = crate::vibrancy::set_window_effects(&window_, Some(effects)); + } + let event = crate::EventName::from_str("tauri://window-created"); + let payload = Some(crate::webview::CreatedEvent { + label: window_label, + }); + let _ = app_manager.emit(event, EmitPayload::Serialize(&payload)); + }); + + Ok(window) + } +} + +/// Desktop APIs +#[cfg(desktop)] +#[cfg_attr(not(feature = "unstable"), allow(dead_code))] +impl<'a, R: Runtime, M: Manager> WindowBuilder<'a, R, M> { + /// Sets the menu for the window. + #[must_use] + pub fn menu(mut self, menu: Menu) -> Self { + self.menu.replace(menu); + self + } + + /// Show window in the center of the screen. + #[must_use] + pub fn center(mut self) -> Self { + self.window_builder = self.window_builder.center(); + self + } + + /// Prevent the window from overflowing the working area (e.g. monitor size - taskbar size) + /// on creation, which means the window size will be limited to `monitor size - taskbar size` + /// + /// **NOTE**: The overflow check is only performed on window creation, resizes can still overflow + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[must_use] + pub fn prevent_overflow(mut self) -> Self { + self.window_builder = self.window_builder.prevent_overflow(); + self + } + + /// Prevent the window from overflowing the working area (e.g. monitor size - taskbar size) + /// on creation with a margin, which means the window size will be limited to `monitor size - taskbar size - margin size` + /// + /// **NOTE**: The overflow check is only performed on window creation, resizes can still overflow + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[must_use] + pub fn prevent_overflow_with_margin(mut self, margin: impl Into) -> Self { + self.window_builder = self + .window_builder + .prevent_overflow_with_margin(margin.into()); + self + } + + /// Whether the window's native maximize button is enabled or not. + /// If resizable is set to false, this setting is ignored. + /// + /// ## Platform-specific + /// + /// - **macOS:** Disables the "zoom" button in the window titlebar, which is also used to enter fullscreen mode. + /// - **Linux / iOS / Android:** Unsupported. + #[must_use] + pub fn maximizable(mut self, maximizable: bool) -> Self { + self.window_builder = self.window_builder.maximizable(maximizable); + self + } + + /// Whether the window's native minimize button is enabled or not. + /// + /// ## Platform-specific + /// + /// - **Linux / iOS / Android:** Unsupported. + #[must_use] + pub fn minimizable(mut self, minimizable: bool) -> Self { + self.window_builder = self.window_builder.minimizable(minimizable); + self + } + + /// Whether the window's native close button is enabled or not. + /// + /// ## Platform-specific + /// + /// - **Linux:** "GTK+ will do its best to convince the window manager not to show a close button. + /// Depending on the system, this function may not have any effect when called on a window that is already visible" + /// - **iOS / Android:** Unsupported. + #[must_use] + pub fn closable(mut self, closable: bool) -> Self { + self.window_builder = self.window_builder.closable(closable); + self + } + + /// Whether to start the window in fullscreen or not. + #[must_use] + pub fn fullscreen(mut self, fullscreen: bool) -> Self { + self.window_builder = self.window_builder.fullscreen(fullscreen); + self + } + + /// Whether the window should be maximized upon creation. + #[must_use] + pub fn maximized(mut self, maximized: bool) -> Self { + self.window_builder = self.window_builder.maximized(maximized); + self + } + + /// Whether the window should have borders and bars. + #[must_use] + pub fn decorations(mut self, decorations: bool) -> Self { + self.window_builder = self.window_builder.decorations(decorations); + self + } + + /// Whether the window should always be below other windows. + #[must_use] + pub fn always_on_bottom(mut self, always_on_bottom: bool) -> Self { + self.window_builder = self.window_builder.always_on_bottom(always_on_bottom); + self + } + + /// Whether the window should always be on top of other windows. + #[must_use] + pub fn always_on_top(mut self, always_on_top: bool) -> Self { + self.window_builder = self.window_builder.always_on_top(always_on_top); + self + } + + /// Whether the window will be visible on all workspaces or virtual desktops. + /// + /// ## Platform-specific + /// + /// - **Windows / iOS / Android:** Unsupported. + #[must_use] + pub fn visible_on_all_workspaces(mut self, visible_on_all_workspaces: bool) -> Self { + self.window_builder = self + .window_builder + .visible_on_all_workspaces(visible_on_all_workspaces); + self + } + + /// Sets the window icon. + pub fn icon(mut self, icon: Image<'a>) -> crate::Result { + self.window_builder = self.window_builder.icon(icon.into())?; + Ok(self) + } + + /// Sets whether or not the window icon should be hidden from the taskbar. + /// + /// ## Platform-specific + /// + /// - **macOS**: Unsupported. + #[must_use] + pub fn skip_taskbar(mut self, skip: bool) -> Self { + self.window_builder = self.window_builder.skip_taskbar(skip); + self + } + + /// Sets custom name for Windows' window class. **Windows only**. + #[must_use] + pub fn window_classname>(mut self, classname: S) -> Self { + self.window_builder = self.window_builder.window_classname(classname); + self + } + + /// This sets `WS_EX_NOREDIRECTIONBITMAP`. + /// + /// This can avoid the white flash that may appear before the webview content is rendered + /// when using a transparent window. **Windows only**. + #[must_use] + pub fn no_redirection_bitmap(mut self, enable: bool) -> Self { + self.window_builder = self.window_builder.no_redirection_bitmap(enable); + self + } + + /// Sets whether or not the window has shadow. + /// + /// ## Platform-specific + /// + /// - **Windows:** + /// - `false` has no effect on decorated window, shadows are always ON. + /// - `true` will make undecorated window have a 1px white border, + /// and on Windows 11, it will have a rounded corners. + /// - **Linux:** Unsupported. + #[must_use] + pub fn shadow(mut self, enable: bool) -> Self { + self.window_builder = self.window_builder.shadow(enable); + self + } + + /// Sets a parent to the window to be created. + /// + /// ## Platform-specific + /// + /// - **Windows**: This sets the passed parent as an owner window to the window to be created. + /// From [MSDN owned windows docs](https://docs.microsoft.com/en-us/windows/win32/winmsg/window-features#owned-windows): + /// - An owned window is always above its owner in the z-order. + /// - The system automatically destroys an owned window when its owner is destroyed. + /// - An owned window is hidden when its owner is minimized. + /// - **Linux**: This makes the new window transient for parent, see + /// - **macOS**: This adds the window as a child of parent, see + pub fn parent(mut self, parent: &Window) -> crate::Result { + #[cfg(windows)] + { + self.window_builder = self.window_builder.owner(parent.hwnd()?); + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + { + self.window_builder = self.window_builder.transient_for(&parent.gtk_window()?); + } + + #[cfg(target_os = "macos")] + { + self.window_builder = self.window_builder.parent(parent.ns_window()?); + } + + Ok(self) + } + + /// Set an owner to the window to be created. + /// + /// From MSDN: + /// - An owned window is always above its owner in the z-order. + /// - The system automatically destroys an owned window when its owner is destroyed. + /// - An owned window is hidden when its owner is minimized. + /// + /// For more information, see + #[cfg(windows)] + pub fn owner(mut self, owner: &Window) -> crate::Result { + self.window_builder = self.window_builder.owner(owner.hwnd()?); + Ok(self) + } + + /// Set an owner to the window to be created. + /// + /// From MSDN: + /// - An owned window is always above its owner in the z-order. + /// - The system automatically destroys an owned window when its owner is destroyed. + /// - An owned window is hidden when its owner is minimized. + /// + /// For more information, see + /// + /// **Note:** This is a low level API. See [`Self::parent`] for a higher level wrapper for Tauri windows. + #[cfg(windows)] + #[must_use] + pub fn owner_raw(mut self, owner: HWND) -> Self { + self.window_builder = self.window_builder.owner(owner); + self + } + + /// Sets a parent to the window to be created. + /// + /// A child window has the WS_CHILD style and is confined to the client area of its parent window. + /// + /// For more information, see + /// + /// **Note:** This is a low level API. See [`Self::parent`] for a higher level wrapper for Tauri windows. + #[cfg(windows)] + #[must_use] + pub fn parent_raw(mut self, parent: HWND) -> Self { + self.window_builder = self.window_builder.parent(parent); + self + } + + /// Sets a parent to the window to be created. + /// + /// See + /// + /// **Note:** This is a low level API. See [`Self::parent`] for a higher level wrapper for Tauri windows. + #[cfg(target_os = "macos")] + #[must_use] + pub fn parent_raw(mut self, parent: *mut std::ffi::c_void) -> Self { + self.window_builder = self.window_builder.parent(parent); + self + } + + /// Sets the window to be created transient for parent. + /// + /// See + /// + /// **Note:** This is a low level API. See [`Self::parent`] for a higher level wrapper for Tauri windows. + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + pub fn transient_for(mut self, parent: &Window) -> crate::Result { + self.window_builder = self.window_builder.transient_for(&parent.gtk_window()?); + Ok(self) + } + + /// Sets the window to be created transient for parent. + /// + /// See + /// + /// **Note:** This is a low level API. See [`Self::parent`] and [`Self::transient_for`] for higher level wrappers for Tauri windows. + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + #[must_use] + pub fn transient_for_raw(mut self, parent: &impl gtk::glib::IsA) -> Self { + self.window_builder = self.window_builder.transient_for(parent); + self + } + + /// Enables or disables drag and drop support. + #[cfg(windows)] + #[must_use] + pub fn drag_and_drop(mut self, enabled: bool) -> Self { + self.window_builder = self.window_builder.drag_and_drop(enabled); + self + } + + /// Sets the [`crate::TitleBarStyle`]. + #[cfg(target_os = "macos")] + #[must_use] + pub fn title_bar_style(mut self, style: crate::TitleBarStyle) -> Self { + self.window_builder = self.window_builder.title_bar_style(style); + self + } + + /// Hide the window title. + #[cfg(target_os = "macos")] + #[must_use] + pub fn hidden_title(mut self, hidden: bool) -> Self { + self.window_builder = self.window_builder.hidden_title(hidden); + self + } + + /// Defines the window [tabbing identifier] for macOS. + /// + /// Windows with matching tabbing identifiers will be grouped together. + /// If the tabbing identifier is not set, automatic tabbing will be disabled. + /// + /// [tabbing identifier]: + #[cfg(target_os = "macos")] + #[must_use] + pub fn tabbing_identifier(mut self, identifier: &str) -> Self { + self.window_builder = self.window_builder.tabbing_identifier(identifier); + self + } + + /// Sets window effects. + /// + /// Requires the window to be transparent. + /// + /// ## Platform-specific: + /// + /// - **Windows**: If using decorations or shadows, you may want to try this workaround + /// - **Linux**: Unsupported + pub fn effects(mut self, effects: WindowEffectsConfig) -> Self { + self.window_effects.replace(effects); + self + } +} + +/// Window APIs. +#[cfg_attr(not(feature = "unstable"), allow(dead_code))] +impl<'a, R: Runtime, M: Manager> WindowBuilder<'a, R, M> { + /// The initial position of the window in logical pixels. + #[must_use] + pub fn position(mut self, x: f64, y: f64) -> Self { + self.window_builder = self.window_builder.position(x, y); + self + } + + /// Window size in logical pixels. + #[must_use] + pub fn inner_size(mut self, width: f64, height: f64) -> Self { + self.window_builder = self.window_builder.inner_size(width, height); + self + } + + /// Window min inner size in logical pixels. + #[must_use] + pub fn min_inner_size(mut self, min_width: f64, min_height: f64) -> Self { + self.window_builder = self.window_builder.min_inner_size(min_width, min_height); + self + } + + /// Window max inner size in logical pixels. + #[must_use] + pub fn max_inner_size(mut self, max_width: f64, max_height: f64) -> Self { + self.window_builder = self.window_builder.max_inner_size(max_width, max_height); + self + } + + /// Window inner size constraints. + #[must_use] + pub fn inner_size_constraints( + mut self, + constraints: tauri_runtime::window::WindowSizeConstraints, + ) -> Self { + self.window_builder = self.window_builder.inner_size_constraints(constraints); + self + } + + /// Whether the window is resizable or not. + /// When resizable is set to false, native window's maximize button is automatically disabled. + #[must_use] + pub fn resizable(mut self, resizable: bool) -> Self { + self.window_builder = self.window_builder.resizable(resizable); + self + } + + /// The title of the window in the title bar. + #[must_use] + pub fn title>(mut self, title: S) -> Self { + self.window_builder = self.window_builder.title(title); + self + } + + /// Sets the window to be initially focused. + #[must_use] + #[deprecated( + since = "1.2.0", + note = "The window is automatically focused by default. This function Will be removed in 3.0.0. Use `focused` instead." + )] + pub fn focus(mut self) -> Self { + self.window_builder = self.window_builder.focused(true); + self + } + + /// Whether the window will be initially focused or not. + #[must_use] + pub fn focused(mut self, focused: bool) -> Self { + self.window_builder = self.window_builder.focused(focused); + self + } + + /// Whether the window will be focusable or not. + #[must_use] + pub fn focusable(mut self, focusable: bool) -> Self { + self.window_builder = self.window_builder.focusable(focusable); + self + } + + /// Whether the window should be immediately visible upon creation. + #[must_use] + pub fn visible(mut self, visible: bool) -> Self { + self.window_builder = self.window_builder.visible(visible); + self + } + + /// Forces a theme or uses the system settings if None was provided. + /// + /// ## Platform-specific + /// + /// - **macOS**: Only supported on macOS 10.14+. + #[must_use] + pub fn theme(mut self, theme: Option) -> Self { + self.window_builder = self.window_builder.theme(theme); + self + } + + /// Whether the window should be transparent. If this is true, writing colors + /// with alpha values different than `1.0` will produce a transparent window. + /// + /// On Windows, using `no_redirection_bitmap` can help avoid a white flash when + /// creating a transparent window. + #[must_use] + pub fn transparent(mut self, transparent: bool) -> Self { + self.window_builder = self.window_builder.transparent(transparent); + self + } + + /// Prevents the window contents from being captured by other apps. + #[must_use] + pub fn content_protected(mut self, protected: bool) -> Self { + self.window_builder = self.window_builder.content_protected(protected); + self + } + + /// Set the window and webview background color. + /// + /// ## Platform-specific: + /// + /// - **Windows**: alpha channel is ignored. + #[must_use] + pub fn background_color(mut self, color: Color) -> Self { + self.window_builder = self.window_builder.background_color(color); + self + } +} + +#[cfg(target_os = "android")] +impl> WindowBuilder<'_, R, M> { + /// The name of the activity to create for this webview window. + pub fn activity_name>(mut self, class_name: S) -> Self { + self.window_builder = self.window_builder.activity_name(class_name); + self + } + + /// Sets the name of the activity that is creating this webview window. + /// + /// This is important to determine which stack the activity will belong to. + pub fn created_by_activity_name>(mut self, class_name: S) -> Self { + self.created_by_activity_name_set = true; + self.window_builder = self.window_builder.created_by_activity_name(class_name); + self + } +} + +/// iOS specific APIs +#[cfg(target_os = "ios")] +impl> WindowBuilder<'_, R, M> { + /// Sets the identifier of the scene that is requesting the new scene, + /// establishing a relationship between the two scenes. + /// + /// By default the system uses the foreground scene. + #[cfg(target_os = "ios")] + pub fn requested_by_scene_identifier(mut self, identifier: String) -> Self { + self.requested_by_scene_identifier_set = true; + self.window_builder = self + .window_builder + .requested_by_scene_identifier(identifier); + self + } +} + +/// A wrapper struct to hold the window menu state +/// and whether it is global per-app or specific to this window. +#[cfg(desktop)] +pub(crate) struct WindowMenu { + pub(crate) is_app_wide: bool, + pub(crate) menu: Menu, +} + +// TODO: expand these docs since this is a pretty important type +/// A window managed by Tauri. +/// +/// This type also implements [`Manager`] which allows you to manage other windows attached to +/// the same application. +#[default_runtime(crate::Wry, wry)] +pub struct Window { + /// The window created by the runtime. + pub(crate) window: DetachedWindow, + /// The manager to associate this window with. + pub(crate) manager: Arc>, + pub(crate) app_handle: AppHandle, + // The menu set for this window + #[cfg(desktop)] + pub(crate) menu: Arc>>>, + pub(crate) resources_table: Arc>, +} + +impl std::fmt::Debug for Window { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Window") + .field("window", &self.window) + .field("manager", &self.manager) + .field("app_handle", &self.app_handle) + .finish() + } +} + +impl raw_window_handle::HasWindowHandle for Window { + fn window_handle( + &self, + ) -> std::result::Result, raw_window_handle::HandleError> { + self.window.dispatcher.window_handle() + } +} + +impl raw_window_handle::HasDisplayHandle for Window { + fn display_handle( + &self, + ) -> std::result::Result, raw_window_handle::HandleError> { + self.app_handle.display_handle() + } +} + +impl Clone for Window { + fn clone(&self) -> Self { + Self { + window: self.window.clone(), + manager: self.manager.clone(), + app_handle: self.app_handle.clone(), + #[cfg(desktop)] + menu: self.menu.clone(), + resources_table: self.resources_table.clone(), + } + } +} + +impl Hash for Window { + /// Only use the [`Window`]'s label to represent its hash. + fn hash(&self, state: &mut H) { + self.window.label.hash(state) + } +} + +impl Eq for Window {} +impl PartialEq for Window { + /// Only use the [`Window`]'s label to compare equality. + fn eq(&self, other: &Self) -> bool { + self.window.label.eq(&other.window.label) + } +} + +impl Manager for Window { + fn resources_table(&self) -> MutexGuard<'_, ResourceTable> { + self + .resources_table + .lock() + .expect("poisoned window resources table") + } +} + +impl ManagerBase for Window { + fn manager(&self) -> &AppManager { + &self.manager + } + + fn manager_owned(&self) -> Arc> { + self.manager.clone() + } + + fn runtime(&self) -> RuntimeOrDispatch<'_, R> { + RuntimeOrDispatch::Dispatch(self.window.dispatcher.clone()) + } + + fn managed_app_handle(&self) -> &AppHandle { + &self.app_handle + } + + #[cfg(target_os = "android")] + fn activity_name(&self) -> Option> { + Some(self.activity_name()) + } + + #[cfg(target_os = "ios")] + fn scene_identifier(&self) -> Option> { + Some(self.scene_identifier()) + } +} + +impl<'de, R: Runtime> CommandArg<'de, R> for Window { + /// Grabs the [`Window`] from the [`CommandItem`]. This will never fail. + fn from_command(command: CommandItem<'de, R>) -> Result { + Ok(command.message.webview().window()) + } +} + +/// Base window functions. +impl Window { + /// Create a new window that is attached to the manager. + pub(crate) fn new( + manager: Arc>, + window: DetachedWindow, + app_handle: AppHandle, + #[cfg(desktop)] menu: Option>, + ) -> Self { + Self { + window, + manager, + app_handle, + #[cfg(desktop)] + menu: Arc::new(std::sync::Mutex::new(menu)), + resources_table: Default::default(), + } + } + + /// Initializes a window builder with the given window label. + /// + /// This creates a window without any webview attached to it; + /// use [`Window::add_child`] to add webviews to it. + /// To create a window with a single webview filling it, use + /// [`WebviewWindowBuilder`](crate::webview::WebviewWindowBuilder) instead. + /// + /// Data URLs are only supported with the `webview-data-url` feature flag. + /// + /// This function requires the `unstable` Cargo feature. + /// + /// # Known issues + /// + /// On Windows, this function deadlocks when used in a synchronous command or event handlers, + /// see [the Webview2 issue]. You should use `async` commands and separate threads when creating windows. + /// + /// # Examples + /// + /// ``` + /// tauri::Builder::default() + /// .setup(|app| { + /// let window = tauri::Window::builder(app, "label") + /// .title("Tauri") + /// .inner_size(800., 600.) + /// .build()?; + /// Ok(()) + /// }); + /// ``` + /// + /// [the Webview2 issue]: https://github.com/tauri-apps/wry/issues/583 + #[cfg(feature = "unstable")] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] + pub fn builder, L: Into>(manager: &M, label: L) -> WindowBuilder<'_, R, M> { + WindowBuilder::new(manager, label.into()) + } + + /// Adds a new webview as a child of this window. + /// + /// This is the multiwebview API: a single window can host any number of webviews, + /// each of them positioned and sized by you. It requires the `unstable` Cargo feature + /// and is only available on desktop. + /// + /// The webview is not resized or moved when the parent window is resized unless + /// auto resize is enabled, either at build time with + /// [`WebviewBuilder::auto_resize`](crate::webview::WebviewBuilder::auto_resize) + /// or later with [`Webview::set_auto_resize`]. + /// + /// # Arguments + /// + /// * `webview_builder` - the [`WebviewBuilder`] defining the webview to create. + /// * `position` - position of the webview's top-left corner, relative to the top-left corner of the window's + /// client area. Any type that converts into [`Position`] is accepted, so you can use either logical + /// ([`LogicalPosition`](crate::LogicalPosition)) or physical ([`PhysicalPosition`]) units. + /// * `size` - the webview size, also either logical ([`LogicalSize`](crate::LogicalSize)) or physical + /// ([`PhysicalSize`]) - any type that converts into [`Size`]. + /// + /// # Known issues + /// + /// On Windows, this function deadlocks when used in a synchronous command or event handlers, + /// see [the Webview2 issue]. You should use `async` commands and separate threads when creating webviews. + /// + /// # Examples + /// + /// Splitting a window between two webviews that resize with it: + /// + /// ``` + /// use tauri::{LogicalPosition, LogicalSize, WebviewUrl}; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let width = 800.; + /// let height = 600.; + /// + /// let window = tauri::Window::builder(app, "main") + /// .inner_size(width, height) + /// .build()?; + /// + /// let _left = window.add_child( + /// tauri::webview::WebviewBuilder::new("left", WebviewUrl::App(Default::default())) + /// .auto_resize(), + /// LogicalPosition::new(0., 0.), + /// LogicalSize::new(width / 2., height), + /// )?; + /// + /// let _right = window.add_child( + /// tauri::webview::WebviewBuilder::new( + /// "right", + /// WebviewUrl::External("https://tauri.app".parse().unwrap()), + /// ) + /// .auto_resize(), + /// LogicalPosition::new(width / 2., 0.), + /// LogicalSize::new(width / 2., height), + /// )?; + /// + /// Ok(()) + /// }); + /// ``` + /// + /// [the Webview2 issue]: https://github.com/tauri-apps/wry/issues/583 + #[cfg(any(test, all(desktop, feature = "unstable")))] + #[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "unstable"))))] + pub fn add_child, S: Into>( + &self, + webview_builder: WebviewBuilder, + position: P, + size: S, + ) -> crate::Result> { + use std::sync::mpsc::channel; + + let (tx, rx) = channel(); + let position = position.into(); + let size = size.into(); + let window_ = self.clone(); + self.run_on_main_thread(move || { + let res = webview_builder.build(window_, position, size); + tx.send(res).unwrap(); + })?; + rx.recv().unwrap() + } + + /// List of webviews associated with this window. + pub fn webviews(&self) -> Vec> { + self + .manager + .webview + .webviews_lock() + .values() + .filter(|w| w.window_label() == self.label()) + .cloned() + .collect() + } + + pub(crate) fn is_webview_window(&self) -> bool { + self.webviews().iter().all(|w| w.label() == self.label()) + } + + /// Runs the given closure on the main thread. + pub fn run_on_main_thread(&self, f: F) -> crate::Result<()> { + self + .window + .dispatcher + .run_on_main_thread(f) + .map_err(Into::into) + } + + /// The label of this window. + pub fn label(&self) -> &str { + &self.window.label + } + + /// Registers a window event listener. + pub fn on_window_event(&self, f: F) { + self + .window + .dispatcher + .on_window_event(move |event| f(&event.clone().into())); + } +} + +/// Menu APIs +#[cfg(desktop)] +impl Window { + /// Registers a global menu event listener. + /// + /// Note that this handler is called for any menu event, + /// whether it is coming from this window, another window or from the tray icon menu. + /// + /// Also note that this handler will not be called if + /// the window used to register it was closed. + /// + /// # Examples + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +use tauri::menu::{Menu, Submenu, MenuItem}; +tauri::Builder::default() + .setup(|app| { + let handle = app.handle(); + let save_menu_item = MenuItem::new(handle, "Save", true, None::<&str>)?; + let menu = Menu::with_items(handle, &[ + &Submenu::with_items(handle, "File", true, &[ + &save_menu_item, + ])?, + ])?; + let window = tauri::window::WindowBuilder::new(app, "editor") + .menu(menu) + .build() + .unwrap(); + + window.on_menu_event(move |window, event| { + if event.id == save_menu_item.id() { + // save menu item + } + }); + + Ok(()) + }); +``` + "#### + )] + pub fn on_menu_event, crate::menu::MenuEvent) + Send + Sync + 'static>( + &self, + f: F, + ) { + self + .manager + .menu + .event_listeners + .lock() + .unwrap() + .insert(self.label().to_string(), Box::new(f)); + } + + pub(crate) fn menu_lock(&self) -> std::sync::MutexGuard<'_, Option>> { + self.menu.lock().expect("poisoned window") + } + + #[cfg_attr(target_os = "macos", allow(dead_code))] + pub(crate) fn has_app_wide_menu(&self) -> bool { + self + .menu_lock() + .as_ref() + .map(|m| m.is_app_wide) + .unwrap_or(false) + } + + #[cfg_attr(target_os = "macos", allow(dead_code))] + pub(crate) fn is_menu_in_use>(&self, id: &I) -> bool { + self + .menu_lock() + .as_ref() + .map(|m| id.eq(m.menu.id())) + .unwrap_or(false) + } + + /// Returns this window menu . + pub fn menu(&self) -> Option> { + self.menu_lock().as_ref().map(|m| m.menu.clone()) + } + + /// Sets the window menu and returns the previous one. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Unsupported. The menu on macOS is app-wide and not specific to one + /// window, if you need to set it, use [`AppHandle::set_menu`] instead. + #[cfg_attr(target_os = "macos", allow(unused_variables))] + pub fn set_menu(&self, menu: Menu) -> crate::Result>> { + let prev_menu = self.remove_menu()?; + + self.manager.menu.insert_menu_into_stash(&menu); + + let window = self.clone(); + let menu_ = menu.clone(); + self.run_on_main_thread(move || { + #[cfg(windows)] + if let Ok(hwnd) = window.hwnd() { + let theme = window + .theme() + .map(crate::menu::map_to_menu_theme) + .unwrap_or(muda::MenuTheme::Auto); + + let _ = unsafe { menu_.inner().init_for_hwnd_with_theme(hwnd.0 as _, theme) }; + } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + if let (Ok(gtk_window), Ok(gtk_box)) = (window.gtk_window(), window.default_vbox()) { + let _ = menu_ + .inner() + .init_for_gtk_window(>k_window, Some(>k_box)); + } + })?; + + self.menu_lock().replace(WindowMenu { + is_app_wide: false, + menu, + }); + + Ok(prev_menu) + } + + /// Removes the window menu and returns it. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Unsupported. The menu on macOS is app-wide and not specific to one + /// window, if you need to remove it, use [`AppHandle::remove_menu`] instead. + pub fn remove_menu(&self) -> crate::Result>> { + let prev_menu = self.menu_lock().take().map(|m| m.menu); + + // remove from the window + #[cfg(not(target_os = "macos"))] + if let Some(menu) = &prev_menu { + let window = self.clone(); + let menu = menu.clone(); + self.run_on_main_thread(move || { + #[cfg(windows)] + if let Ok(hwnd) = window.hwnd() { + let _ = unsafe { menu.inner().remove_for_hwnd(hwnd.0 as _) }; + } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + if let Ok(gtk_window) = window.gtk_window() { + let _ = menu.inner().remove_for_gtk_window(>k_window); + } + })?; + } + + self + .manager + .remove_menu_from_stash_by_id(prev_menu.as_ref().map(|m| m.id())); + + Ok(prev_menu) + } + + /// Hides the window menu. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Unsupported. + pub fn hide_menu(&self) -> crate::Result<()> { + // remove from the window + #[cfg(not(target_os = "macos"))] + if let Some(window_menu) = &*self.menu_lock() { + let window = self.clone(); + let menu_ = window_menu.menu.clone(); + self.run_on_main_thread(move || { + #[cfg(windows)] + if let Ok(hwnd) = window.hwnd() { + let _ = unsafe { menu_.inner().hide_for_hwnd(hwnd.0 as _) }; + } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + if let Ok(gtk_window) = window.gtk_window() { + let _ = menu_.inner().hide_for_gtk_window(>k_window); + } + })?; + } + + Ok(()) + } + + /// Shows the window menu. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Unsupported. + pub fn show_menu(&self) -> crate::Result<()> { + // remove from the window + #[cfg(not(target_os = "macos"))] + if let Some(window_menu) = &*self.menu_lock() { + let window = self.clone(); + let menu_ = window_menu.menu.clone(); + self.run_on_main_thread(move || { + #[cfg(windows)] + if let Ok(hwnd) = window.hwnd() { + let _ = unsafe { menu_.inner().show_for_hwnd(hwnd.0 as _) }; + } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + if let Ok(gtk_window) = window.gtk_window() { + let _ = menu_.inner().show_for_gtk_window(>k_window); + } + })?; + } + + Ok(()) + } + + /// Shows the window menu. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Unsupported. + pub fn is_menu_visible(&self) -> crate::Result { + // remove from the window + #[cfg(not(target_os = "macos"))] + if let Some(window_menu) = &*self.menu_lock() { + let (tx, rx) = std::sync::mpsc::channel(); + let window = self.clone(); + let menu_ = window_menu.menu.clone(); + self.run_on_main_thread(move || { + #[cfg(windows)] + if let Ok(hwnd) = window.hwnd() { + let _ = tx.send(unsafe { menu_.inner().is_visible_on_hwnd(hwnd.0 as _) }); + } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + if let Ok(gtk_window) = window.gtk_window() { + let _ = tx.send(menu_.inner().is_visible_on_gtk_window(>k_window)); + } + })?; + + return Ok(rx.recv().unwrap_or(false)); + } + + Ok(false) + } + + /// Shows the specified menu as a context menu at the cursor position. + pub fn popup_menu(&self, menu: &M) -> crate::Result<()> { + menu.popup(self.clone()) + } + + /// Shows the specified menu as a context menu at the specified position. + /// + /// The position is relative to the window's top-left corner. + pub fn popup_menu_at>( + &self, + menu: &M, + position: P, + ) -> crate::Result<()> { + menu.popup_at(self.clone(), position) + } +} + +/// Window getters. +impl Window { + /// Returns the scale factor that can be used to map logical pixels to physical pixels, and vice versa. + pub fn scale_factor(&self) -> crate::Result { + self.window.dispatcher.scale_factor().map_err(Into::into) + } + + /// Returns the position of the top-left hand corner of the window's client area relative to the top-left hand corner of the desktop. + pub fn inner_position(&self) -> crate::Result> { + self.window.dispatcher.inner_position().map_err(Into::into) + } + + /// Returns the position of the top-left hand corner of the window relative to the top-left hand corner of the desktop. + pub fn outer_position(&self) -> crate::Result> { + self.window.dispatcher.outer_position().map_err(Into::into) + } + + /// Returns the physical size of the window's client area. + /// + /// The client area is the content of the window, excluding the title bar and borders. + pub fn inner_size(&self) -> crate::Result> { + self.window.dispatcher.inner_size().map_err(Into::into) + } + + /// Returns the physical size of the entire window. + /// + /// These dimensions include the title bar and borders. If you don't want that (and you usually don't), use inner_size instead. + pub fn outer_size(&self) -> crate::Result> { + self.window.dispatcher.outer_size().map_err(Into::into) + } + + /// Gets the window's current fullscreen state. + pub fn is_fullscreen(&self) -> crate::Result { + self.window.dispatcher.is_fullscreen().map_err(Into::into) + } + + /// Gets the window's current minimized state. + pub fn is_minimized(&self) -> crate::Result { + self.window.dispatcher.is_minimized().map_err(Into::into) + } + + /// Gets the window's current maximized state. + pub fn is_maximized(&self) -> crate::Result { + self.window.dispatcher.is_maximized().map_err(Into::into) + } + + /// Gets the window's current focus state. + pub fn is_focused(&self) -> crate::Result { + self.window.dispatcher.is_focused().map_err(Into::into) + } + + /// Gets the window's current decoration state. + pub fn is_decorated(&self) -> crate::Result { + self.window.dispatcher.is_decorated().map_err(Into::into) + } + + /// Gets the window's current resizable state. + pub fn is_resizable(&self) -> crate::Result { + self.window.dispatcher.is_resizable().map_err(Into::into) + } + + /// Whether the window is enabled or disabled. + pub fn is_enabled(&self) -> crate::Result { + self.window.dispatcher.is_enabled().map_err(Into::into) + } + + /// Determines if this window should always be on top of other windows. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + pub fn is_always_on_top(&self) -> crate::Result { + self + .window + .dispatcher + .is_always_on_top() + .map_err(Into::into) + } + + /// Gets the window's native maximize button state + /// + /// ## Platform-specific + /// + /// - **Linux / iOS / Android:** Unsupported. + pub fn is_maximizable(&self) -> crate::Result { + self.window.dispatcher.is_maximizable().map_err(Into::into) + } + + /// Gets the window's native minimize button state + /// + /// ## Platform-specific + /// + /// - **Linux / iOS / Android:** Unsupported. + pub fn is_minimizable(&self) -> crate::Result { + self.window.dispatcher.is_minimizable().map_err(Into::into) + } + + /// Gets the window's native close button state + /// + /// ## Platform-specific + /// + /// - **Linux / iOS / Android:** Unsupported. + pub fn is_closable(&self) -> crate::Result { + self.window.dispatcher.is_closable().map_err(Into::into) + } + + /// Gets the window's current visibility state. + pub fn is_visible(&self) -> crate::Result { + self.window.dispatcher.is_visible().map_err(Into::into) + } + + /// Gets the window's current title. + pub fn title(&self) -> crate::Result { + self.window.dispatcher.title().map_err(Into::into) + } + + /// Returns the monitor on which the window currently resides. + /// + /// Returns None if current monitor can't be detected. + pub fn current_monitor(&self) -> crate::Result> { + self + .window + .dispatcher + .current_monitor() + .map(|m| m.map(Into::into)) + .map_err(Into::into) + } + + /// Returns the monitor that contains the given point. + pub fn monitor_from_point(&self, x: f64, y: f64) -> crate::Result> { + self + .window + .dispatcher + .monitor_from_point(x, y) + .map(|m| m.map(Into::into)) + .map_err(Into::into) + } + + /// Returns the primary monitor of the system. + /// + /// Returns None if it can't identify any monitor as a primary one. + pub fn primary_monitor(&self) -> crate::Result> { + self + .window + .dispatcher + .primary_monitor() + .map(|m| m.map(Into::into)) + .map_err(Into::into) + } + + /// Returns the list of all the monitors available on the system. + pub fn available_monitors(&self) -> crate::Result> { + self + .window + .dispatcher + .available_monitors() + .map(|m| m.into_iter().map(Into::into).collect()) + .map_err(Into::into) + } + + /// Returns the native handle that is used by this window. + #[cfg(target_os = "macos")] + pub fn ns_window(&self) -> crate::Result<*mut std::ffi::c_void> { + self + .window + .dispatcher + .window_handle() + .map_err(Into::into) + .and_then(|handle| { + if let raw_window_handle::RawWindowHandle::AppKit(h) = handle.as_raw() { + let view: &objc2_app_kit::NSView = unsafe { h.ns_view.cast().as_ref() }; + let ns_window = view.window().expect("view to be installed in window"); + Ok(objc2::rc::Retained::autorelease_ptr(ns_window).cast()) + } else { + Err(crate::Error::InvalidWindowHandle) + } + }) + } + + /// Returns the pointer to the content view of this window. + #[cfg(target_os = "macos")] + pub fn ns_view(&self) -> crate::Result<*mut std::ffi::c_void> { + self + .window + .dispatcher + .window_handle() + .map_err(Into::into) + .and_then(|handle| { + if let raw_window_handle::RawWindowHandle::AppKit(h) = handle.as_raw() { + Ok(h.ns_view.as_ptr()) + } else { + Err(crate::Error::InvalidWindowHandle) + } + }) + } + + /// Returns the native handle that is used by this window. + #[cfg(windows)] + pub fn hwnd(&self) -> crate::Result { + self + .window + .dispatcher + .window_handle() + .map_err(Into::into) + .and_then(|handle| { + if let raw_window_handle::RawWindowHandle::Win32(h) = handle.as_raw() { + Ok(HWND(h.hwnd.get() as _)) + } else { + Err(crate::Error::InvalidWindowHandle) + } + }) + } + + /// Returns the `ApplicationWindow` from gtk crate that is used by this window. + /// + /// Note that this type can only be used on the main thread. + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + pub fn gtk_window(&self) -> crate::Result { + self.window.dispatcher.gtk_window().map_err(Into::into) + } + + /// Returns the vertical [`gtk::Box`] that is added by default as the sole child of this window. + /// + /// Note that this type can only be used on the main thread. + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + pub fn default_vbox(&self) -> crate::Result { + self.window.dispatcher.default_vbox().map_err(Into::into) + } + + /// Returns the name of the Android activity associated with this window. + #[cfg(target_os = "android")] + pub fn activity_name(&self) -> crate::Result { + self.window.dispatcher.activity_name().map_err(Into::into) + } + + /// Returns the identifier of the UIScene tied to this window. + #[cfg(target_os = "ios")] + pub fn scene_identifier(&self) -> crate::Result { + self + .window + .dispatcher + .scene_identifier() + .map_err(Into::into) + } + + /// Returns the current window theme. + /// + /// ## Platform-specific + /// + /// - **macOS**: Only supported on macOS 10.14+. + pub fn theme(&self) -> crate::Result { + self.window.dispatcher.theme().map_err(Into::into) + } +} + +/// Desktop window getters. +#[cfg(desktop)] +impl Window { + /// Get the cursor position relative to the top-left hand corner of the desktop. + /// + /// Note that the top-left hand corner of the desktop is not necessarily the same as the screen. + /// If the user uses a desktop with multiple monitors, + /// the top-left hand corner of the desktop is the top-left hand corner of the main monitor on Windows and macOS + /// or the top-left of the leftmost monitor on X11. + /// + /// The coordinates can be negative if the top-left hand corner of the window is outside of the visible screen region. + pub fn cursor_position(&self) -> crate::Result> { + self.app_handle.cursor_position() + } +} + +/// Window setters and actions. +impl Window { + /// Determines if this window should be resizable. + /// When resizable is set to false, native window's maximize button is automatically disabled. + pub fn set_resizable(&self, resizable: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_resizable(resizable) + .map_err(Into::into) + } + + /// Set this window's title. + pub fn set_title(&self, title: &str) -> crate::Result<()> { + self + .window + .dispatcher + .set_title(title.to_string()) + .map_err(Into::into) + } + + /// Enable or disable the window. + pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_enabled(enabled) + .map_err(Into::into) + } + + /// Show this window. + pub fn show(&self) -> crate::Result<()> { + self.window.dispatcher.show().map_err(Into::into) + } + + /// Hide this window. + pub fn hide(&self) -> crate::Result<()> { + self.window.dispatcher.hide().map_err(Into::into) + } + + /// Closes this window. It emits [`crate::WindowEvent::CloseRequested`] first like a user-initiated close request so you can intercept it. + pub fn close(&self) -> crate::Result<()> { + self.window.dispatcher.close().map_err(Into::into) + } + + /// Destroys this window. Similar to [`Self::close`] but does not emit any events and force close the window instead. + pub fn destroy(&self) -> crate::Result<()> { + self.window.dispatcher.destroy().map_err(Into::into) + } + + /// Sets the window background color. + /// + /// ## Platform-specific: + /// + /// - **Windows:** alpha channel is ignored. + /// - **iOS / Android:** Unsupported. + pub fn set_background_color(&self, color: Option) -> crate::Result<()> { + self + .window + .dispatcher + .set_background_color(color) + .map_err(Into::into) + } + + /// Prevents the window contents from being captured by other apps. + pub fn set_content_protected(&self, protected: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_content_protected(protected) + .map_err(Into::into) + } + + /// Resizes this window. + pub fn set_size>(&self, size: S) -> crate::Result<()> { + self + .window + .dispatcher + .set_size(size.into()) + .map_err(Into::into) + } + + /// Sets this window's minimum inner size. + pub fn set_min_size>(&self, size: Option) -> crate::Result<()> { + self + .window + .dispatcher + .set_min_size(size.map(|s| s.into())) + .map_err(Into::into) + } + + /// Sets this window's maximum inner size. + pub fn set_max_size>(&self, size: Option) -> crate::Result<()> { + self + .window + .dispatcher + .set_max_size(size.map(|s| s.into())) + .map_err(Into::into) + } + + /// Sets this window's minimum inner width. + pub fn set_size_constraints( + &self, + constraints: tauri_runtime::window::WindowSizeConstraints, + ) -> crate::Result<()> { + self + .window + .dispatcher + .set_size_constraints(constraints) + .map_err(Into::into) + } + + /// Sets this window's position. + pub fn set_position>(&self, position: Pos) -> crate::Result<()> { + self + .window + .dispatcher + .set_position(position.into()) + .map_err(Into::into) + } + + /// Bring the window to front and focus. + pub fn set_focus(&self) -> crate::Result<()> { + self.window.dispatcher.set_focus().map_err(Into::into) + } + + /// Sets whether the window can be focused. + /// + /// ## Platform-specific + /// + /// - **macOS**: If the window is already focused, it is not possible to unfocus it after calling `set_focusable(false)`. + /// In this case, you might consider calling [`Window::set_focus`] but it will move the window to the back i.e. at the bottom in terms of z-order. + pub fn set_focusable(&self, focusable: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_focusable(focusable) + .map_err(Into::into) + } + + /// Sets the theme for this window. + /// + /// ## Platform-specific + /// + /// - **Linux / macOS**: Theme is app-wide and not specific to this window. + /// - **iOS / Android:** Unsupported. + pub fn set_theme(&self, theme: Option) -> crate::Result<()> { + self + .window + .dispatcher + .set_theme(theme) + .map_err(Into::::into)?; + #[cfg(windows)] + if let (Some(menu), Ok(hwnd)) = (self.menu(), self.hwnd()) { + let raw_hwnd = hwnd.0 as isize; + self.run_on_main_thread(move || { + let _ = unsafe { + menu.inner().set_theme_for_hwnd( + raw_hwnd, + theme + .map(crate::menu::map_to_menu_theme) + .unwrap_or(muda::MenuTheme::Auto), + ) + }; + })?; + }; + Ok(()) + } +} + +/// Desktop window setters and actions. +#[cfg(desktop)] +impl Window { + /// Centers the window. + pub fn center(&self) -> crate::Result<()> { + self.window.dispatcher.center().map_err(Into::into) + } + + /// Requests user attention to the window, this has no effect if the application + /// is already focused. How requesting for user attention manifests is platform dependent, + /// see `UserAttentionType` for details. + /// + /// Providing `None` will unset the request for user attention. Unsetting the request for + /// user attention might not be done automatically by the WM when the window receives input. + /// + /// ## Platform-specific + /// + /// - **macOS:** `None` has no effect. + /// - **Linux:** Urgency levels have the same effect. + pub fn request_user_attention( + &self, + request_type: Option, + ) -> crate::Result<()> { + self + .window + .dispatcher + .request_user_attention(request_type) + .map_err(Into::into) + } + + /// Determines if this window's native maximize button should be enabled. + /// If resizable is set to false, this setting is ignored. + /// + /// ## Platform-specific + /// + /// - **macOS:** Disables the "zoom" button in the window titlebar, which is also used to enter fullscreen mode. + /// - **Linux / iOS / Android:** Unsupported. + pub fn set_maximizable(&self, maximizable: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_maximizable(maximizable) + .map_err(Into::into) + } + + /// Determines if this window's native minimize button should be enabled. + /// + /// ## Platform-specific + /// + /// - **Linux / iOS / Android:** Unsupported. + pub fn set_minimizable(&self, minimizable: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_minimizable(minimizable) + .map_err(Into::into) + } + + /// Determines if this window's native close button should be enabled. + /// + /// ## Platform-specific + /// + /// - **Linux:** "GTK+ will do its best to convince the window manager not to show a close button. + /// Depending on the system, this function may not have any effect when called on a window that is already visible" + /// - **iOS / Android:** Unsupported. + pub fn set_closable(&self, closable: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_closable(closable) + .map_err(Into::into) + } + + /// Maximizes this window. + pub fn maximize(&self) -> crate::Result<()> { + self.window.dispatcher.maximize().map_err(Into::into) + } + + /// Un-maximizes this window. + pub fn unmaximize(&self) -> crate::Result<()> { + self.window.dispatcher.unmaximize().map_err(Into::into) + } + + /// Minimizes this window. + pub fn minimize(&self) -> crate::Result<()> { + self.window.dispatcher.minimize().map_err(Into::into) + } + + /// Un-minimizes this window. + pub fn unminimize(&self) -> crate::Result<()> { + self.window.dispatcher.unminimize().map_err(Into::into) + } + + /// Determines if this window should be [decorated]. + /// + /// [decorated]: https://en.wikipedia.org/wiki/Window_(computing)#Window_decoration + pub fn set_decorations(&self, decorations: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_decorations(decorations) + .map_err(Into::into) + } + + /// Determines if this window should have shadow. + /// + /// ## Platform-specific + /// + /// - **Windows:** + /// - `false` has no effect on decorated window, shadow are always ON. + /// - `true` will make undecorated window have a 1px white border, + /// and on Windows 11, it will have a rounded corners. + /// - **Linux:** Unsupported. + pub fn set_shadow(&self, enable: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_shadow(enable) + .map_err(Into::into) + } + + /// Sets window effects, pass [`None`] to clear any effects applied if possible. + /// + /// Requires the window to be transparent. + /// + /// See [`EffectsBuilder`] for a convenient builder for [`WindowEffectsConfig`]. + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust,no_run +use tauri::{Manager, window::{Color, Effect, EffectState, EffectsBuilder}}; +tauri::Builder::default() + .setup(|app| { + let window = app.get_window("main").unwrap(); + window.set_effects( + EffectsBuilder::new() + .effect(Effect::Popover) + .state(EffectState::Active) + .radius(5.) + .color(Color(0, 0, 0, 255)) + .build(), + )?; + Ok(()) + }); +``` + "#### + )] + /// + /// ## Platform-specific: + /// + /// - **Windows**: If using decorations or shadows, you may want to try this workaround + /// - **Linux**: Unsupported + pub fn set_effects>>(&self, effects: E) -> crate::Result<()> { + let effects = effects.into(); + let window = self.clone(); + self.run_on_main_thread(move || { + let _ = crate::vibrancy::set_window_effects(&window, effects); + }) + } + + /// Determines if this window should always be below other windows. + pub fn set_always_on_bottom(&self, always_on_bottom: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_always_on_bottom(always_on_bottom) + .map_err(Into::into) + } + + /// Determines if this window should always be on top of other windows. + pub fn set_always_on_top(&self, always_on_top: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_always_on_top(always_on_top) + .map_err(Into::into) + } + + /// Sets whether the window should be visible on all workspaces or virtual desktops. + /// + /// ## Platform-specific + /// + /// - **Windows / iOS / Android:** Unsupported. + pub fn set_visible_on_all_workspaces( + &self, + visible_on_all_workspaces: bool, + ) -> crate::Result<()> { + self + .window + .dispatcher + .set_visible_on_all_workspaces(visible_on_all_workspaces) + .map_err(Into::into) + } + + /// Determines if this window should be fullscreen. + pub fn set_fullscreen(&self, fullscreen: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_fullscreen(fullscreen) + .map_err(Into::into) + } + + /// Sets the window as fullscreen on the monitor that contains the given physical position, + /// such as a [`Monitor::position`](crate::Monitor::position). + /// + /// Does nothing if no monitor contains the position. + /// + /// # Examples + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust,no_run +use tauri::Manager; +tauri::Builder::default() + .setup(|app| { + let window = app.get_window("main").unwrap(); + if let Some(monitor) = window.available_monitors()?.into_iter().nth(1) { + let position = monitor.position(); + window.set_fullscreen_on_monitor(tauri::PhysicalPosition::new( + position.x as f64, + position.y as f64, + ))?; + } + Ok(()) + }); +``` + "#### + )] + pub fn set_fullscreen_on_monitor(&self, position: PhysicalPosition) -> crate::Result<()> { + self + .window + .dispatcher + .set_fullscreen_on_monitor(position) + .map_err(Into::into) + } + + /// Toggles a fullscreen mode that doesn't require a new macOS space. + /// Returns a boolean indicating whether the transition was successful (this won't work if the window was already in the native fullscreen). + /// + /// This is how fullscreen used to work on macOS in versions before Lion. + /// And allows the user to have a fullscreen window without using another space or taking control over the entire monitor. + /// + /// ## Platform-specific + /// + /// - **macOS:** Uses native simple fullscreen mode. + /// - **Other platforms:** Falls back to [`Self::set_fullscreen`]. + pub fn set_simple_fullscreen(&self, enable: bool) -> crate::Result<()> { + #[cfg(target_os = "macos")] + { + self + .window + .dispatcher + .set_simple_fullscreen(enable) + .map_err(Into::into) + } + #[cfg(not(target_os = "macos"))] + self.set_fullscreen(enable) + } + + /// Sets this window' icon. + pub fn set_icon(&self, icon: Image<'_>) -> crate::Result<()> { + self + .window + .dispatcher + .set_icon(icon.into()) + .map_err(Into::into) + } + + /// Whether to hide the window icon from the taskbar or not. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub fn set_skip_taskbar(&self, skip: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_skip_taskbar(skip) + .map_err(Into::into) + } + + /// Grabs the cursor, preventing it from leaving the window. + /// + /// There's no guarantee that the cursor will be hidden. You should + /// hide it by yourself if you want so. + /// + /// ## Platform-specific + /// + /// - **Linux:** Unsupported. + /// - **macOS:** This locks the cursor in a fixed location, which looks visually awkward. + pub fn set_cursor_grab(&self, grab: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_cursor_grab(grab) + .map_err(Into::into) + } + + /// Modifies the cursor's visibility. + /// + /// If `false`, this will hide the cursor. If `true`, this will show the cursor. + /// + /// ## Platform-specific + /// + /// - **Windows:** The cursor is only hidden within the confines of the window. + /// - **macOS:** The cursor is hidden as long as the window has input focus, even if the cursor is + /// outside of the window. + pub fn set_cursor_visible(&self, visible: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_cursor_visible(visible) + .map_err(Into::into) + } + + /// Modifies the cursor icon of the window. + pub fn set_cursor_icon(&self, icon: CursorIcon) -> crate::Result<()> { + self + .window + .dispatcher + .set_cursor_icon(icon) + .map_err(Into::into) + } + + /// Changes the position of the cursor in window coordinates. + pub fn set_cursor_position>(&self, position: Pos) -> crate::Result<()> { + self + .window + .dispatcher + .set_cursor_position(position) + .map_err(Into::into) + } + + /// Ignores the window cursor events. + pub fn set_ignore_cursor_events(&self, ignore: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_ignore_cursor_events(ignore) + .map_err(Into::into) + } + + /// Starts dragging the window. + pub fn start_dragging(&self) -> crate::Result<()> { + self.window.dispatcher.start_dragging().map_err(Into::into) + } + + /// Starts resize-dragging the window. + pub fn start_resize_dragging( + &self, + direction: tauri_runtime::ResizeDirection, + ) -> crate::Result<()> { + self + .window + .dispatcher + .start_resize_dragging(direction) + .map_err(Into::into) + } + + /// Sets the overlay icon on the taskbar **Windows only**. Using `None` to remove the overlay icon + /// + /// The overlay icon can be unique for each window. + #[cfg(target_os = "windows")] + #[cfg_attr(docsrs, doc(cfg(target_os = "windows")))] + pub fn set_overlay_icon(&self, icon: Option>) -> crate::Result<()> { + self + .window + .dispatcher + .set_overlay_icon(icon.map(|x| x.into())) + .map_err(Into::into) + } + + /// Sets the taskbar badge count. Using `0` or `None` will remove the badge + /// + /// ## Platform-specific + /// - **Windows:** Unsupported, use [`Window::set_overlay_icon`] instead. + /// - **iOS:** iOS expects i32, the value will be clamped to i32::MIN, i32::MAX. + /// - **Android:** Unsupported. + pub fn set_badge_count(&self, count: Option) -> crate::Result<()> { + self + .window + .dispatcher + .set_badge_count(count, Some(format!("{}.desktop", self.package_info().name))) + .map_err(Into::into) + } + + /// Sets the taskbar badge label **macOS only**. Using `None` will remove the badge + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + pub fn set_badge_label(&self, label: Option) -> crate::Result<()> { + self + .window + .dispatcher + .set_badge_label(label) + .map_err(Into::into) + } + + /// Sets the taskbar progress state. + /// + /// ## Platform-specific + /// + /// - **Linux / macOS**: Progress bar is app-wide and not specific to this window. + /// - **Linux**: Only supported desktop environments with `libunity` (e.g. GNOME). + /// - **iOS / Android:** Unsupported. + pub fn set_progress_bar(&self, progress_state: ProgressBarState) -> crate::Result<()> { + self + .window + .dispatcher + .set_progress_bar(crate::runtime::ProgressBarState { + status: progress_state.status, + progress: progress_state.progress, + desktop_filename: Some(format!("{}.desktop", self.package_info().name)), + }) + .map_err(Into::into) + } + + /// Sets the title bar style. **macOS only**. + pub fn set_title_bar_style(&self, style: tauri_utils::TitleBarStyle) -> crate::Result<()> { + self + .window + .dispatcher + .set_title_bar_style(style) + .map_err(Into::into) + } +} + +/// Progress bar state. +#[cfg(desktop)] +#[cfg_attr( + docsrs, + doc(cfg(any(target_os = "macos", target_os = "linux", windows))) +)] +#[derive(serde::Deserialize, Debug)] +pub struct ProgressBarState { + /// The progress bar status. + pub status: Option, + /// The progress bar progress. This can be a value ranging from `0` to `100` + pub progress: Option, +} + +impl Listener for Window { + /// Listen to an event on this window. + /// + /// # Examples + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +use tauri::{Manager, Listener}; + +tauri::Builder::default() + .setup(|app| { + let window = app.get_window("main").unwrap(); + window.listen("component-loaded", move |event| { + println!("window just loaded a component"); + }); + + Ok(()) + }); +``` + "#### + )] + fn listen(&self, event: impl Into, handler: F) -> EventId + where + F: Fn(Event) + Send + 'static, + { + let event = EventName::new(event.into()).unwrap(); + self.manager.listen( + event, + EventTarget::Window { + label: self.label().to_string(), + }, + handler, + ) + } + + /// Listen to an event on this window only once. + /// + /// See [`Self::listen`] for more information. + fn once(&self, event: impl Into, handler: F) -> EventId + where + F: FnOnce(Event) + Send + 'static, + { + let event = EventName::new(event.into()).unwrap(); + self.manager.once( + event, + EventTarget::Window { + label: self.label().to_string(), + }, + handler, + ) + } + + /// Unlisten to an event on this window. + /// + /// # Examples + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +use tauri::{Manager, Listener}; + +tauri::Builder::default() + .setup(|app| { + let window = app.get_window("main").unwrap(); + let window_ = window.clone(); + let handler = window.listen("component-loaded", move |event| { + println!("window just loaded a component"); + + // we no longer need to listen to the event + // we also could have used `window.once` instead + window_.unlisten(event.id()); + }); + + // stop listening to the event when you do not need it anymore + window.unlisten(handler); + + Ok(()) + }); +``` + "#### + )] + fn unlisten(&self, id: EventId) { + self.manager.unlisten(id) + } +} + +impl Emitter for Window {} + +/// The [`WindowEffectsConfig`] object builder +#[derive(Default)] +pub struct EffectsBuilder(WindowEffectsConfig); +impl EffectsBuilder { + /// Create a new [`WindowEffectsConfig`] builder + pub fn new() -> Self { + Self(WindowEffectsConfig::default()) + } + + /// Adds effect to the [`WindowEffectsConfig`] `effects` field + pub fn effect(mut self, effect: Effect) -> Self { + self.0.effects.push(effect); + self + } + + /// Adds effects to the [`WindowEffectsConfig`] `effects` field + pub fn effects>(mut self, effects: I) -> Self { + self.0.effects.extend(effects); + self + } + + /// Clears the [`WindowEffectsConfig`] `effects` field + pub fn clear_effects(mut self) -> Self { + self.0.effects.clear(); + self + } + + /// Sets `state` field for the [`WindowEffectsConfig`] **macOS Only** + pub fn state(mut self, state: EffectState) -> Self { + self.0.state = Some(state); + self + } + /// Sets `radius` field fo the [`WindowEffectsConfig`] **macOS Only** + pub fn radius(mut self, radius: f64) -> Self { + self.0.radius = Some(radius); + self + } + /// Sets `color` field fo the [`WindowEffectsConfig`] **Windows Only** + pub fn color(mut self, color: Color) -> Self { + self.0.color = Some(color); + self + } + + /// Sets `interactive` field for the [`WindowEffectsConfig`] **macOS 27.0+ Only** + pub fn interactive(mut self, interactive: bool) -> Self { + self.0.interactive = interactive; + self + } + + /// Builds a [`WindowEffectsConfig`] + pub fn build(self) -> WindowEffectsConfig { + self.0 + } +} + +impl From for EffectsBuilder { + fn from(value: WindowEffectsConfig) -> Self { + Self(value) + } +} + +#[cfg(test)] +mod tests { + #[test] + fn window_is_send_sync() { + crate::test_utils::assert_send::(); + crate::test_utils::assert_sync::(); + } +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/window/plugin.rs b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/window/plugin.rs new file mode 100644 index 000000000..2ccc2fe92 --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/window/plugin.rs @@ -0,0 +1,351 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! The tauri plugin to create and manipulate windows from JS. + +use crate::{ + Runtime, Window, + plugin::{Builder, TauriPlugin}, + sealed::ManagerBase, +}; + +fn get_window(window: Window, label: Option) -> crate::Result> { + match label { + Some(l) if !l.is_empty() => window + .manager() + .get_window(&l) + .ok_or(crate::Error::WindowNotFound), + _ => Ok(window), + } +} +macro_rules! getter { + ($cmd: ident, $ret: ty) => { + #[command(root = "crate")] + pub async fn $cmd(window: Window, label: Option) -> crate::Result<$ret> { + get_window(window, label)?.$cmd().map_err(Into::into) + } + }; +} + +macro_rules! setter { + ($cmd: ident) => { + #[command(root = "crate")] + pub async fn $cmd(window: Window, label: Option) -> crate::Result<()> { + get_window(window, label)?.$cmd().map_err(Into::into) + } + }; + + ($cmd: ident, $input: ty) => { + #[command(root = "crate")] + pub async fn $cmd( + window: Window, + label: Option, + value: $input, + ) -> crate::Result<()> { + get_window(window, label)?.$cmd(value).map_err(Into::into) + } + }; +} + +mod commands { + use tauri_runtime::window::WindowSizeConstraints; + + use super::*; + use crate::{ + AppHandle, Monitor, PhysicalPosition, PhysicalSize, Position, Size, Theme, Window, command, + sealed::ManagerBase, utils::config::WindowConfig, window::Color, window::WindowBuilder, + }; + + #[command(root = "crate")] + pub async fn get_all_windows(app: AppHandle) -> Vec { + app.manager().windows().keys().cloned().collect() + } + + #[command(root = "crate")] + pub async fn create(window: Window, options: WindowConfig) -> crate::Result<()> { + WindowBuilder::from_config(&window, &options)?.build()?; + Ok(()) + } + + getter!(scale_factor, f64); + getter!(inner_position, PhysicalPosition); + getter!(outer_position, PhysicalPosition); + getter!(inner_size, PhysicalSize); + getter!(outer_size, PhysicalSize); + getter!(is_focused, bool); + getter!(is_resizable, bool); + getter!(is_visible, bool); + getter!(is_enabled, bool); + getter!(title, String); + getter!(theme, Theme); + #[cfg(target_os = "android")] + getter!(activity_name, String); + #[cfg(target_os = "ios")] + getter!(scene_identifier, String); + + setter!(set_resizable, bool); + setter!(set_title, &str); + setter!(show); + setter!(hide); + setter!(close); + setter!(destroy); + setter!(set_content_protected, bool); + setter!(set_size, Size); + setter!(set_min_size, Option); + setter!(set_max_size, Option); + setter!(set_position, Position); + setter!(set_focus); + setter!(set_focusable, bool); + setter!(set_background_color, Option); + setter!(set_size_constraints, WindowSizeConstraints); + setter!(set_theme, Option); + setter!(set_enabled, bool); + + getter!(current_monitor, Option); + getter!(primary_monitor, Option); + getter!(available_monitors, Vec); + + #[command(root = "crate")] + pub async fn monitor_from_point( + window: Window, + label: Option, + x: f64, + y: f64, + ) -> crate::Result> { + let window = get_window(window, label)?; + window.monitor_from_point(x, y) + } +} + +#[cfg(desktop)] +mod desktop_commands { + use tauri_runtime::ResizeDirection; + use tauri_utils::TitleBarStyle; + + use super::*; + use crate::{ + CursorIcon, Manager, PhysicalPosition, Position, UserAttentionType, Webview, command, + utils::config::WindowEffectsConfig, window::ProgressBarState, + }; + + getter!(is_fullscreen, bool); + getter!(is_minimized, bool); + getter!(is_maximized, bool); + getter!(is_decorated, bool); + getter!(is_maximizable, bool); + getter!(is_minimizable, bool); + getter!(is_closable, bool); + getter!(cursor_position, PhysicalPosition); + getter!(is_always_on_top, bool); + + setter!(center); + setter!(request_user_attention, Option); + setter!(set_maximizable, bool); + setter!(set_minimizable, bool); + setter!(set_closable, bool); + setter!(maximize); + setter!(unmaximize); + setter!(minimize); + setter!(unminimize); + setter!(set_decorations, bool); + setter!(set_shadow, bool); + setter!(set_effects, Option); + setter!(set_always_on_top, bool); + setter!(set_always_on_bottom, bool); + setter!(set_fullscreen, bool); + setter!(set_fullscreen_on_monitor, PhysicalPosition); + setter!(set_simple_fullscreen, bool); + setter!(set_skip_taskbar, bool); + setter!(set_cursor_grab, bool); + setter!(set_cursor_visible, bool); + setter!(set_cursor_icon, CursorIcon); + setter!(set_cursor_position, Position); + setter!(set_ignore_cursor_events, bool); + setter!(start_dragging); + setter!(start_resize_dragging, ResizeDirection); + setter!(set_progress_bar, ProgressBarState); + setter!(set_badge_count, Option); + #[cfg(target_os = "macos")] + setter!(set_badge_label, Option); + setter!(set_visible_on_all_workspaces, bool); + setter!(set_title_bar_style, TitleBarStyle); + + #[command(root = "crate")] + #[cfg(target_os = "windows")] + pub async fn set_overlay_icon( + webview: Webview, + window: Window, + label: Option, + value: Option, + ) -> crate::Result<()> { + let window = get_window(window, label)?; + let resources_table = webview.resources_table(); + + let value = match value { + Some(value) => Some(value.into_img(&resources_table)?.as_ref().clone()), + None => None, + }; + + window.set_overlay_icon(value) + } + + #[command(root = "crate")] + pub async fn set_icon( + webview: Webview, + window: Window, + label: Option, + value: crate::image::JsImage, + ) -> crate::Result<()> { + let window = get_window(window, label)?; + let resources_table = webview.resources_table(); + window.set_icon(value.into_img(&resources_table)?.as_ref().clone()) + } + + #[command(root = "crate")] + pub async fn toggle_maximize( + window: Window, + label: Option, + ) -> crate::Result<()> { + let window = get_window(window, label)?; + if window.is_maximized()? { + window.unmaximize()? + } else { + window.maximize()? + }; + Ok(()) + } + + #[command(root = "crate")] + pub async fn internal_toggle_maximize( + window: Window, + label: Option, + ) -> crate::Result<()> { + let window = get_window(window, label)?; + if window.is_resizable()? { + if window.is_maximized()? { + window.unmaximize()? + } else if window.is_maximizable()? { + window.maximize()? + }; + } + Ok(()) + } +} + +/// Initializes the plugin. +pub fn init() -> TauriPlugin { + use serialize_to_javascript::{DefaultTemplate, Template, default_template}; + + let mut init_script = String::new(); + + #[derive(Template)] + #[default_template("./scripts/drag.js")] + struct Drag<'a> { + os_name: &'a str, + } + + init_script.push_str( + &Drag { + os_name: std::env::consts::OS, + } + .render_default(&Default::default()) + .unwrap() + .into_string(), + ); + + Builder::new("window") + .js_init_script(init_script) + .invoke_handler(crate::generate_handler![ + #![plugin(window)] + commands::create, + // getters + commands::get_all_windows, + commands::scale_factor, + commands::inner_position, + commands::outer_position, + commands::inner_size, + commands::outer_size, + commands::is_focused, + commands::is_resizable, + commands::is_visible, + commands::is_enabled, + commands::title, + commands::theme, + #[cfg(target_os = "android")] + commands::activity_name, + #[cfg(target_os = "ios")] + commands::scene_identifier, + + commands::set_resizable, + commands::set_title, + commands::show, + commands::hide, + commands::close, + commands::destroy, + commands::set_content_protected, + commands::set_size, + commands::set_min_size, + commands::set_max_size, + commands::set_position, + commands::set_size_constraints, + commands::set_focus, + commands::set_focusable, + commands::set_enabled, + commands::set_background_color, + commands::set_theme, + commands::current_monitor, + commands::primary_monitor, + commands::monitor_from_point, + commands::available_monitors, + + #[cfg(desktop)] desktop_commands::is_fullscreen, + #[cfg(desktop)] desktop_commands::is_minimized, + #[cfg(desktop)] desktop_commands::is_maximized, + #[cfg(desktop)] desktop_commands::is_decorated, + #[cfg(desktop)] desktop_commands::is_maximizable, + #[cfg(desktop)] desktop_commands::is_minimizable, + #[cfg(desktop)] desktop_commands::is_closable, + + #[cfg(desktop)] desktop_commands::cursor_position, + #[cfg(desktop)] desktop_commands::is_always_on_top, + // setters + #[cfg(desktop)] desktop_commands::center, + #[cfg(desktop)] desktop_commands::request_user_attention, + #[cfg(desktop)] desktop_commands::set_maximizable, + #[cfg(desktop)] desktop_commands::set_minimizable, + #[cfg(desktop)] desktop_commands::set_closable, + #[cfg(desktop)] desktop_commands::maximize, + #[cfg(desktop)] desktop_commands::unmaximize, + #[cfg(desktop)] desktop_commands::minimize, + #[cfg(desktop)] desktop_commands::unminimize, + #[cfg(desktop)] desktop_commands::set_decorations, + #[cfg(desktop)] desktop_commands::set_shadow, + #[cfg(desktop)] desktop_commands::set_effects, + #[cfg(desktop)] desktop_commands::set_always_on_top, + #[cfg(desktop)] desktop_commands::set_always_on_bottom, + #[cfg(desktop)] desktop_commands::set_fullscreen, + #[cfg(desktop)] desktop_commands::set_fullscreen_on_monitor, + #[cfg(desktop)] desktop_commands::set_simple_fullscreen, + #[cfg(desktop)] desktop_commands::set_skip_taskbar, + #[cfg(desktop)] desktop_commands::set_cursor_grab, + #[cfg(desktop)] desktop_commands::set_cursor_visible, + #[cfg(desktop)] desktop_commands::set_cursor_icon, + #[cfg(desktop)] desktop_commands::set_cursor_position, + #[cfg(desktop)] desktop_commands::set_ignore_cursor_events, + #[cfg(desktop)] desktop_commands::start_dragging, + #[cfg(desktop)] desktop_commands::start_resize_dragging, + #[cfg(desktop)] desktop_commands::set_badge_count, + #[cfg(target_os = "macos")] + #[cfg(desktop)] desktop_commands::set_badge_label, + #[cfg(desktop)] desktop_commands::set_progress_bar, + #[cfg(target_os = "windows")] + #[cfg(desktop)] desktop_commands::set_overlay_icon, + #[cfg(desktop)] desktop_commands::set_icon, + #[cfg(desktop)] desktop_commands::set_visible_on_all_workspaces, + #[cfg(desktop)] desktop_commands::set_title_bar_style, + #[cfg(desktop)] desktop_commands::toggle_maximize, + #[cfg(desktop)] desktop_commands::internal_toggle_maximize, + ]) + .build() +} diff --git a/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/window/scripts/drag.js b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/window/scripts/drag.js new file mode 100644 index 000000000..5a0b1244f --- /dev/null +++ b/packages/mobile-app/src-tauri/vendor/tauri-2.12.1/src/window/scripts/drag.js @@ -0,0 +1,129 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +;(function () { + //-----------------------// + // drag on mousedown and maximize on double click on Windows and Linux + // while macOS maximization should be on mouseup and if the mouse + // moves after the double click, it should be cancelled (see https://github.com/tauri-apps/tauri/issues/8306) + //-----------------------// + const TAURI_DRAG_REGION_ATTR = 'data-tauri-drag-region' + const CLICKABLE_TAGS = new Set([ + 'A', + 'BUTTON', + 'INPUT', + 'SELECT', + 'TEXTAREA', + 'LABEL', + 'SUMMARY' + ]) + const INTERACTIVE_ROLES = new Set([ + 'button', + 'link', + 'menuitem', + 'tab', + 'checkbox', + 'radio', + 'switch', + 'option' + ]) + + function isClickableElement(el) { + return ( + CLICKABLE_TAGS.has(el.tagName) + || (el.hasAttribute('contenteditable') + && el.getAttribute('contenteditable') !== 'false') + || (el.hasAttribute('tabindex') && el.getAttribute('tabindex') !== '-1') + || INTERACTIVE_ROLES.has(el.getAttribute('role')) + ) + } + + // Walk the composed path from target upward. + // + // Supported values for data-tauri-drag-region: + // (bare / no value / "true") -> self: only direct clicks on this element trigger drag + // "deep" -> deep: clicks anywhere in the subtree trigger drag + // "false" -> disabled: drag is blocked here (and for ancestors) + // + // Clickable elements (buttons, links, etc.) normally block dragging, + // but if they themselves carry data-tauri-drag-region they act as drag regions. + function isDragRegion(composedPath) { + for (const el of composedPath) { + if (!(el instanceof HTMLElement)) continue + + const attr = el.getAttribute(TAURI_DRAG_REGION_ATTR) + + // clickable without explicit drag region → blocks drag + if (isClickableElement(el) && attr === null) return false + // no attr → keep walking up + if (attr === null) continue + // explicitly disabled + if (attr === 'false') return false + // subtree drag — any descendant triggers + if (attr === 'deep') return true + // bare or "true" attr — only direct clicks on this element + if (attr === '' || attr === 'true') return el === composedPath[0] + } + + return false + } + + const osName = __TEMPLATE_os_name__ + + // initial mousedown position for macOS + let initialX = 0 + let initialY = 0 + + document.addEventListener('mousedown', (e) => { + if ( + // was left mouse button + e.button === 0 + // and was normal click to drag or double click to maximize + && (e.detail === 1 || e.detail === 2) + // and is drag region + && isDragRegion(e.composedPath()) + ) { + // macOS maximization happens on `mouseup`, + // so we save needed state and early return + if (osName === 'macos' && e.detail === 2) { + initialX = e.clientX + initialY = e.clientY + return + } + + // prevents text cursor + e.preventDefault() + + // fix #2549: double click on drag region edge causes content to maximize without window sizing change + // https://github.com/tauri-apps/tauri/issues/2549#issuecomment-1250036908 + e.stopImmediatePropagation() + + // start dragging if the element has a `tauri-drag-region` data attribute and maximize on double-clicking it + const cmd = e.detail === 2 ? 'internal_toggle_maximize' : 'start_dragging' + window.__TAURI_INTERNALS__.invoke('plugin:window|' + cmd) + } + }) + + // on macOS we maximize on mouseup instead, to match the system behavior where maximization can be canceled + // if the mouse moves outside the data-tauri-drag-region + if (osName === 'macos') { + document.addEventListener('mouseup', (e) => { + if ( + // was left mouse button + e.button === 0 + // and was double click + && e.detail === 2 + // and the cursor hasn't moved from initial mousedown + && e.clientX === initialX + && e.clientY === initialY + // and the event path contains a drag region (with no clickable element in between) + && isDragRegion(e.composedPath()) + ) { + window.__TAURI_INTERNALS__.invoke( + 'plugin:window|internal_toggle_maximize' + ) + } + }) + } +})() diff --git a/packages/mobile-app/src/endpoint.ts b/packages/mobile-app/src/endpoint.ts new file mode 100644 index 000000000..e395db66f --- /dev/null +++ b/packages/mobile-app/src/endpoint.ts @@ -0,0 +1,15 @@ +// Keep this policy and src-tauri/policy in agreement through tests/urls.json. +// Reject URL-parser repairs before parsing: path normalization can hide /../. +export function canonicalEndpoint(input: string): string { + if (!/^https:\/\/[\x21-\x7e]+$/i.test(input) || /[\\?#@%]/.test(input)) throw new Error("invalid") + const remainder = input.slice(8) + const slash = remainder.indexOf("/") + if (slash !== -1 && remainder.slice(slash) !== "/") throw new Error("invalid") + const url = new URL(input) + const host = url.hostname.toLowerCase() + if (url.protocol !== "https:" || !host || host.endsWith(".") || url.username || url.password || + url.pathname !== "/" || url.search || url.hash || host === "localhost" || + host.endsWith(".localhost") || host === "tauri" || host === "[::1]" || + host === "0.0.0.0" || /^127\./.test(host)) throw new Error("invalid") + return `${url.origin}/` +} diff --git a/packages/mobile-app/src/launcher.css b/packages/mobile-app/src/launcher.css new file mode 100644 index 000000000..32ebde8e8 --- /dev/null +++ b/packages/mobile-app/src/launcher.css @@ -0,0 +1,17 @@ +* { box-sizing: border-box; } +html { background: var(--surface-base); color: var(--text-primary); font-family: system-ui, sans-serif; } +body { margin: 0; padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left); } +main { max-width: 38rem; margin: 0 auto; padding: 2rem 1.25rem; } +.brand { color: var(--accent-primary); font-weight: 700; letter-spacing: .02em; } +h1 { font-size: clamp(1.5rem, 5vw, 2rem); line-height: 1.2; } +p { line-height: 1.6; } +form { display: grid; gap: .75rem; margin-block: 1.5rem; } +input, button { border: 1px solid var(--border-strong); border-radius: 0; min-height: 3rem; padding: .75rem; font: inherit; } +input { width: 100%; background: var(--surface-base); color: var(--text-primary); font-family: ui-monospace, Consolas, monospace; } +button { background: var(--accent-primary); color: var(--text-on-accent); font-weight: 600; cursor: pointer; } +button:hover { background: var(--accent-hover); } +:focus-visible { outline: 2px solid var(--accent-primary); outline-offset: 3px; } +:disabled { opacity: .6; cursor: default; } +.note { color: var(--text-secondary); font-size: .875rem; } +#error { color: var(--status-error); } +@media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto; } } diff --git a/packages/mobile-app/src/main.tsx b/packages/mobile-app/src/main.tsx new file mode 100644 index 000000000..0bc42c40c --- /dev/null +++ b/packages/mobile-app/src/main.tsx @@ -0,0 +1,48 @@ +import { createSignal } from "solid-js" +import { render } from "solid-js/web" +import { invoke } from "@tauri-apps/api/core" +import { canonicalEndpoint } from "./endpoint" +import { loadMessages } from "./messages" +import "../../ui/src/styles/tokens.css" +import "./launcher.css" + +async function start() { + const t = await loadMessages(navigator.languages) + function Launcher() { + const [endpoint, setEndpoint] = createSignal("") + const [error, setError] = createSignal("") + const [busy, setBusy] = createSignal(false) + async function connect(event: SubmitEvent) { + event.preventDefault() + if (busy()) return + setError("") + let canonical: string + try { canonical = canonicalEndpoint(endpoint()) } + catch { setError(t("invalid")); return } + setBusy(true) + try { await invoke("connect_server", { endpoint: canonical }) } + catch (error) { setError(t(String(error) === "unsupported" ? "unsupported" : "failed")) } + finally { setBusy(false) } + } + return

+

CodeNomad

+

{t("remoteAccess.title")}

+

{t("help")}

+
+ + setEndpoint(event.currentTarget.value)} + disabled={busy()} aria-describedby="network error" /> + +
+ +

{t("network")}

+

{t("recovery")}

+

{t("privacy")}

+
+ } + render(() => , document.getElementById("root")!) +} + +void start() diff --git a/packages/mobile-app/src/messages.ts b/packages/mobile-app/src/messages.ts new file mode 100644 index 000000000..e5fcaf215 --- /dev/null +++ b/packages/mobile-app/src/messages.ts @@ -0,0 +1,40 @@ +// This standalone launcher must not mount the server-backed preferences provider. +// Reuse each UI locale's remote-access messages, not an English-only import. +const shared = import.meta.glob<{ remoteAccessMessages: Record }>( + "../../ui/src/lib/i18n/messages/*/remoteAccess.ts", +) +const keys = ["endpoint", "help", "invalid", "failed", "working", "network", "recovery", "privacy"] as const +const unsupported: Record = { + en: "Native recovery is unavailable or Android System WebView needs an update. Reopen the app after updating.", + es: "La recuperación nativa no está disponible o Android System WebView necesita una actualización. Actualiza y vuelve a abrir la aplicación.", + fr: "La récupération native est indisponible ou Android System WebView doit être mis à jour. Rouvrez l’application après la mise à jour.", + de: "Native Wiederherstellung ist nicht verfügbar oder Android System WebView muss aktualisiert werden. Öffnen Sie die App danach erneut.", + ru: "Нативный возврат недоступен или требуется обновление Android System WebView. После обновления откройте приложение снова.", + ja: "ネイティブの復帰機能が利用できないか、Android System WebViewの更新が必要です。更新後にアプリを開き直してください。", + "zh-Hans": "原生恢复功能不可用,或Android System WebView需要更新。更新后请重新打开应用。", + he: "החזרה המקורית אינה זמינה או שיש לעדכן את Android System WebView. פתחו שוב את האפליקציה לאחר העדכון.", + ne: "नेटिभ पुनःप्राप्ति उपलब्ध छैन वा Android System WebView अद्यावधिक गर्नुपर्छ। अद्यावधिकपछि एप फेरि खोल्नुहोस्।", + tr: "Yerel kurtarma kullanılamıyor veya Android System WebView güncellenmeli. Güncellemeden sonra uygulamayı yeniden açın.", +} +const messages: Record = { + en: ["Server HTTPS origin", "Open the hosted CodeNomad interface and sign in there.", "Enter a root HTTPS origin without credentials, path, query or fragment.", "Unable to open the server. Try again.", "Opening…", "Use a trusted server reachable from this device. No local backend runs here.", "The native ↩ CodeNomad control returns here even if the server is offline. Returning disconnects this view, not the server session.", "Returning preserves sign-in and server drafts. Sign out in the hosted interface to end authentication."], + es: ["Origen HTTPS del servidor", "Abre la interfaz alojada de CodeNomad e inicia sesión allí.", "Introduce un origen HTTPS raíz sin credenciales, ruta, consulta ni fragmento.", "No se pudo abrir el servidor. Inténtalo de nuevo.", "Abriendo…", "Usa un servidor de confianza accesible desde este dispositivo. Aquí no se ejecuta un servidor local.", "El control nativo ↩ CodeNomad vuelve aquí incluso sin conexión. Volver desconecta esta vista, no la sesión del servidor.", "Volver conserva el acceso y los borradores del servidor. Cierra sesión en la interfaz alojada para terminar la autenticación."], + fr: ["Origine HTTPS du serveur", "Ouvrez l’interface hébergée de CodeNomad et connectez-vous.", "Saisissez une origine HTTPS racine sans identifiants, chemin, paramètres ni fragment.", "Impossible d’ouvrir le serveur. Réessayez.", "Ouverture…", "Utilisez un serveur de confiance accessible depuis cet appareil. Aucun serveur local ne fonctionne ici.", "Le contrôle natif ↩ CodeNomad permet de revenir ici même hors ligne. Le retour déconnecte cette vue, pas la session serveur.", "Le retour conserve la connexion et les brouillons du serveur. Déconnectez-vous dans l’interface hébergée pour terminer l’authentification."], + de: ["HTTPS-Ursprung des Servers", "Öffnen Sie die gehostete CodeNomad-Oberfläche und melden Sie sich dort an.", "Geben Sie einen HTTPS-Ursprung ohne Zugangsdaten, Pfad, Abfrage oder Fragment ein.", "Der Server konnte nicht geöffnet werden. Versuchen Sie es erneut.", "Wird geöffnet…", "Verwenden Sie einen vertrauenswürdigen, vom Gerät erreichbaren Server. Hier läuft kein lokaler Server.", "Die native Steuerung ↩ CodeNomad führt auch offline hierher zurück. Die Rückkehr trennt diese Ansicht, nicht die Serversitzung.", "Die Rückkehr erhält Anmeldung und Serverentwürfe. Melden Sie sich in der gehosteten Oberfläche ab, um die Authentifizierung zu beenden."], + ru: ["HTTPS-источник сервера", "Откройте размещённый интерфейс CodeNomad и войдите в нём.", "Введите корневой HTTPS-источник без учётных данных, пути, запроса или фрагмента.", "Не удалось открыть сервер. Попробуйте снова.", "Открытие…", "Используйте доверенный сервер, доступный с этого устройства. Локальный сервер здесь не запускается.", "Нативная кнопка ↩ CodeNomad возвращает сюда даже без сети. Возврат отключает этот вид, но не сеанс сервера.", "Возврат сохраняет вход и черновики сервера. Для завершения аутентификации выйдите в интерфейсе сервера."], + ja: ["サーバーのHTTPSオリジン", "ホストされたCodeNomad画面を開き、そこでログインします。", "認証情報、パス、クエリ、フラグメントを含まないルートHTTPSオリジンを入力してください。", "サーバーを開けませんでした。再試行してください。", "開いています…", "この端末から接続できる信頼済みサーバーを使用してください。ローカルサーバーは起動しません。", "ネイティブの↩ CodeNomadボタンでオフライン時もここに戻れます。戻るとこの画面を切断しますが、サーバーのセッションは終了しません。", "戻ってもログインとサーバーの下書きは保持されます。認証を終了するにはサーバー画面でログアウトしてください。"], + "zh-Hans": ["服务器HTTPS源", "打开托管的CodeNomad界面并在那里登录。", "请输入不含凭据、路径、查询或片段的根HTTPS源。", "无法打开服务器,请重试。", "正在打开…", "请使用此设备可以访问的可信服务器。此处不会运行本地后端。", "即使服务器离线,原生↩ CodeNomad控件也能返回此处。返回会断开此视图,而非服务器会话。", "返回会保留登录和服务器草稿。请在托管界面中退出登录以结束身份验证。"], + he: ["מקור HTTPS של השרת", "פתחו את ממשק CodeNomad המתארח והתחברו בו.", "הזינו מקור HTTPS ראשי ללא פרטי גישה, נתיב, שאילתה או מקטע.", "לא ניתן לפתוח את השרת. נסו שוב.", "פותח…", "השתמשו בשרת מהימן הנגיש מהמכשיר. לא פועל כאן שרת מקומי.", "הפקד המקורי ↩ CodeNomad מחזיר לכאן גם ללא רשת. החזרה מנתקת את התצוגה ולא את הפעלת השרת.", "החזרה שומרת את ההתחברות ואת טיוטות השרת. התנתקו בממשק המתארח כדי לסיים את האימות."], + ne: ["सर्भरको HTTPS मूल", "होस्ट गरिएको CodeNomad खोल्नुहोस् र त्यहाँ लगइन गर्नुहोस्।", "प्रमाण, पथ, क्वेरी वा खण्ड नभएको मूल HTTPS ठेगाना प्रविष्ट गर्नुहोस्।", "सर्भर खोल्न सकिएन। फेरि प्रयास गर्नुहोस्।", "खोलिँदै…", "यस उपकरणबाट पहुँच हुने विश्वसनीय सर्भर प्रयोग गर्नुहोस्। यहाँ स्थानीय ब्याकएन्ड चल्दैन।", "नेटिभ ↩ CodeNomad नियन्त्रणले अफलाइन हुँदा पनि यहाँ फर्काउँछ। फर्किँदा यो दृश्य छुट्छ, सर्भर सत्र होइन।", "फर्किँदा लगइन र सर्भरका मस्यौदा सुरक्षित रहन्छन्। प्रमाणीकरण अन्त्य गर्न होस्ट गरिएको इन्टरफेसमा लगआउट गर्नुहोस्।"], + tr: ["Sunucunun HTTPS kökeni", "Barındırılan CodeNomad arayüzünü açın ve orada oturum açın.", "Kimlik bilgisi, yol, sorgu veya parça içermeyen kök HTTPS kökeni girin.", "Sunucu açılamadı. Yeniden deneyin.", "Açılıyor…", "Bu cihazdan erişilebilen güvenilir bir sunucu kullanın. Burada yerel arka uç çalışmaz.", "Yerel ↩ CodeNomad denetimi çevrimdışıyken bile buraya döner. Dönüş sunucu oturumunu değil, bu görünümü ayırır.", "Dönüş oturum açmayı ve sunucu taslaklarını korur. Kimlik doğrulamayı bitirmek için barındırılan arayüzde oturumu kapatın."], +} + +export async function loadMessages(languages: readonly string[]) { + const locale = languages.map((tag) => tag.toLowerCase().split("-")[0]) + .map((base) => base === "zh" ? "zh-Hans" : base).find((candidate) => candidate in messages) ?? "en" + const { remoteAccessMessages } = await shared[`../../ui/src/lib/i18n/messages/${locale}/remoteAccess.ts`]() + document.documentElement.lang = locale + document.documentElement.dir = locale === "he" ? "rtl" : "ltr" + const own = Object.fromEntries(keys.map((key, index) => [key, messages[locale][index]])) + return (key: string) => key === "unsupported" ? unsupported[locale] : own[key] ?? remoteAccessMessages[key] ?? key +} diff --git a/packages/mobile-app/tests/android-transport.test.ts b/packages/mobile-app/tests/android-transport.test.ts new file mode 100644 index 000000000..ae416143c --- /dev/null +++ b/packages/mobile-app/tests/android-transport.test.ts @@ -0,0 +1,37 @@ +import assert from "node:assert/strict" +import { readFile } from "node:fs/promises" +import { spawnSync } from "node:child_process" +import { fileURLToPath } from "node:url" +import path from "node:path" +import { test } from "node:test" + +const read = (relative: string) => readFile(new URL(relative, import.meta.url), "utf8") + +test("production Android manifest/XML deny cleartext including loopback variants", (context) => { + const java = process.env.JAVA_HOME ? path.join(process.env.JAVA_HOME, "bin", "java") : "java" + const result = spawnSync(java, ["--source", "17", "tests/native/TransportPolicyTest.java"], { + cwd: fileURLToPath(new URL("..", import.meta.url)), encoding: "utf8", + }) + if (result.error && (result.error as NodeJS.ErrnoException).code === "ENOENT") { + context.skip("JDK 17+ required for production XML parsing; Android builds must run this check") + return + } + assert.equal(result.status, 0, result.stdout + result.stderr) +}) + +test("Android transport settings deny mixed content without replacing Wry HTTPS protocols", async () => { + const base = "../src-tauri/recovery/android/src/main/java/ai/neuralnomads/codenomad/recovery/" + const plugin = await read(`${base}RecoveryPlugin.kt`) + const load = plugin.slice(plugin.indexOf("override fun load(")) + assert.match(load, /webView\.settings\.mixedContentMode = WebSettings\.MIXED_CONTENT_NEVER_ALLOW/) + assert(load.indexOf("MIXED_CONTENT_NEVER_ALLOW") < load.indexOf("webView.post")) + assert.doesNotMatch(plugin, /MIXED_CONTENT_(ALWAYS_ALLOW|COMPATIBILITY_MODE)/) + assert.match(await read("../src-tauri/src/lib.rs"), /\.use_https_scheme\(true\)/) + const fence = await read(`${base}NavigationFence.kt`) + assert.match(fence, /return original\.shouldInterceptRequest\(view, request\)/) + assert.match(fence, /override fun onReceivedSslError[\s\S]*handler\.cancel\(\)/) + assert.doesNotMatch(fence, /handler\.proceed\(/) + // Subresource fetch login/upload/SSE and Wry IPC must not be restricted by + // the selected-origin main-frame gate. + assert.match(await read(`${base}ConnectionAuthority.kt`), /!mainFrame \|\| \(method == "GET" && admits\(url\)\)/) +}) diff --git a/packages/mobile-app/tests/android-workflow.test.ts b/packages/mobile-app/tests/android-workflow.test.ts new file mode 100644 index 000000000..2d631aca2 --- /dev/null +++ b/packages/mobile-app/tests/android-workflow.test.ts @@ -0,0 +1,58 @@ +import assert from "node:assert/strict" +import { readFileSync } from "node:fs" +import { test } from "node:test" +import { runInNewContext } from "node:vm" + +const workflow = readFileSync(new URL("../../../.github/workflows/mobile-android.yml", import.meta.url), "utf8").replace(/\r\n/g, "\n") +const source = workflow.slice(workflow.indexOf(" source-validation:"), workflow.indexOf(" debug-apk:")) +const native = workflow.slice(workflow.indexOf(" debug-apk:")) + +test("Android PR CI keeps source checks unconditional and native SDK consent explicit", () => { + assert.match(workflow, /^ pull_request:\n paths:/m) + assert.match(workflow, /^ workflow_dispatch:/m) + assert.match(workflow, /accept_sdk_licenses:[\s\S]*?type: boolean[\s\S]*?default: false/) + assert.match(native, /^ needs: source-validation$/m) + assert.doesNotMatch(source, /^ if:/m) + const gate = native.match(/^ if: \$\{\{ (.+) \}\}$/m)?.[1] + assert(gate, "native job must have an explicit consent gate") + assert(source.includes(`SDK_LICENSE_AUTHORIZED: \${{ ${gate} }}`), "summary must report the actual job gate") + for (const event of ["pull_request", "workflow_dispatch", "push"]) { + for (const consent of [undefined, "false", "TRUE", "true"]) { + for (const checkbox of [undefined, false, true]) { + const actual = runInNewContext(gate, { + github: { event_name: event }, + // GitHub expression string equality is case-insensitive. + vars: { CODENOMAD_ANDROID_ACCEPT_SDK_LICENSES: consent?.toLowerCase() }, + inputs: { accept_sdk_licenses: checkbox }, + }, { timeout: 100 }) + assert.equal(actual, (event === "pull_request" && consent?.toLowerCase() === "true") || + (event === "workflow_dispatch" && checkbox === true), `${event}/${consent}/${checkbox}`) + } + } + } + for (const command of ["npm run typecheck:mobile", "npm run test:mobile", "npm run ios:verify", + "npm run test:browser", "cargo test --locked --manifest-path packages/mobile-app/src-tauri/policy/Cargo.toml"]) { + assert(source.includes(command), `missing source check: ${command}`) + } + assert.doesNotMatch(source, /setup-android@|npm run android:(init|debug)/) + assert.match(source, /working-directory: packages\/mobile-app\n run: java --source 17 tests\/native\/TransportPolicyTest.java/) + assert.match(native, /npm run android:init --workspace @codenomad\/mobile-app -- --ci --skip-targets-install/) + assert.match(native, /npm run android:debug --workspace @codenomad\/mobile-app -- --ci/) + const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) + assert.equal(pkg.scripts["android:debug"], "tauri android build --debug --apk --target aarch64") +}) + +test("Android PR CI pins actions, checks the merge SHA and has no privileged publication path", () => { + assert.match(workflow, /^permissions:\n contents: read\n/m) + assert.doesNotMatch(workflow, /pull_request_target|secrets\.|continue-on-error|cache:|actions\/cache@|npm run android:(release|bundle)|write-all|contents: write/) + const actions = [...workflow.matchAll(/uses: ([^\s]+)(?: #.*)?/g)].map((match) => match[1]) + assert(actions.length >= 6) + for (const action of actions) assert.match(action, /^[\w-]+\/[\w-]+@[a-f0-9]{40}$/) + assert.equal(workflow.match(/ref: \$\{\{ github.sha \}\}/g)?.length, 2) + assert.equal(workflow.match(/persist-credentials: false/g)?.length, 2) + assert.equal(workflow.match(/run: npm ci --ignore-scripts/g)?.length, 2) + assert.match(native, /if \(\$apks.Count -eq 0\) \{ throw/) + assert.match(native, /if-no-files-found: error/) + assert.match(native, /retention-days: 7/) + assert.match(source, /APK job SKIPPED/) +}) diff --git a/packages/mobile-app/tests/browser/launcher.test.ts b/packages/mobile-app/tests/browser/launcher.test.ts new file mode 100644 index 000000000..a0a3445f9 --- /dev/null +++ b/packages/mobile-app/tests/browser/launcher.test.ts @@ -0,0 +1,65 @@ +import assert from "node:assert/strict" +import { createServer } from "node:http" +import { mkdir, readFile } from "node:fs/promises" +import { fileURLToPath } from "node:url" +import { test } from "node:test" +import { chromium, type Browser } from "playwright" + +test("real bundled launcher validates before invoking and handles native failures locally", async () => { + const server = createServer(async (request, response) => { + const path = new URL(request.url!, "http://localhost").pathname + if (path.includes("..")) { response.writeHead(403).end(); return } + const file = new URL(`../../dist${path === "/" ? "/index.html" : path}`, import.meta.url) + try { + const type = path.endsWith(".js") ? "text/javascript" : path.endsWith(".css") ? "text/css" : "text/html" + response.writeHead(200, { "Content-Type": type }).end(await readFile(file)) + } catch { response.writeHead(404).end() } + }) + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)) + const address = server.address() as { port: number } + let browser: Browser | undefined + try { + browser = await chromium.launch({ executablePath: process.env.CODENOMAD_BROWSER_PATH }) + for (const locale of ["en-US", "es-ES", "fr-FR", "de-DE", "ru-RU", "ja-JP", "zh-CN", "he-IL", "ne-NP", "tr-TR"]) { + const context = await browser.newContext({ viewport: { width: 390, height: 844 }, locale }) + // Literal script avoids tsx/esbuild helper references in serialized functions. + await context.addInitScript(` + window.calls = []; + window.__TAURI_INTERNALS__ = { invoke: async (command, args) => { + window.calls.push({ command, args }); + throw window.failure ?? "unavailable"; + }}; + `) + const page = await context.newPage() + await page.goto(`http://127.0.0.1:${address.port}`) + await page.locator("form").waitFor() + assert.equal(await page.locator("iframe").count(), 0) + assert.equal(await page.locator("html").getAttribute("dir"), locale.startsWith("he") ? "rtl" : "ltr") + assert.equal(await page.locator("input").getAttribute("dir"), "ltr") + assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true) + await page.locator("input").fill("https://user:secret@example.com") + await page.locator("button").click() + assert.notEqual(await page.locator("#error").textContent(), "") + assert.deepEqual(await page.evaluate(() => (window as any).calls), []) + await page.locator("input").fill("https://EXAMPLE.com:443/") + await page.locator("button").click() + await page.waitForFunction(() => !(document.querySelector("button") as HTMLButtonElement).disabled) + assert.deepEqual(await page.evaluate(() => (window as any).calls), [ + { command: "connect_server", args: { endpoint: "https://example.com/" } }, + ], JSON.stringify(await page.evaluate(() => ({ error: document.querySelector("#error")?.textContent, + value: (document.querySelector("input") as HTMLInputElement).value, + valid: (document.querySelector("input") as HTMLInputElement).validity.valid })))) + await page.evaluate(() => { (window as any).failure = "unsupported" }) + await page.locator("button").click() + await page.waitForFunction(() => !(document.querySelector("button") as HTMLButtonElement).disabled) + assert.match((await page.locator("#error").textContent())!, /Android System WebView/) + const output = new URL(`../../test-results/${locale}.png`, import.meta.url) + await mkdir(new URL("../../test-results/", import.meta.url), { recursive: true }) + await page.screenshot({ path: fileURLToPath(output), fullPage: true }) + await context.close() + } + } finally { + await browser?.close() + await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())) + } +}) diff --git a/packages/mobile-app/tests/bundle.test.ts b/packages/mobile-app/tests/bundle.test.ts new file mode 100644 index 000000000..dcc80fbf0 --- /dev/null +++ b/packages/mobile-app/tests/bundle.test.ts @@ -0,0 +1,52 @@ +import assert from "node:assert/strict" +import { test } from "node:test" +import { fileURLToPath } from "node:url" +import { build } from "vite" + +// Inspect the real Rollup module graph, not minified symbol names. Seroval and +// Solid's server entry points are installed for SSR tooling but must never enter +// the untrusted-input-facing bundled launcher (including dynamic locale chunks). +function assertClientOnly(ids: Iterable) { + for (const id of ids) { + const path = id.replaceAll("\\", "/") + assert.doesNotMatch(path, /\/node_modules\/seroval(?:-plugins)?\//, id) + assert.doesNotMatch(path, /\/solid-js\/(?:web\/|store\/)?dist\/server(?:\.[^/]*)?\.js/, id) + } +} + +test("bundle guard rejects Seroval deserialization and Solid SSR module paths", () => { + for (const id of [ + "/node_modules/seroval/dist/esm/production/index.mjs", + "C:\\repo\\node_modules\\seroval\\dist\\cjs\\production\\index.cjs", + "/node_modules/seroval-plugins/web/index.js", + "/node_modules/solid-js/dist/server.js", + "/node_modules/solid-js/web/dist/server.js", + ]) assert.throws(() => assertClientOnly([id])) + assertClientOnly(["/node_modules/solid-js/dist/solid.js", "/node_modules/solid-js/web/dist/web.js"]) +}) + +test("real launcher bundle and every locale exclude Seroval/SSR/deserialization modules", async () => { + const modules = new Set() + const scripts: string[] = [] + await build({ + root: fileURLToPath(new URL("../", import.meta.url)), + configFile: fileURLToPath(new URL("../vite.config.ts", import.meta.url)), + logLevel: "error", + build: { write: false }, + plugins: [{ + name: "verify-launcher-client-only-graph", + generateBundle(_, bundle) { + for (const output of Object.values(bundle)) { + if (output.type !== "chunk") continue + Object.keys(output.modules).forEach((id) => modules.add(id)) + scripts.push(output.code) + } + }, + }], + }) + assert(modules.size > 10, "guard must inspect a nonempty real bundle") + assert([...modules].some((id) => id.replaceAll("\\", "/").includes("/solid-js/web/dist/web.js"))) + assert.equal([...modules].filter((id) => /\/messages\/[^/]+\/remoteAccess\.ts$/.test(id.replaceAll("\\", "/"))).length, 10) + assertClientOnly(modules) + assert.doesNotMatch(scripts.join("\n"), /seroval|\bfromJSON\s*\(|\bdeserialize\s*\(/) +}) diff --git a/packages/mobile-app/tests/contract.test.ts b/packages/mobile-app/tests/contract.test.ts new file mode 100644 index 000000000..a6918f0db --- /dev/null +++ b/packages/mobile-app/tests/contract.test.ts @@ -0,0 +1,104 @@ +import assert from "node:assert/strict" +import { readFile } from "node:fs/promises" +import { test } from "node:test" + +const read = (relative: string) => readFile(new URL(relative, import.meta.url), "utf8") + +test("bundled launcher is the only capability; no server/CLI resources", async () => { + const config = JSON.parse(await read("../src-tauri/tauri.conf.json")) + const capability = JSON.parse(await read("../src-tauri/capabilities/launcher.json")) + assert.deepEqual(config.app.security.capabilities, ["launcher"]) + assert.equal(config.app.withGlobalTauri, false) + assert.equal(config.bundle.resources, undefined) + assert.equal(config.bundle.externalBin, undefined) + assert.equal(config.bundle.android.minSdkVersion, 26) + assert.equal(capability.local, true) + assert.equal(capability.remote, undefined) + assert.deepEqual(capability.webviews, ["main"]) + assert.deepEqual(capability.permissions, ["allow-connect-server"]) + assert.match(config.app.security.csp, /frame-src 'none'/) +}) + +test("native mobile recovery uses fixed bundled destinations, not remote DOM/IPC", async () => { + const kotlin = await read("../src-tauri/recovery/android/src/main/java/ai/neuralnomads/codenomad/recovery/RecoveryPlugin.kt") + const fence = await read("../src-tauri/recovery/android/src/main/java/ai/neuralnomads/codenomad/recovery/NavigationFence.kt") + const swift = await read("../src-tauri/recovery/ios/Sources/RecoveryPlugin.swift") + assert.match(kotlin, /root\.addView\(button/) + assert.match(kotlin, /current\.view\.loadUrl\(NavigationFence\.LAUNCHER\)/) + assert.match(kotlin, /DOCUMENT_START_SCRIPT/) + assert.match(fence, /LAUNCHER = ConnectionAuthority\.LAUNCHER/) + assert.match(fence, /authority\.allowsRequest\(request\.url\.toString\(\), request\.method, request\.isForMainFrame\)/) + assert.match(swift, /root\.addSubview\(control\)/) + assert.match(swift, /tauri:\/\/localhost\/index\.html/) + assert.doesNotMatch(kotlin + swift, /evaluateJavaScript|evaluateJavascript|goBack\(/) +}) + +test("iOS app and recovery package share the approved iOS 15 minimum", async () => { + const config = JSON.parse(await read("../src-tauri/tauri.conf.json")) + const swiftPackage = await read("../src-tauri/recovery/ios/Package.swift") + assert.equal(config.bundle.iOS.minimumSystemVersion, "15.0") + assert.match(swiftPackage, /platforms: \[\.iOS\("15\.0"\)\]/) +}) + +test("host flags use the actual hosted UI override names", async () => { + const rust = await read("../src-tauri/src/lib.rs") + assert.match(rust, /__CODENOMAD_RUNTIME_HOST__.*value: 'web'/) + assert.match(rust, /__CODENOMAD_WINDOW_CONTEXT__.*value: 'remote'/) + assert.match(rust, /\.initialization_script\(HOSTED_CONTEXT\)/) + assert.match(rust, /tauri_plugin_mobile_recovery::connect\(/) + assert.match(await read("../src-tauri/build.rs"), /AppManifest::new\(\)\.commands\(&\["connect_server"\]\)/) +}) + +test("Android rejects non-GET main-frame requests before delegating networking (307/308)", async () => { + const base = "../src-tauri/recovery/android/src/main/java/ai/neuralnomads/codenomad/recovery/" + const fence = await read(`${base}NavigationFence.kt`) + const authority = await read(`${base}ConnectionAuthority.kt`) + const intercept = fence.slice(fence.indexOf("override fun shouldInterceptRequest"), fence.indexOf("override fun onPageStarted")) + assert.match(authority, /!mainFrame \|\| \(method == "GET" && admits\(url\)\)/) + assert.match(intercept, /if \(!authority\.interceptRequest[\s\S]*return WebResourceResponse/) + assert.match(authority, /if \(!allowsRequest\(url, method, mainFrame\)\) return false/) + assert(intercept.indexOf("return WebResourceResponse") < intercept.indexOf("original.shouldInterceptRequest")) + const tests = await read("./native/ConnectionAuthorityTest.kt") + assert.match(tests, /listOf\(307, 308\)/) + assert.match(tests, /initialRequests == 0 && redirectedRequests == 0/) + const login = await read("../../server/src/server/routes/auth-pages/login.html") + assert.match(login, /fetch\("\/api\/auth\/login",\s*\{\s*method: "POST"/) + assert.match(login, /window\.location\.replace\("\/"\)/) +}) + +test("native readiness is read-only and commit/load shares the platform UI transaction", async () => { + const kotlin = await read("../src-tauri/recovery/android/src/main/java/ai/neuralnomads/codenomad/recovery/RecoveryPlugin.kt") + const readiness = kotlin.slice(kotlin.indexOf("fun readiness("), kotlin.indexOf("fun connect(")) + assert.match(readiness, /activity\.runOnUiThread/) + assert.match(readiness, /authority\.snapshot\(\)/) + assert.doesNotMatch(readiness, /\.commit\(|\.select\(|loadUrl\(|webViewClient =/) + const connect = kotlin.slice(kotlin.indexOf("fun connect("), kotlin.indexOf("override fun load(")) + assert.match(connect, /activity\.runOnUiThread/) + assert.match(connect, /authority\.commit\(args\.generation, args\.endpoint\)/) + assert(connect.indexOf("authority.commit") < connect.indexOf("current.view.loadUrl(args.endpoint)")) + const rust = await read("../src-tauri/src/lib.rs") + assert(rust.indexOf("guard.commit_connection") < rust.indexOf("tauri_plugin_mobile_recovery::connect(")) + assert.match(rust, /guard\.abort_connection\(&committed\)/) +}) + +test("Android renderer death revokes, destroys and closes; never reuses dead WebView", async () => { + const kotlin = await read("../src-tauri/recovery/android/src/main/java/ai/neuralnomads/codenomad/recovery/RecoveryPlugin.kt") + const fence = await read("../src-tauri/recovery/android/src/main/java/ai/neuralnomads/codenomad/recovery/NavigationFence.kt") + const callback = fence.slice(fence.indexOf("override fun onRenderProcessGone")) + assert.match(callback, /authority\.disconnect\(\)/) + assert.match(callback, /rendererGone\(view\)/) + assert.match(callback, /return true/) + assert.doesNotMatch(callback, /original\.onRenderProcessGone|\.loadUrl\(/) + assert.match(kotlin, /binding = null[\s\S]*removeView\(dead\)[\s\S]*dead\.destroy\(\)[\s\S]*activity\.finish\(\)/) +}) + +test("iOS readiness and connection UIKit access/resolve are on the main queue", async () => { + const swift = await read("../src-tauri/recovery/ios/Sources/RecoveryPlugin.swift") + const readiness = swift.slice(swift.indexOf("@objc public func readiness"), swift.indexOf("@objc public func connect")) + assert.match(readiness, /DispatchQueue\.main\.async[\s\S]*self\.launcherReady\(\)/) + assert(readiness.indexOf("DispatchQueue.main.async") < readiness.indexOf("invoke.resolve")) + const connect = swift.slice(swift.indexOf("@objc public func connect"), swift.indexOf("override func load")) + assert.match(connect, /DispatchQueue\.main\.async[\s\S]*args\.generation == self\.generation/) + assert.match(swift, /webview\.observe\(\\\.isLoading/) + assert.match(swift, /webview\.observe\(\\\.url/) +}) diff --git a/packages/mobile-app/tests/endpoint.test.ts b/packages/mobile-app/tests/endpoint.test.ts new file mode 100644 index 000000000..d19e194f9 --- /dev/null +++ b/packages/mobile-app/tests/endpoint.test.ts @@ -0,0 +1,11 @@ +import assert from "node:assert/strict" +import { test } from "node:test" +import fixtures from "./urls.json" +import { canonicalEndpoint } from "../src/endpoint.ts" + +test("canonical HTTPS roots agree with native fixtures", () => { + for (const [input, expected] of fixtures.valid) assert.equal(canonicalEndpoint(input), expected, input) +}) +test("reject deceptive URLs and launcher-origin collisions before normalization", () => { + for (const input of fixtures.invalid) assert.throws(() => canonicalEndpoint(input), Error, input) +}) diff --git a/packages/mobile-app/tests/ios-abi.test.ts b/packages/mobile-app/tests/ios-abi.test.ts new file mode 100644 index 000000000..26bfff8ea --- /dev/null +++ b/packages/mobile-app/tests/ios-abi.test.ts @@ -0,0 +1,50 @@ +import assert from "node:assert/strict" +import { readFileSync, cpSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs" +import { join } from "node:path" +import test from "node:test" +import { assertCommandABI, mobileRoot, vendorRoot, verifyIOSABI } from "../scripts/verify-ios-abi.mjs" + +test("source-owned Cargo patch, native pre-link guard and actual Rust/Swift callers agree", () => { + assert.deepEqual(verifyIOSABI(), { upstreamFiles: 142, nativeGuardInputs: 13 }) + const appBuild = readFileSync(join(mobileRoot, "src-tauri/build.rs"), "utf8") + assert.match(appBuild, /DEP_TAURI_IOS_LIBRARY_PATH/) + assert.match(appBuild, /assert_eq!\(actual, expected, "unpatched Tauri iOS dependency selected"\)/) + const recovery = readFileSync(join(mobileRoot, "src-tauri/recovery/src/lib.rs"), "utf8") + assert.match(recovery, /run_mobile_plugin/) +}) + +test("regression rejects the original Tauri Int/Bool export, not a parallel contract model", () => { + const patched = readFileSync(join(vendorRoot, "mobile/ios-api/Sources/Tauri/Tauri.swift"), "utf8") + const original = patched.replace("id: Int32,", "id: Int,") + .replace("(Int32, Int32, UnsafePointer)", "(Int, Bool, UnsafePointer)") + .replace('callback(id, success ? 1 : 0, payload ?? "null")', 'callback(id, success, payload ?? "null")') + assert.throws(() => assertCommandABI(original), /ABI mismatch/) + for (const broken of [patched.replace("id: Int32,", "id: Int,"), + patched.replace("(Int32, Int32,", "(Int32, Bool,"), + patched.replace("success ? 1 : 0", "success")]) { + assert.throws(() => assertCommandABI(broken), /ABI mismatch/) + } +}) + +test("integrated verification fails closed on Swift source and Cargo route drift", () => { + const parent = join(mobileRoot, "test-results/ios-abi") + mkdirSync(parent, { recursive: true }) + const root = mkdtempSync(join(parent, "negative-")) + try { + cpSync(join(mobileRoot, "src-tauri/vendor"), join(root, "src-tauri/vendor"), { recursive: true }) + for (const file of ["Cargo.toml", "Cargo.lock"]) { + cpSync(join(mobileRoot, "src-tauri", file), join(root, "src-tauri", file)) + } + verifyIOSABI(root) + const swift = join(root, "src-tauri/vendor/tauri-2.12.1/mobile/ios-api/Sources/Tauri/Tauri.swift") + const source = readFileSync(swift, "utf8") + writeFileSync(swift, source.replace("id: Int32,", "id: Int,")) + assert.throws(() => verifyIOSABI(root), /input drift/) + writeFileSync(swift, source) + const cargo = join(root, "src-tauri/Cargo.toml") + writeFileSync(cargo, readFileSync(cargo, "utf8").replace('path = "vendor/tauri-2.12.1"', 'path = "wrong"')) + assert.throws(() => verifyIOSABI(root), /wiring drift/) + } finally { + rmSync(root, { recursive: true }) + } +}) diff --git a/packages/mobile-app/tests/native/ConnectionAuthorityTest.kt b/packages/mobile-app/tests/native/ConnectionAuthorityTest.kt new file mode 100644 index 000000000..4b479250a --- /dev/null +++ b/packages/mobile-app/tests/native/ConnectionAuthorityTest.kt @@ -0,0 +1,79 @@ +package ai.neuralnomads.codenomad.recovery + +// Standalone JVM tests of the production class: no Android SDK, mocks, or +// replicated policy. Compile both files with kotlinc and run this main. +fun main() { + val root = "https://one.example/" + val authority = ConnectionAuthority() + val first = authority.snapshot() + val duplicate = authority.snapshot() + check(first == duplicate) // Readiness never rotates/selects authority. + check(!authority.admits(root)) + for (invalid in listOf("http://one.example/", "https://user@one.example/", "https://one.example/path", + "https://one.example/?token=secret", "https://tauri.localhost/")) { + check(!authority.commit(first, invalid)) + check(authority.snapshot() == first) + } + check(authority.commit(first, root)) + check(!authority.commit(duplicate, "https://two.example/")) + check(authority.admits(root)) + check(!authority.admits("https://two.example/")) + + // Regression: block the INITIAL POST, before its 307/308 response can send + // preserved credentials/body to a different origin. No initial request is + // admitted, so neither the initial server nor redirect target is reached. + for (status in listOf(307, 308)) { + var initialRequests = 0 + var redirectedRequests = 0 + if (authority.interceptRequest("${root}redirect-$status", "POST", true)) { + initialRequests++ + // Android may follow this without re-running interception. + redirectedRequests++ + } + check(initialRequests == 0 && redirectedRequests == 0) { "Unsafe $status POST chain" } + } + for (method in listOf("POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "get")) { + check(!authority.allowsRequest(root, method, true)) + check(!authority.allowsRequest(ConnectionAuthority.LAUNCHER, method, true)) + } + check(authority.allowsRequest("${root}api/auth/login", "POST", false)) // fetch login unaffected. + check(authority.allowsRequest(root, "GET", true)) // location.replace('/') after login. + check(!authority.allowsRequest("https://evil.example/", "GET", true)) + check(!authority.admits("https://user@one.example/")) + check(!authority.admits("http://one.example/")) + check(!authority.admits("https://one.example:8443/")) + + authority.disconnect() + val beforeReturn = authority.snapshot() + authority.disconnect() // Native return before Rust navigation notification. + check(!authority.commit(beforeReturn, root)) + val beforeReplacement = authority.snapshot() + authority.documentStarted(ConnectionAuthority.LAUNCHER) // Same-URL reload. + check(!authority.commit(beforeReplacement, root)) + check(!authority.canConnect()) + authority.documentFinished(ConnectionAuthority.LAUNCHER) + check(authority.canConnect()) + check(!ConnectionAuthority().commit(authority.snapshot(), root)) // Different WebView identity. + + val beforeRequest = authority.snapshot() + check(authority.interceptRequest(ConnectionAuthority.LAUNCHER, "GET", true)) + check(!authority.commit(beforeRequest, root)) // Background request, before UI onPageStarted. + check(!authority.canConnect()) + authority.documentFinished("https://old-document.example/") + check(!authority.canConnect()) // Late page-finished cannot settle a different document. + authority.documentFinished(ConnectionAuthority.LAUNCHER) + + val beforeRejectedPost = authority.snapshot() + check(!authority.interceptRequest(ConnectionAuthority.LAUNCHER, "POST", true)) + check(authority.snapshot() == beforeRejectedPost) // Denial does not select/rotate. + + val afterReturn = authority.snapshot() + check(authority.commit(afterReturn, "https://two.example/")) + check(!authority.commit(beforeReturn, root)) // Delayed earlier native commit. + check(authority.admits("https://two.example/")) + check(!authority.admits(root)) + authority.disconnect() // Renderer death uses the same revocation. + check(!authority.admits("https://two.example/")) + check(!authority.commit(afterReturn, root)) + println("ConnectionAuthority: readiness, concurrent selection, return/replacement, renderer revocation, and 307/308 non-GET regression checks passed") +} diff --git a/packages/mobile-app/tests/native/TransportPolicyTest.java b/packages/mobile-app/tests/native/TransportPolicyTest.java new file mode 100644 index 000000000..397e57812 --- /dev/null +++ b/packages/mobile-app/tests/native/TransportPolicyTest.java @@ -0,0 +1,79 @@ +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import javax.xml.parsers.DocumentBuilderFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +// Parses the production XML, optionally the real Gradle-merged manifest. +// This is a configuration regression, NOT a WebView/network execution test. +public final class TransportPolicyTest { + private static final String ANDROID = "http://schemas.android.com/apk/res/android"; + private static final String RESOURCE = "codenomad_mobile_network_security"; + + private static void require(boolean value, String message) { + if (!value) throw new AssertionError(message); + } + + private static Document xml(Path file) throws Exception { + var factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + return factory.newDocumentBuilder().parse(file.toFile()); + } + + private static Element single(Document document, String tag) { + NodeList nodes = document.getElementsByTagName(tag); + require(nodes.getLength() == 1, "Exactly one " + tag); + return (Element) nodes.item(0); + } + + public static void main(String[] args) throws Exception { + Path main = Path.of("src-tauri/recovery/android/src/main"); + Path manifest = args.length == 0 ? main.resolve("AndroidManifest.xml") : Path.of(args[0]); + Path policyPath = args.length < 2 ? main.resolve("res/xml/" + RESOURCE + ".xml") : Path.of(args[1]); + String reference = args.length < 3 ? "@xml/" + RESOURCE : args[2]; + var application = single(xml(manifest), "application"); + require(application.getAttributeNS(ANDROID, "networkSecurityConfig").equals(reference), + "Production/merged application must use owned NSC"); + var policy = xml(policyPath); + require(policy.getDocumentElement().getTagName().equals("network-security-config"), "NSC root"); + var base = single(policy, "base-config"); + require(base.getAttribute("cleartextTrafficPermitted").equals("false"), "Base must deny cleartext"); + var domainConfig = single(policy, "domain-config"); + require(domainConfig.getAttribute("cleartextTrafficPermitted").equals("false"), "Loopback must deny cleartext"); + require(single(policy, "certificates").getAttribute("src").equals("system"), "System CA trust only"); + require(single(policy, "trust-anchors").getParentNode() == base, "Loopback inherits system trust"); + require(policy.getElementsByTagName("debug-overrides").getLength() == 0, "No debug trust override"); + require(!Files.exists(main.resolve("res/xml/" + RESOURCE + "_debug.xml")), "No implicit debug resource"); + NodeList domains = policy.getElementsByTagName("domain"); + var expected = List.of("localhost", "ip6-localhost", "127.0.0.1", "::1"); + require(domains.getLength() == expected.size(), "Only explicit denying loopback rules"); + for (int i = 0; i < domains.getLength(); i++) { + var domain = (Element) domains.item(i); + require(domain.getTextContent().trim().equals(expected.get(i)), "Explicit domain " + expected.get(i)); + require(domain.getParentNode() == domainConfig, "Every domain uses denial"); + if (i < 2) require(domain.getAttribute("includeSubdomains").equals("true"), "Localhost subdomains denied"); + } + // Android 17 XmlConfigSource.isLocalhostDefined suppresses the implicit + // fallback globally when ANY loopback domain is present. Thus other + // loopback spellings inherit this false base, not an implicit true rule. + for (String host : List.of("localhost", "LOCALHOST.", "ip6-localhost", "127.0.0.1", + "127.0.0.2", "127.255.255.254", "[::1]", "::1", "0:0:0:0:0:0:0:1", + "[0:0:0:0:0:0:0:1]", "tauri.localhost", "ipc.localhost", "server.example")) { + String normalized = host.toLowerCase(java.util.Locale.ROOT).replaceAll("\\.$", ""); + if (normalized.startsWith("[")) normalized = normalized.substring(1, normalized.length() - 1); + Element effective = base; + for (int i = 0; i < domains.getLength(); i++) { + var domain = (Element) domains.item(i); + String name = domain.getTextContent().trim(); + if (normalized.equals(name) || (domain.getAttribute("includeSubdomains").equals("true") && + normalized.endsWith("." + name))) effective = (Element) domain.getParentNode(); + } + require(effective.getAttribute("cleartextTrafficPermitted").equals("false"), "XML rule for " + host); + } + System.out.println("Production XML/manifest: base and loopback cleartext denied, system CA only; " + manifest); + System.out.println("Structural variants covered; no claim of real WebView/network absence."); + } +} diff --git a/packages/mobile-app/tests/native/ios-abi-guard/Cargo.lock b/packages/mobile-app/tests/native/ios-abi-guard/Cargo.lock new file mode 100644 index 000000000..37afb784c --- /dev/null +++ b/packages/mobile-app/tests/native/ios-abi-guard/Cargo.lock @@ -0,0 +1,93 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "codenomad-ios-abi-guard-tests" +version = "2.12.1" +dependencies = [ + "sha2", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" diff --git a/packages/mobile-app/tests/native/ios-abi-guard/Cargo.toml b/packages/mobile-app/tests/native/ios-abi-guard/Cargo.toml new file mode 100644 index 000000000..71e9ca86f --- /dev/null +++ b/packages/mobile-app/tests/native/ios-abi-guard/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "codenomad-ios-abi-guard-tests" +version = "2.12.1" +edition = "2021" +publish = false + +[workspace] + +[dependencies] +sha2 = "=0.10.9" + +[[test]] +name = "integration" +path = "integration.rs" diff --git a/packages/mobile-app/tests/native/ios-abi-guard/integration.rs b/packages/mobile-app/tests/native/ios-abi-guard/integration.rs new file mode 100644 index 000000000..62323d1c5 --- /dev/null +++ b/packages/mobile-app/tests/native/ios-abi-guard/integration.rs @@ -0,0 +1,47 @@ +#[path = "../../../src-tauri/vendor/tauri-2.12.1/codenomad_ios_abi_guard.rs"] +mod guard; + +use std::{fs, path::PathBuf}; + +fn vendor() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../../src-tauri/vendor/tauri-2.12.1") + .canonicalize() + .unwrap() +} + +#[test] +fn native_guard_accepts_the_real_integrated_sources() { + guard::verify_at(&vendor(), "2.12.1"); +} + +#[test] +fn native_guard_rejects_version_drift() { + assert!(std::panic::catch_unwind(|| guard::verify_at(&vendor(), "2.12.2")).is_err()); +} + +#[test] +fn native_guard_rejects_original_swift_and_arbitrary_source_drift() { + let original = vendor(); + let copy = original.join("../../../test-results/ios-abi/native-negative"); + assert!(!copy.exists(), "no shared or prior negative fixture overwrite"); + fs::create_dir_all(©).unwrap(); + for line in include_str!("../../../src-tauri/vendor/tauri-2.12.1/codenomad-ios-abi.sha256").lines() { + let (_, name) = line.split_once(" ").unwrap(); + let destination = copy.join(name); + fs::create_dir_all(destination.parent().unwrap()).unwrap(); + fs::copy(original.join(name), destination).unwrap(); + } + guard::verify_at(©, "2.12.1"); + let swift = copy.join("mobile/ios-api/Sources/Tauri/Tauri.swift"); + let patched = fs::read_to_string(&swift).unwrap(); + let upstream = patched.replace("id: Int32,", "id: Int,") + .replace("(Int32, Int32, UnsafePointer)", "(Int, Bool, UnsafePointer)") + .replace("callback(id, success ? 1 : 0,", "callback(id, success,"); + fs::write(&swift, upstream).unwrap(); + assert!(std::panic::catch_unwind(|| guard::verify_at(©, "2.12.1")).is_err()); + fs::write(&swift, patched).unwrap(); + fs::write(copy.join("src/ios.rs"), "drift").unwrap(); + assert!(std::panic::catch_unwind(|| guard::verify_at(©, "2.12.1")).is_err()); + fs::remove_dir_all(copy).unwrap(); +} diff --git a/packages/mobile-app/tests/native/verify-embedded-assets.mjs b/packages/mobile-app/tests/native/verify-embedded-assets.mjs new file mode 100644 index 000000000..b4886d828 --- /dev/null +++ b/packages/mobile-app/tests/native/verify-embedded-assets.mjs @@ -0,0 +1,48 @@ +import assert from "node:assert/strict" +import crypto from "node:crypto" +import fs from "node:fs" +import path from "node:path" +import zlib from "node:zlib" +import { fileURLToPath } from "node:url" + +const mobile = fileURLToPath(new URL("../..", import.meta.url)) +const [library, output] = process.argv.slice(2) +assert(library && output, "Usage: node verify-embedded-assets.mjs ") +const binary = fs.readFileSync(library) +assert.equal(binary.readUInt16LE(18), 183, "ELF machine must be AArch64") +assert.equal(binary[4], 2, "ELF must be 64 bit") +const linked = fs.readFileSync(path.join(mobile, "src-tauri/target/aarch64-linux-android/debug/libcodenomad_mobile_lib.so")) +assert(binary.equals(linked), "Packaged library must equal actual current linked Rust output") +const build = path.join(mobile, "src-tauri/target/aarch64-linux-android/debug/build") +const blobs = fs.readdirSync(build).filter(name => name.startsWith("codenomad-mobile-")).flatMap(name => { + const generated = path.join(build, name, "out/tauri-codegen-assets") + if (!fs.existsSync(generated)) return [] + return fs.readdirSync(generated).map(file => { + const packed = fs.readFileSync(path.join(generated, file)) + let decoded + try { decoded = zlib.brotliDecompressSync(packed) } catch { decoded = packed } + return { name: file, packed, decoded } + }) +}) +const assets = path.join(mobile, "dist") +const verified = [] +function walk(directory) { + for (const item of fs.readdirSync(directory, { withFileTypes: true })) { + const file = path.join(directory, item.name) + if (item.isDirectory()) { walk(file); continue } + const raw = fs.readFileSync(file) + const relative = path.relative(assets, file).replaceAll("\\", "/") + const references = [...raw.toString().matchAll(/(?:src|href)="([^"]+)"/g)].map(match => match[1]) + const blob = blobs.find(candidate => binary.includes(candidate.packed) && (relative === "index.html" + ? candidate.name.endsWith(".html") && references.every(reference => candidate.decoded.toString().includes(reference)) && + candidate.decoded.toString().includes('
') + : candidate.decoded.equals(raw))) + assert(blob, `Current launcher bytes missing from packaged native library: ${relative}`) + verified.push({ asset: relative, sha256: crypto.createHash("sha256").update(raw).digest("hex"), + embeddedBytes: blob.packed.length, offset: binary.indexOf(blob.packed) }) + } +} +walk(assets) +assert.equal(verified.length, 13, "Expected index, CSS, main JS and ten locale assets") +fs.writeFileSync(output, JSON.stringify(verified, null, 2) + "\n") +console.log("All 13 current launcher assets verified inside packaged AArch64 library; JS/CSS exact bytes, augmented HTML current references/root; library equals current Rust output.") diff --git a/packages/mobile-app/tests/native/verify-transport-apk.ps1 b/packages/mobile-app/tests/native/verify-transport-apk.ps1 new file mode 100644 index 000000000..e7e4edb39 --- /dev/null +++ b/packages/mobile-app/tests/native/verify-transport-apk.ps1 @@ -0,0 +1,84 @@ +param( + [Parameter(Mandatory)][string]$Apk, + [Parameter(Mandatory)][string]$Evidence +) +$ErrorActionPreference = 'Stop' +$Apk = (Resolve-Path $Apk).Path +New-Item -ItemType Directory -Force $Evidence | Out-Null +$Evidence = (Resolve-Path $Evidence).Path +$mobile = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path + +function Capture([string]$Name, [string]$Tool, [string[]]$Arguments) { + $output = & $Tool @Arguments 2>&1 + if ($LASTEXITCODE -ne 0) { throw "$Tool failed: $output" } + $output | Set-Content -Encoding utf8 (Join-Path $Evidence $Name) + return ($output -join "`n") +} + +$manifestTree = Capture 'aapt-manifest.log' 'aapt' @('dump', 'xmltree', $Apk, 'AndroidManifest.xml') +$resources = Capture 'aapt-resources.log' 'aapt' @('dump', 'resources', $Apk) +$resource = [regex]::Match($resources, 'spec resource (0x[0-9a-f]+) [^\s]+:xml/codenomad_mobile_network_security:') +if (!$resource.Success) { throw 'Owned NSC resource missing' } +$id = $resource.Groups[1].Value +if ($manifestTree -notmatch "android:networkSecurityConfig\([^)]*\)=@$id") { throw 'Manifest NSC mismatch' } +Capture 'packaged-manifest.xml' 'apkanalyzer.bat' @('manifest', 'print', $Apk) | Out-Null +$policyTree = Capture 'aapt-network-security.log' 'aapt' @('dump', 'xmltree', $Apk, 'res/xml/codenomad_mobile_network_security.xml') +# SDK apkanalyzer's resources xml drops domain text nodes. Reconstruct only the +# strict NSC E/A/C grammar from aapt (which preserves them); never accept a +# blank-domain decode or substitute the source XML for the packaged resource. +$document = [System.Xml.XmlDocument]::new() +$document.AppendChild($document.CreateXmlDeclaration('1.0', 'utf-8', $null)) | Out-Null +$stack = @{} +$current = $null +foreach ($line in ($policyTree -split "`n")) { + if ($line -match '^( *)E: ([a-z-]+) \(line=\d+\)$') { + $depth = $Matches[1].Length / 2 + $element = $document.CreateElement($Matches[2]) + if ($depth -eq 0) { $document.AppendChild($element) | Out-Null } + else { $stack[$depth - 1].AppendChild($element) | Out-Null } + $stack[$depth] = $element + $current = $element + } elseif ($line -match '^ *A: ([a-zA-Z]+)=\(type 0x12\)(0x0|0xffffffff)$') { + $current.SetAttribute($Matches[1], $(if ($Matches[2] -eq '0x0') { 'false' } else { 'true' })) + } elseif ($line -match '^ *A: ([a-zA-Z]+)="([^"]*)" \(Raw: "[^"]*"\)$') { + $current.SetAttribute($Matches[1], $Matches[2]) + } elseif ($line -match '^ *C: ("[^"]*")$') { + $current.AppendChild($document.CreateTextNode(($Matches[1] | ConvertFrom-Json))) | Out-Null + } elseif ($line.Trim()) { throw "Unrecognized packaged NSC tree line: $line" } +} +$document.Save((Join-Path $Evidence 'packaged-network-security.xml')) + +Push-Location $mobile +try { + Capture 'transport-packaged-xml.log' 'java' @('--source', '17', 'tests/native/TransportPolicyTest.java', + (Join-Path $Evidence 'packaged-manifest.xml'), (Join-Path $Evidence 'packaged-network-security.xml'), "@ref/$id") | Out-Null +} finally { Pop-Location } + +Add-Type -AssemblyName System.IO.Compression.FileSystem +$zip = [IO.Compression.ZipFile]::OpenRead($Apk) +try { + $zip.Entries | ForEach-Object { "$($_.FullName)`t$($_.Length)" } | + Set-Content (Join-Path $Evidence 'apk-contents.tsv') + if ($zip.Entries.FullName -contains 'res/xml/codenomad_mobile_network_security_debug.xml') { throw 'Unexpected debug NSC' } + $native = @($zip.Entries | Where-Object { $_.FullName -match '^lib/.*\.so$' }) + if ($native.Count -ne 1 -or $native[0].FullName -ne 'lib/arm64-v8a/libcodenomad_mobile_lib.so') { throw 'ARM64-only native inventory changed' } + if (@($zip.Entries | Where-Object { $_.FullName -match '(^|/)(node|node_modules|opencode|backend)(/|\.|$)' }).Count) { throw 'Unexpected desktop resources' } + [IO.Compression.ZipFileExtensions]::ExtractToFile($native[0], (Join-Path $Evidence 'packaged-libcodenomad_mobile_lib.so'), $true) +} finally { $zip.Dispose() } + +$code = Capture 'packaged-recovery-load.log' 'apkanalyzer.bat' @('dex', 'code', '--class', + 'ai.neuralnomads.codenomad.recovery.RecoveryPlugin', $Apk) +$load = [regex]::Match($code, '(?s)\.method public load\(Landroid/webkit/WebView;\)V.*?\.end method').Value +if ($load -notmatch 'const/4 (v\d+), 0x1\s+invoke-virtual \{v\d+, \1\}, Landroid/webkit/WebSettings;->setMixedContentMode\(I\)V') { + throw 'Packaged mixed-content denial not found; inspect DEX before accepting APK' +} +foreach ($name in @('ConnectionAuthority', 'NavigationFence')) { + Capture "packaged-$name.log" 'apkanalyzer.bat' @('dex', 'code', '--class', + "ai.neuralnomads.codenomad.recovery.$name", $Apk) | Out-Null +} +Capture 'apksigner.log' 'apksigner.bat' @('verify', '--verbose', '--print-certs', $Apk) | Out-Null +Capture 'zipalign.log' 'zipalign' @('-c', '-P', '16', '-v', '4', $Apk) | Out-Null +$hash = (Get-FileHash $Apk -Algorithm SHA256).Hash.ToLowerInvariant() +"$hash $([IO.Path]::GetFileName($Apk))" | Set-Content (Join-Path $Evidence 'SHA256SUMS.txt') +Write-Output "Verified packaged NSC $id, XML denial/system trust, mixed-content DEX, recovery classes, ARM64 inventory, signature and ZIP alignment. SHA256 $hash" +Write-Output 'Static artifact inspection only: no device, WebView network, login/upload/SSE or renderer-recovery qualification.' diff --git a/packages/mobile-app/tests/urls.json b/packages/mobile-app/tests/urls.json new file mode 100644 index 000000000..2fda6246d --- /dev/null +++ b/packages/mobile-app/tests/urls.json @@ -0,0 +1,25 @@ +{ + "valid": [ + ["https://example.com", "https://example.com/"], + ["HTTPS://EXAMPLE.COM:443/", "https://example.com/"], + ["https://server.example:8443/", "https://server.example:8443/"], + ["https://192.168.1.10:8443", "https://192.168.1.10:8443/"], + ["https://[fd00::2]:8443/", "https://[fd00::2]:8443/"], + ["https://xn--bcher-kva.example", "https://xn--bcher-kva.example/"], + ["https://tauri.localhost.example", "https://tauri.localhost.example/"] + ], + "invalid": [ + "", " example.com", " https://example.com", "https://example.com ", "🔒https://example.com", "éééééhttps://example.com", + "http://example.com", "javascript:alert(1)", "file:///index.html", "//example.com", + "https:example.com", "https:///example.com", "https://example.com//", + "https://example.com/path", "https://example.com/.", "https://example.com/a/../", + "https://example.com/%2e", "https://example.com?", "https://example.com/#", + "https://example.com?token=secret", "https://example.com/#secret", + "https://user:secret@example.com", "https://example.com@evil.com", + "https://example.com\\@evil.com", "https://%65xample.com", "https://bücher.example", + "https://example.com\n", "https://example.com\t", "https://example.com.", + "https://localhost", "https://tauri.localhost", "https://sub.localhost", "https://tauri", + "https://127.0.0.1", "https://127.1", "https://2130706433", "https://0x7f000001", + "https://[::1]", "https://0.0.0.0", "https://example.com:99999", "https://" + ] +} diff --git a/packages/mobile-app/tsconfig.json b/packages/mobile-app/tsconfig.json new file mode 100644 index 000000000..1f509a9ef --- /dev/null +++ b/packages/mobile-app/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "preserve", + "jsxImportSource": "solid-js", + "strict": true, + "noEmit": true, + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + "types": ["vite/client", "node"], + "skipLibCheck": true + }, + "include": ["src", "tests", "vite.config.ts"] +} diff --git a/packages/mobile-app/vite.config.ts b/packages/mobile-app/vite.config.ts new file mode 100644 index 000000000..968807c4d --- /dev/null +++ b/packages/mobile-app/vite.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vite" +import solid from "vite-plugin-solid" + +export default defineConfig({ + plugins: [solid()], + clearScreen: false, + build: { target: "es2020" }, +}) diff --git a/packages/server/src/previews/websocket-response.test.ts b/packages/server/src/previews/websocket-response.test.ts new file mode 100644 index 000000000..fa7c25d6f --- /dev/null +++ b/packages/server/src/previews/websocket-response.test.ts @@ -0,0 +1,126 @@ +import assert from "node:assert/strict" +import { createHash } from "node:crypto" +import { once } from "node:events" +import { Readable, Writable } from "node:stream" +import { finished, pipeline } from "node:stream/promises" +import { it } from "node:test" +import { PreviewWebSocketResponse } from "./websocket-response" + +const key = "dGhlIHNhbXBsZSBub25jZQ==" +const accept = createHash("sha1").update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest("base64") +const header = `HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: keep-alive, Upgrade\r\nSec-WebSocket-Accept: ${accept}\r\n` +const limits = { maxHeaderBytes: 16 * 1024, timeoutMs: 1000 } + +async function collect(chunks: Buffer[], options = limits, requestKey: string | undefined = key) { + const response = new PreviewWebSocketResponse(requestKey, options) + const output: Buffer[] = [] + response.on("data", (chunk: Buffer) => output.push(chunk)) + const completion = finished(response) + for (const chunk of chunks) response.write(chunk) + response.end() + await completion + return Buffer.concat(output) +} + +it("admits fragmented upgrade headers, strips every cookie copy and preserves negotiation headers and coalesced frames", async () => { + const raw = `${header}Set-Cookie: codenomad_session=bad; HttpOnly; Path=/\r\nsEt-CoOkIe: another=bad\r\nSET-COOKIE2: legacy=bad\r\nSec-WebSocket-Protocol: fixture\r\nSec-WebSocket-Extensions: permessage-deflate\r\nX-Harmless: preserved\r\n\r\n` + const frames = Buffer.from([0x81, 2, 0x68, 0x69, 0x88, 0]) + const chunks = [...Buffer.from(raw).subarray(0, -1)].map((byte) => Buffer.from([byte])) + chunks.push(Buffer.concat([Buffer.from("\n"), frames])) + const result = await collect(chunks) + const split = result.indexOf("\r\n\r\n") + 4 + assert.doesNotMatch(result.subarray(0, split).toString(), /set-cookie/i) + assert.match(result.toString(), /Sec-WebSocket-Protocol: fixture/) + assert.match(result.toString(), /Sec-WebSocket-Extensions: permessage-deflate/) + assert.match(result.toString(), /X-Harmless: preserved/) + assert.deepEqual(result.subarray(split), frames) +}) + +it("rejects non-101, malformed, folded and invalid/duplicate acceptance headers before emitting anything", async () => { + const invalid = [ + "HTTP/1.1 302 Found\r\nLocation: http://untrusted.invalid/\r\nSet-Cookie: auth=bad\r\n\r\n", + "HTTP/1.1 200 OK\r\n\r\n", + `${header} broken-fold: value\r\n\r\n`, + `${header}Bad Name: value\r\n\r\n`, + `${header}missing-colon\r\n\r\n`, + `${header}X-Control: bad\u0000value\r\n\r\n`, + `${header}X-LF: bad\nSet-Cookie: auth=bad\r\n\r\n`, + `${header}Sec-WebSocket-Accept: ${accept}\r\n\r\n`, + `${header.replace(accept, "wrong")}\r\n`, + `${header.replace("Upgrade: websocket", "Upgrade: not-websocket")}\r\n`, + `${header.replace("Connection: keep-alive, Upgrade", "Connection: close")}\r\n`, + `${header.replace("HTTP/1.1", "HTTP/1.0")}\r\n`, + ] + for (const source of invalid) { + const response = new PreviewWebSocketResponse(key, limits) + let leaked = false + response.on("data", () => { leaked = true }) + const completion = finished(response) + response.end(Buffer.from(source)) + await assert.rejects(completion) + assert.equal(leaked, false) + assert.equal(response.admitted, false) + } +}) + +it("bounds only the response header, not legitimate frames arriving in the same chunk", async () => { + const valid = Buffer.from(`${header}\r\n`) + const frames = Buffer.alloc(100_000, 0xab) + const result = await collect([Buffer.concat([valid, frames])], { maxHeaderBytes: valid.length, timeoutMs: 1000 }) + assert.deepEqual(result.subarray(valid.length), frames) + const fragmented = await collect([valid.subarray(0, -2), Buffer.concat([valid.subarray(-2), frames])], { maxHeaderBytes: valid.length, timeoutMs: 1000 }) + assert.deepEqual(fragmented, Buffer.concat([valid, frames])) + await assert.rejects(collect([valid], { maxHeaderBytes: valid.length - 1, timeoutMs: 1000 }), /too large/) + await assert.rejects(collect([Buffer.from(`${header}X-Large: ${"x".repeat(17_000)}`)]), /too large/) +}) + +it("fails closed on EOF, missing request identity, handshake deadline and cancellation", async () => { + await assert.rejects(collect([Buffer.from(header)]), /Incomplete/) + const missingKey = new PreviewWebSocketResponse(undefined, limits) + const missingCompletion = finished(missingKey) + missingKey.end(Buffer.from(`${header}\r\n`)) + await assert.rejects(missingCompletion, /Invalid/) + const stalled = new PreviewWebSocketResponse(key, { maxHeaderBytes: 1000, timeoutMs: 25 }) + const stalledError = once(stalled, "error") + // Keep the unit fixture alive while the production deadline is unreferenced. + const keepAlive = setInterval(() => {}, 1000) + try { + assert.match(String((await stalledError)[0]), /timed out/) + assert.equal(stalled.destroyed, true) + } finally { clearInterval(keepAlive) } + const cancelled = new PreviewWebSocketResponse(key, { maxHeaderBytes: 1000, timeoutMs: 25 }) + let error = false + cancelled.on("error", () => { error = true }) + const close = once(cancelled, "close") + cancelled.destroy() + await close + await new Promise((resolve) => setTimeout(resolve, 40)) + assert.equal(error, false, "cancellation must clear the handshake deadline") +}) + +it("retains streaming backpressure and drains every frame before a graceful EOF", async () => { + const valid = Buffer.from(`${header}\r\n`) + const frames = Array.from({ length: 64 }, (_, index) => Buffer.alloc(32_768, index)) + const response = new PreviewWebSocketResponse(key, limits) + const source = Readable.from([valid, ...frames], { objectMode: false, highWaterMark: 16_384 }) + const output: Buffer[] = [] + let release!: () => void + let markBlocked!: () => void + const blocked = new Promise((resolve) => { markBlocked = resolve }) + let first = true + const sink = new Writable({ highWaterMark: 16_384, write(chunk: Buffer, _encoding, done) { + output.push(chunk) + if (first) { + first = false + release = done + markBlocked() + } else setImmediate(done) + } }) + const completion = pipeline(source, response, sink) + await blocked + assert.equal(source.readableEnded, false, "a blocked recipient must stop upstream consumption") + assert.ok(output.length < frames.length) + release() + await completion + assert.deepEqual(Buffer.concat(output), Buffer.concat([valid, ...frames])) +}) diff --git a/packages/server/src/previews/websocket-response.ts b/packages/server/src/previews/websocket-response.ts new file mode 100644 index 000000000..81aac3e58 --- /dev/null +++ b/packages/server/src/previews/websocket-response.ts @@ -0,0 +1,94 @@ +import { createHash } from "node:crypto" +import { Transform, type TransformCallback } from "node:stream" + +const MAX_HEADER_BYTES = 16 * 1024 +const HANDSHAKE_TIMEOUT_MS = 10_000 +const HEADER_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/ +const HEADER_VALUE = /^[\t\x20-\x7e\x80-\xff]*$/ + +/** The HTTP response is untrusted even when its TLS certificate is valid. + * Admit one bounded 101 response, then stream frames without interpreting them. + * The accepted event opens the client-to-upstream half of the tunnel. + */ +export class PreviewWebSocketResponse extends Transform { + private header = Buffer.alloc(0) + private readonly timer: ReturnType + private readonly expectedAccept: string | undefined + admitted = false + + constructor(requestKey: string | string[] | undefined, private readonly limits = { + maxHeaderBytes: MAX_HEADER_BYTES, + timeoutMs: HANDSHAKE_TIMEOUT_MS, + }) { + super() + this.expectedAccept = typeof requestKey === "string" + ? createHash("sha1").update(`${requestKey}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest("base64") + : undefined + this.timer = setTimeout(() => this.destroy(new Error("Preview WebSocket handshake timed out")), limits.timeoutMs) + this.timer.unref() + } + + override _transform(chunk: Buffer, _encoding: BufferEncoding, callback: TransformCallback) { + if (this.admitted) { + callback(null, chunk) + return + } + const previousLength = this.header.length + const candidate = Buffer.concat([this.header, chunk.subarray(0, this.limits.maxHeaderBytes - previousLength)]) + const end = candidate.indexOf("\r\n\r\n") + if (end < 0) { + this.header = candidate + callback(candidate.length >= this.limits.maxHeaderBytes ? new Error("Preview WebSocket response headers too large") : undefined) + return + } + try { + const safeHeaders = this.admit(candidate.subarray(0, end + 4)) + this.header = Buffer.alloc(0) + this.admitted = true + clearTimeout(this.timer) + this.push(safeHeaders) + // Do not lose frames coalesced with the final fragment of the headers. + const remainder = chunk.subarray(end + 4 - previousLength) + if (remainder.length) this.push(remainder) + this.emit("accepted") + callback() + } catch (error) { + callback(error as Error) + } + } + + private admit(header: Buffer): Buffer { + const [status, ...lines] = header.toString("latin1").slice(0, -4).split("\r\n") + if (!/^HTTP\/1\.1 101(?: [\x20-\x7e]*)?$/.test(status)) throw new Error("Preview WebSocket upstream did not switch protocols") + const fields = new Map() + const safe = [status] + for (const line of lines) { + const colon = line.indexOf(":") + const name = line.slice(0, colon) + const value = line.slice(colon + 1) + if (colon <= 0 || !HEADER_NAME.test(name) || !HEADER_VALUE.test(value)) throw new Error("Malformed preview WebSocket response header") + const lower = name.toLowerCase() + const values = fields.get(lower) ?? [] + values.push(value.trim()) + fields.set(lower, values) + if (lower !== "set-cookie" && lower !== "set-cookie2") safe.push(line) + } + const tokens = (name: string) => fields.get(name)?.join(",").toLowerCase().split(",").map((value) => value.trim()) ?? [] + const accept = fields.get("sec-websocket-accept") + if (!tokens("upgrade").includes("websocket") || !tokens("connection").includes("upgrade") + || !this.expectedAccept || accept?.length !== 1 || accept[0] !== this.expectedAccept) { + throw new Error("Invalid preview WebSocket upgrade response") + } + return Buffer.from(`${safe.join("\r\n")}\r\n\r\n`, "latin1") + } + + override _flush(callback: TransformCallback) { + callback(this.admitted ? undefined : new Error("Incomplete preview WebSocket upgrade response")) + } + + override _destroy(error: Error | null, callback: (error?: Error | null) => void) { + clearTimeout(this.timer) + this.header = Buffer.alloc(0) + callback(error) + } +} diff --git a/packages/server/src/server/__tests__/fixtures/preview-security.ts b/packages/server/src/server/__tests__/fixtures/preview-security.ts new file mode 100644 index 000000000..99f3a6be3 --- /dev/null +++ b/packages/server/src/server/__tests__/fixtures/preview-security.ts @@ -0,0 +1,423 @@ +import assert from "node:assert/strict" +import { createHash } from "node:crypto" +import fs from "node:fs" +import { createServer as createHttpServer, type IncomingHttpHeaders, type IncomingMessage, type ServerResponse } from "node:http" +import { createServer as createHttpsServer } from "node:https" +import { connect, type Socket } from "node:net" +import path from "node:path" +import { Readable } from "node:stream" +import { after, before, it } from "node:test" +import { AuthManager } from "../../../auth/manager" +import type { Logger } from "../../../logger" +import { PreviewManager } from "../../../previews/manager" +import { createHttpServer as createCodeNomadServer } from "../../http-server" + +const directory = process.env.PREVIEW_SECURITY_DIRECTORY! +assert.ok(directory, "run through preview-security.test.ts with disposable certificates") +const certificates = JSON.parse(fs.readFileSync(path.join(directory, "certificates.json"), "utf8")) as Record +const logger = { child: () => logger, debug() {}, info() {}, warn() {}, error() {}, trace() {}, isLevelEnabled: () => false } as unknown as Logger +const previews = new PreviewManager() +const auth = new AuthManager({ configPath: path.join(directory, "config.yaml"), username: "test", password: "fixture-password-only", generateToken: false }, logger) +const session = auth.createSession("test") +const cookie = `${auth.getCookieName()}=${session.id}; harmless_app_cookie=fixture` +const credentials = { cookie, cookie2: "legacy_fixture_cookie=value", authorization: "Bearer fixture-only", "proxy-authorization": "Basic fixture-only" } +const seen: { url: string; headers: IncomingHttpHeaders; kind: string; socket: Socket }[] = [] +const servers: ReturnType[] = [] +const sockets = new Set() +const targets = new Map() +const clientFrame = Buffer.from([0x81, 0x83, 1, 2, 3, 4, 0x60, 0x60, 0x60]) +let echoInput = Buffer.alloc(0) +let markAbortStarted: ((socket: Socket) => void) | undefined +let blockedSource: Readable | undefined +const bulkFrames = Array.from({ length: 64 }, (_, index) => Buffer.concat([Buffer.from([0x82, 126, 0x80, 0]), Buffer.alloc(32_768, index)])) +let app: ReturnType +let port: number + +function assertSanitized(headers: IncomingHttpHeaders) { + for (const name of Object.keys(credentials)) assert.equal(headers[name], undefined, `${name} must never reach a preview`) + assert.equal(headers["x-preview-test"], "preserved") + assert.equal(headers["accept"], "application/json") +} + +function waitForPeerClose(socket: Socket): Promise { + if (socket.destroyed) return Promise.resolve() + return new Promise((resolve, reject) => { + const onClose = () => { clearTimeout(timer); resolve() } + const timer = setTimeout(() => { + socket.off("close", onClose) + reject(new Error("owned upstream peer did not close after cancellation")) + }, 1000) + // A reset is expected when cancelling while frames remain queued. Wait for + // close, not events.once(), which rejects on the preceding ECONNRESET. + socket.once("close", onClose) + }) +} + +before(async () => { + const handler = (kind: string) => async (request: IncomingMessage, response: ServerResponse) => { + const chunks: Buffer[] = [] + for await (const chunk of request) chunks.push(Buffer.from(chunk)) + seen.push({ url: request.url!, headers: request.headers, kind, socket: request.socket }) + if (request.url === "/redirect") { + response.writeHead(302, { location: "/final", "set-cookie": "untrusted=fixture; Path=/" }).end() + } else if (request.url === "/absolute-redirect") { + response.writeHead(302, { location: `${targets.get(kind)}/final` }).end() + } else if (request.url === "/cross-origin-redirect") { + response.writeHead(302, { location: `${targets.get("valid")}/final` }).end() + } else if (request.url === "/safe-external-redirect") { + response.writeHead(302, { location: "https://public-preview.invalid/final" }).end() + } else if (request.url === "/relative-redirect/page") { + response.writeHead(302, { location: "../../../final?keep=1#section" }).end() + } else if (request.url === "/backslash-redirect") { + response.writeHead(302, { location: targets.get("valid")!.replaceAll("/", "\\") + "\\final" }).end() + } else if (request.url === "/refresh") { + response.writeHead(200, { refresh: `0; url=${targets.get("valid")}/final` }).end() + } else { + response.setHeader("content-type", "application/json") + response.end(JSON.stringify({ url: request.url, headers: request.headers, method: request.method, body: Buffer.concat(chunks).toString() })) + } + } + for (const kind of ["http", ...Object.keys(certificates)]) { + const server = kind === "http" ? createHttpServer(handler(kind)) : createHttpsServer(certificates[kind], handler(kind)) + servers.push(server) + server.on("connection", (socket) => { + sockets.add(socket) + socket.on("close", () => sockets.delete(socket)) + socket.on("error", () => {}) + }) + server.on("upgrade", (request, socket) => { + // The owned peer acknowledges TCP EOF. Node's upgraded HTTP sockets are + // half-open by default; retaining our fixture's write half is not a proxy + // resource leak and would make remote-close assertions meaningless. + socket.once("end", () => socket.end()) + seen.push({ url: request.url!, headers: request.headers, kind, socket: request.socket }) + const accept = createHash("sha1").update(`${request.headers["sec-websocket-key"]}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest("base64") + const base = `HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${accept}\r\n` + const cookies = `Set-Cookie: ${auth.getCookieName()}=malicious; HttpOnly; Path=/\r\nsEt-CoOkIe: other=bad; Path=/\r\nSET-COOKIE2: legacy=bad\r\n` + if (request.url === "/fragmented-ws") { + socket.write(`${base}${cookies}\r`) + setImmediate(() => socket.end("\nfixture-upgrade")) + } else if (request.url === "/malicious-ws") { + socket.end(`${base}${cookies}\r\nfixture-upgrade`) + } else if (request.url === "/redirect-ws") { + socket.end(`HTTP/1.1 302 Found\r\n${cookies}Location: ${targets.get("valid")}/socket\r\n\r\n`) + } else if (request.url === "/oversized-ws") { + socket.end(`${base}${cookies}X-Large: ${"x".repeat(17_000)}\r\n\r\n`) + } else if (request.url === "/malformed-ws") { + socket.end(`${base}${cookies} bad-folded: value\r\n\r\n`) + } else if (request.url === "/incomplete-ws") { + socket.end(`${base}${cookies}`) + } else if (request.url === "/stall-ws") { + socket.write(base) + } else if (request.url === "/abort-ws") { + socket.write(base) + markAbortStarted?.(request.socket) + } else if (request.url === "/close-ws") { + socket.destroy() + } else if (request.url === "/error-ws") { + socket.write(`${base}\r\nfixture-upgrade`) + setTimeout(() => socket.destroy(new Error("owned fixture upstream failure")), 20) + } else if (request.url === "/echo-ws") { + socket.write(Buffer.concat([Buffer.from(`${base}\r\n`), Buffer.from([0x81, 5]), Buffer.from("early")])) + socket.on("data", (chunk) => { + echoInput = Buffer.concat([echoInput, chunk]) + if (echoInput.length >= clientFrame.length) socket.end(Buffer.from([0x81, 4, ...Buffer.from("echo")])) + }) + } else if (request.url === "/bulk-ws") { + Readable.from([Buffer.from(`${base}\r\n`), ...bulkFrames], { objectMode: false }).pipe(socket) + } else if (request.url === "/disconnect-bulk-ws") { + blockedSource = Readable.from([Buffer.from(`${base}\r\n`), ...Array.from({ length: 512 }, (_, index) => bulkFrames[index % bulkFrames.length])], { objectMode: false }) + socket.once("close", () => blockedSource?.destroy()) + blockedSource.pipe(socket) + } else socket.end(`${base}\r\nfixture-upgrade`) + }) + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)) + const address = server.address() + assert.ok(address && typeof address === "object") + targets.set(kind, `${kind === "http" ? "http" : "https"}://127.0.0.1:${address.port}`) + } + // Unused integrations are inert mocks. The real production auth hook, proxy + // registration, fallback router, reply-from transport and upgrade listeners run. + app = createCodeNomadServer({ + bindHost: "127.0.0.1", bindPort: 0, defaultPort: 0, protocol: "http", + logger, authManager: auth, previewManager: previews, uiStaticDir: directory, + workspaceManager: {}, settings: {}, fileSystemBrowser: {}, eventBus: {}, serverMeta: {}, + instanceStore: {}, speechService: {}, sidecarManager: {}, clientConnectionManager: {}, + remoteProxySessionManager: {}, yoloManager: {}, nativeParent: {}, automationBridgeToken: "fixture-only", + } as unknown as Parameters[0]) + app.instance.server.on("connection", (socket) => { + sockets.add(socket) + socket.once("close", () => sockets.delete(socket)) + }) + port = (await app.start()).port +}) + +after(async () => { + for (const socket of sockets) socket.destroy() + await app?.stop() + await Promise.all(servers.map((server) => new Promise((resolve) => server.close(() => resolve())))) +}) + +function createPreview(kind: string) { + const preview = previews.create("fixture-session", targets.get(kind)!) + return { token: preview.token, base: previews.buildProxyBasePath(preview.token) } +} + +const headers = { ...credentials, host: "127.0.0.1", "x-preview-test": "preserved", accept: "application/json", origin: "http://codenomad-fixture.invalid" } + +it("strips all credentials on path, capability host, POST body and authenticated fallback routes", async () => { + const { token, base } = createPreview("http") + const responses = [ + await app.instance.inject({ url: `${base}/final?query=kept`, headers }), + await app.instance.inject({ url: "/final", headers: { ...headers, host: `${token}.preview.localhost:${port}` } }), + await app.instance.inject({ method: "POST", url: `${base}/final`, headers: { ...headers, "content-type": "text/plain" }, payload: "harmless payload" }), + await app.instance.inject({ url: "/fallback-asset.js", headers: { ...headers, referer: `http://127.0.0.1:${port}${base}/page` } }), + ] + for (const response of responses) { + assert.equal(response.statusCode, 200, response.body) + assertSanitized(response.json().headers) + assert.equal(response.json().headers.origin, targets.get("http")) + } + assert.equal(responses[0].json().url, "/final?query=kept") + assert.equal(responses[2].json().method, "POST") + assert.equal(responses[2].json().body, "harmless payload") +}) + +it("keeps backend auth and unknown/revoked capability fences", async () => { + const login = await app.instance.inject({ method: "POST", url: "/api/auth/login", payload: { username: "test", password: "fixture-password-only" } }) + assert.equal(login.statusCode, 200) + assert.match(String(login.headers["set-cookie"]), /; Path=\//) + const loginCookie = String(login.headers["set-cookie"]).split(";")[0] + const status = await app.instance.inject({ url: "/api/auth/status", headers: { cookie: loginCookie } }) + assert.equal(status.json().authenticated, true) + const loginPreview = createPreview("http") + const proxied = await app.instance.inject({ url: `${loginPreview.base}/final`, headers: { ...headers, cookie: loginCookie } }) + assert.equal(proxied.statusCode, 200) + assertSanitized(proxied.json().headers) + assert.equal((await app.instance.inject({ url: "/api/previews" })).statusCode, 401) + assert.equal((await app.instance.inject({ url: "/previews/unknown/final", headers: { accept: "application/json" } })).statusCode, 401) + assert.equal((await app.instance.inject({ url: "/previews/unknown/final", headers })).statusCode, 404) + const { token, base } = createPreview("http") + assert.equal((await app.instance.inject({ url: `${base}/final`, headers: { origin: "null" } })).statusCode, 200) + assert.equal((await app.instance.inject({ url: "/api/auth/status", headers: { origin: "null" } })).statusCode, 403) + previews.delete(token) + assert.equal((await app.instance.inject({ url: `${base}/final`, headers })).statusCode, 404) +}) + +it("does not follow upstream redirects and strips credentials again on browser follow-up", async () => { + const { base } = createPreview("http") + for (const suffix of ["redirect", "absolute-redirect"]) { + const before = seen.length + const response = await app.instance.inject({ url: `${base}/${suffix}`, headers }) + assert.equal(response.statusCode, 302) + assert.equal(seen.length, before + 1, "reply-from must not follow the redirect internally") + assertSanitized(seen.at(-1)!.headers) + assert.equal(response.headers.location, `${base}/final`) + assert.equal(response.headers["set-cookie"], undefined) + const follow = await app.instance.inject({ url: String(response.headers.location), headers }) + assert.equal(follow.statusCode, 200) + assertSanitized(follow.json().headers) + } + const before = seen.length + const cross = await app.instance.inject({ url: `${base}/cross-origin-redirect`, headers }) + assert.equal(cross.statusCode, 302) + assert.equal(cross.headers.location, undefined, "same-host/different-port redirects must not expose CodeNomad cookies to a browser") + assert.equal(seen.length, before + 1, "no second origin request is made by the backend") + const external = await app.instance.inject({ url: `${base}/safe-external-redirect`, headers }) + assert.equal(external.headers.location, undefined, "external redirects remain inert even behind a Host-rewriting reverse proxy") + const relative = await app.instance.inject({ url: `${base}/relative-redirect/page`, headers }) + assert.equal(relative.headers.location, `${base}/final?keep=1#section`, "relative redirects cannot escape the capability path") + const backslash = await app.instance.inject({ url: `${base}/backslash-redirect`, headers }) + assert.equal(backslash.headers.location, undefined) + const refresh = await app.instance.inject({ url: `${base}/refresh`, headers }) + assert.equal(refresh.headers.refresh, undefined) +}) + +it("does not reuse HTTP upstream sockets, containing the nested Undici response-queue advisory", async () => { + const { base } = createPreview("http") + const connections: Socket[] = [] + for (let index = 0; index < 3; index++) { + const response = await app.instance.inject({ url: `${base}/final`, headers }) + assert.equal(response.statusCode, 200) + assert.equal(seen.at(-1)!.headers.connection, "close") + connections.push(seen.at(-1)!.socket) + } + assert.equal(new Set(connections).size, 3) +}) + +it("HTTP accepts a trusted matching certificate but rejects expired, mismatched and untrusted certificates even with inherited TLS bypass", async () => { + for (const kind of Object.keys(certificates)) { + const { base } = createPreview(kind) + const before = seen.length + const response = await app.instance.inject({ url: `${base}/final`, headers }) + assert.equal(response.statusCode, kind === "valid" ? 200 : 502, `${kind}: ${response.body}`) + assert.equal(seen.length, before + (kind === "valid" ? 1 : 0), "invalid TLS must fail before HTTP headers") + if (kind === "valid") assertSanitized(response.json().headers) + } +}) + +function upgrade(url: string, host = `127.0.0.1:${port}`, extraHeaders: Record = credentials, initialFrame = Buffer.alloc(0)): Promise { + return new Promise((resolve, reject) => { + const socket = connect(port, "127.0.0.1") + let result = "" + socket.setTimeout(15_000, () => socket.destroy(new Error("upgrade timeout"))) + socket.once("error", reject) + socket.on("data", (chunk) => { result += chunk }) + socket.once("close", () => resolve(result)) + socket.once("connect", () => socket.write(Buffer.concat([Buffer.from([ + `GET ${url} HTTP/1.1`, `Host: ${host}`, "Connection: Upgrade", "Upgrade: websocket", + "Sec-WebSocket-Version: 13", "Sec-WebSocket-Key: Zml4dHVyZS10ZXN0LW9ubHk=", + "X-Preview-Test: preserved", "Accept: application/json", "Origin: http://codenomad-fixture.invalid", + ...Object.entries(extraHeaders).map(([name, value]) => `${name.toUpperCase()}: ${value}`), "", "", + ].join("\r\n")), initialFrame]))) + }) +} + +it("WebSocket path and capability-host upgrades strip credentials and preserve the handshake", async () => { + const { token, base } = createPreview("http") + for (const [url, host] of [[`${base}/socket?keep=1`, undefined], ["/socket?keep=1", `${token}.preview.localhost:${port}`]]) { + const response = await upgrade(url!, host) + assert.match(response, /^HTTP\/1\.1 101/) + assert.match(response, /fixture-upgrade/) + assert.equal(seen.at(-1)!.url, "/socket?keep=1") + assertSanitized(seen.at(-1)!.headers) + assert.equal(seen.at(-1)!.headers.origin, targets.get("http")) + assert.equal(seen.at(-1)!.headers["sec-websocket-version"], "13") + } + assert.match(await upgrade("/previews/unknown/socket", undefined, {}), /^HTTP\/1\.1 401/) + assert.match(await upgrade("/previews/unknown/socket"), /^HTTP\/1\.1 404/) +}) + +it("WebSocket TLS verifies trust, expiry and hostname before sending upgrade credentials", async () => { + for (const kind of Object.keys(certificates)) { + const { base } = createPreview(kind) + const before = seen.length + const response = await upgrade(`${base}/socket`) + assert.match(response, kind === "valid" ? /^HTTP\/1\.1 101/ : /^HTTP\/1\.1 502/, kind) + assert.equal(seen.length, before + (kind === "valid" ? 1 : 0), "invalid TLS must fail before the upgrade request") + if (kind === "valid") assertSanitized(seen.at(-1)!.headers) + } +}) + +it("production WebSocket response admission strips malicious and fragmented cookies without losing coalesced payload bytes", async () => { + const { base } = createPreview("http") + for (const path of ["malicious-ws", "fragmented-ws"]) { + const response = await upgrade(`${base}/${path}`) + assert.match(response, /^HTTP\/1\.1 101/) + assert.match(response, /fixture-upgrade$/) + assert.doesNotMatch(response, /set-cookie|malicious/i) + assert.match(response, /Sec-WebSocket-Accept:/) + } +}) + +it("production WebSocket response admission rejects redirects, oversized, malformed, truncated and closed upstream handshakes", async () => { + const { base } = createPreview("http") + for (const path of ["redirect-ws", "oversized-ws", "malformed-ws", "incomplete-ws", "close-ws"]) { + const response = await upgrade(`${base}/${path}`) + assert.equal(response, "HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\nContent-Length: 0\r\n\r\n") + const upstream = seen.at(-1)!.socket + await waitForPeerClose(upstream) + assert.equal(upstream.destroyed, true, "rejection must release the upstream connection") + } +}) + +it("production WebSocket tunnel preserves a client frame coalesced with the request and upstream early frames", async () => { + const { base } = createPreview("http") + echoInput = Buffer.alloc(0) + const response = await upgrade(`${base}/echo-ws`, undefined, credentials, clientFrame) + assert.match(response, /^HTTP\/1\.1 101/) + assert.match(response, /early/) + assert.match(response, /echo$/) + assert.deepEqual(echoInput, clientFrame, "request head bytes must be forwarded exactly once after admission") +}) + +it("production WebSocket handshake deadline closes a stalled upstream fail-closed", { timeout: 20_000 }, async () => { + const { base } = createPreview("http") + const start = Date.now() + const response = await upgrade(`${base}/stall-ws`) + assert.match(response, /^HTTP\/1\.1 502/) + assert.ok(Date.now() - start < 15_000) +}) + +it("production WebSocket tunnel drains a slow recipient without dropping frames at upstream EOF", async () => { + const { base } = createPreview("http") + const result = await new Promise((resolve, reject) => { + const socket = connect(port, "127.0.0.1") + const chunks: Buffer[] = [] + let delayed = false + socket.setTimeout(5000, () => socket.destroy(new Error("bulk response timeout"))) + socket.once("error", reject) + socket.on("data", (chunk) => { + chunks.push(chunk) + if (!delayed) { + delayed = true + socket.pause() + setTimeout(() => socket.resume(), 50) + } + }) + socket.once("close", () => resolve(Buffer.concat(chunks))) + socket.once("connect", () => socket.write(`GET ${base}/bulk-ws HTTP/1.1\r\nHost: 127.0.0.1:${port}\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n`)) + }) + const boundary = result.indexOf("\r\n\r\n") + 4 + assert.match(result.subarray(0, boundary).toString(), /^HTTP\/1\.1 101/) + assert.deepEqual(result.subarray(boundary), Buffer.concat(bulkFrames)) +}) + +it("production WebSocket tunnel closes both halves on client disconnect or upstream failure after admission", async () => { + const { base } = createPreview("http") + await new Promise((resolve, reject) => { + const socket = connect(port, "127.0.0.1") + socket.once("error", reject) + socket.setTimeout(5000, () => socket.destroy(new Error("disconnect fixture timeout"))) + socket.once("data", async (chunk) => { + try { + assert.match(chunk.toString(), /^HTTP\/1\.1 101/) + const upstream = seen.at(-1)!.socket + const closed = waitForPeerClose(upstream) + socket.destroy() + await closed + assert.equal(upstream.destroyed, true) + resolve() + } catch (error) { socket.destroy(); reject(error) } + }) + socket.once("connect", () => socket.write(`GET ${base}/echo-ws HTTP/1.1\r\nHost: 127.0.0.1:${port}\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n`)) + }) + const response = await upgrade(`${base}/error-ws`) + assert.match(response, /^HTTP\/1\.1 101/) + assert.doesNotMatch(response, /502 Bad Gateway/, "post-admission failures must not inject a second HTTP response into frames") +}) + +it("production WebSocket tunnel cancels pending admission and blocked streaming on client disconnect", async () => { + const { base } = createPreview("http") + const started = new Promise((resolve) => { markAbortStarted = resolve }) + const pending = connect(port, "127.0.0.1") + pending.on("error", () => {}) + pending.once("connect", () => pending.write(`GET ${base}/abort-ws HTTP/1.1\r\nHost: 127.0.0.1:${port}\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n`)) + try { + const upstream = await started + const closed = waitForPeerClose(upstream) + pending.destroy() + await closed + assert.equal(upstream.destroyed, true, "client cancellation must release a stalled upstream before its deadline") + } finally { pending.destroy(); markAbortStarted = undefined } + + await new Promise((resolve, reject) => { + const client = connect(port, "127.0.0.1") + client.once("error", reject) + client.setTimeout(5000, () => client.destroy(new Error("blocked disconnect fixture timeout"))) + client.once("data", async (chunk) => { + try { + assert.match(chunk.toString(), /^HTTP\/1\.1 101/) + client.pause() + assert.equal(blockedSource?.readableEnded, false, "fixture must still have queued frames when the client cancels") + const upstream = seen.at(-1)!.socket + const closed = waitForPeerClose(upstream) + client.destroy() + await closed + assert.equal(upstream.destroyed, true) + resolve() + } catch (error) { client.destroy(); reject(error) } + }) + client.once("connect", () => client.write(`GET ${base}/disconnect-bulk-ws HTTP/1.1\r\nHost: 127.0.0.1:${port}\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n`)) + }) +}) diff --git a/packages/server/src/server/__tests__/fixtures/preview-ws-browser.tsx b/packages/server/src/server/__tests__/fixtures/preview-ws-browser.tsx new file mode 100644 index 000000000..86fa3d926 --- /dev/null +++ b/packages/server/src/server/__tests__/fixtures/preview-ws-browser.tsx @@ -0,0 +1,18 @@ +import { render } from "solid-js/web" +import { BrowserFrame } from "../../../../../ui/src/components/browser-frame" +import { I18nProvider } from "../../../../../ui/src/lib/i18n" +import { ConfigProvider } from "../../../../../ui/src/stores/preferences" + +// Exercise the production hosted-preview surface, including its opaque-origin +// sandbox and real providers. Their preference reads are disposable test data; +// this fixture has no desktop bridge or daemon. +const query = new URLSearchParams(location.search) +const base = query.get("preview")! +render(() => address} +/>, document.getElementById("root")!) diff --git a/packages/server/src/server/__tests__/preview-security.test.ts b/packages/server/src/server/__tests__/preview-security.test.ts new file mode 100644 index 000000000..65bf02394 --- /dev/null +++ b/packages/server/src/server/__tests__/preview-security.test.ts @@ -0,0 +1,77 @@ +import assert from "node:assert/strict" +import { spawn } from "node:child_process" +import fs from "node:fs" +import path from "node:path" +import { it } from "node:test" +import { fileURLToPath } from "node:url" +import forge from "node-forge" + +// Trust only a disposable fixture CA in a child process: neither the parent nor +// the user's trust store, cookies, settings, or running daemon are modified. +it("isolates preview credentials and verifies HTTP/WebSocket TLS on real upstreams", { timeout: 120_000 }, async () => { + const tempRoot = process.env.LOCALAPPDATA + ? path.join(process.env.LOCALAPPDATA, "Temp", "opencode") + : process.env.TMPDIR ?? process.env.TEMP ?? "/tmp" + fs.mkdirSync(tempRoot, { recursive: true }) + const directory = fs.mkdtempSync(path.join(tempRoot, "preview-security-")) + try { + const caKeys = forge.pki.rsa.generateKeyPair(2048) + const ca = forge.pki.createCertificate() + ca.publicKey = caKeys.publicKey + ca.serialNumber = "01" + ca.validity.notBefore = new Date("2020-01-01T00:00:00Z") + ca.validity.notAfter = new Date("2040-01-01T00:00:00Z") + ca.setSubject([{ name: "commonName", value: "Disposable preview test CA" }]) + ca.setIssuer(ca.subject.attributes) + ca.setExtensions([{ name: "basicConstraints", cA: true }, { name: "keyUsage", keyCertSign: true }]) + ca.sign(caKeys.privateKey, forge.md.sha256.create()) + fs.writeFileSync(path.join(directory, "ca.pem"), forge.pki.certificateToPem(ca)) + + const keys = forge.pki.rsa.generateKeyPair(2048) + const fixtures = Object.fromEntries(["valid", "expired", "mismatched", "untrusted"].map((kind, index) => { + const cert = forge.pki.createCertificate() + cert.publicKey = keys.publicKey + cert.serialNumber = `0${index + 2}` + cert.validity.notBefore = new Date("2020-01-01T00:00:00Z") + cert.validity.notAfter = new Date(kind === "expired" ? "2021-01-01T00:00:00Z" : "2040-01-01T00:00:00Z") + cert.setSubject([{ name: "commonName", value: "Disposable preview upstream" }]) + cert.setIssuer(kind === "untrusted" ? cert.subject.attributes : ca.subject.attributes) + cert.setExtensions([ + { name: "basicConstraints", cA: false }, + { name: "keyUsage", digitalSignature: true, keyEncipherment: true }, + { name: "extKeyUsage", serverAuth: true }, + { name: "subjectAltName", altNames: kind === "mismatched" + ? [{ type: 2, value: "wrong.invalid" }] + : [{ type: 7, ip: "127.0.0.1" }, { type: 2, value: "localhost" }] }, + ]) + cert.sign(kind === "untrusted" ? keys.privateKey : caKeys.privateKey, forge.md.sha256.create()) + return [kind, { key: forge.pki.privateKeyToPem(keys.privateKey), cert: forge.pki.certificateToPem(cert) }] + })) + fs.writeFileSync(path.join(directory, "certificates.json"), JSON.stringify(fixtures)) + + const fixture = fileURLToPath(new URL("./fixtures/preview-security.ts", import.meta.url)) + const result = await new Promise<{ code: number | null; output: string }>((resolve, reject) => { + const env = { ...process.env, PREVIEW_SECURITY_DIRECTORY: directory, + NODE_EXTRA_CA_CERTS: path.join(directory, "ca.pem"), + // Explicit transport verification must win even over this inherited + // bypass. This is confined to the disposable child, not production. + NODE_TLS_REJECT_UNAUTHORIZED: "0" } + // Node marks test workers in the environment. The isolated TLS child is + // a fresh runner, not a recursive node:test invocation in this worker. + delete (env as NodeJS.ProcessEnv).NODE_TEST_CONTEXT + const child = spawn(process.execPath, ["--import", "tsx", "--test", fixture], { + env, + signal: AbortSignal.timeout(90_000), + stdio: ["ignore", "pipe", "pipe"], + }) + let output = "" + child.stdout.on("data", (chunk) => { output += chunk; process.stdout.write(chunk) }) + child.stderr.on("data", (chunk) => { output += chunk; process.stdout.write(chunk) }) + child.once("error", reject) + child.once("close", (code) => resolve({ code, output })) + }) + assert.equal(result.code, 0, result.output) + } finally { + fs.rmSync(directory, { recursive: true, force: true }) + } +}) diff --git a/packages/server/src/server/__tests__/preview-ws-browser.test.ts b/packages/server/src/server/__tests__/preview-ws-browser.test.ts new file mode 100644 index 000000000..2d4d9945d --- /dev/null +++ b/packages/server/src/server/__tests__/preview-ws-browser.test.ts @@ -0,0 +1,254 @@ +import assert from "node:assert/strict" +import { createHash } from "node:crypto" +import fs from "node:fs" +import { createServer as createUpstreamServer, type IncomingHttpHeaders } from "node:http" +import type { Socket } from "node:net" +import os from "node:os" +import path from "node:path" +import { after, before, it } from "node:test" +import { fileURLToPath } from "node:url" +import { chromium, type Browser } from "playwright" +import { build } from "vite" +import solid from "vite-plugin-solid" +import { AuthManager } from "../../auth/manager" +import type { Logger } from "../../logger" +import { PreviewManager } from "../../previews/manager" +import { createHttpServer } from "../http-server" + +const logger = { child: () => logger, debug() {}, info() {}, warn() {}, error() {}, trace() {}, isLevelEnabled: () => false } as unknown as Logger +const previews = new PreviewManager() +const sockets = new Set() +const requests: { mode: string; headers: IncomingHttpHeaders }[] = [] +let browser: Browser | undefined +let app: ReturnType | undefined +let upstream: ReturnType | undefined +let directory: string | undefined +let auth: AuthManager +let origin: string +let base: string + +function track(socket: Socket) { + sockets.add(socket) + socket.once("close", () => sockets.delete(socket)) +} + +function textFrame(text: string): Buffer { + const payload = Buffer.from(text) + assert.ok(payload.length < 126) + return Buffer.concat([Buffer.from([0x81, payload.length]), payload]) +} + +// Deliberately small, real RFC 6455 echo peer. Browser client frames are masked; +// all fixture messages fit the short-length form. No WebSocket client emulation. +function echoFrames(socket: Socket, head: Buffer) { + let pending = Buffer.alloc(0) + const consume = (chunk: Buffer) => { + pending = Buffer.concat([pending, chunk]) + while (pending.length >= 2) { + const opcode = pending[0] & 0x0f + const length = pending[1] & 0x7f + if (!(pending[1] & 0x80) || length >= 126) return socket.destroy() + if (pending.length < 6 + length) return + const mask = pending.subarray(2, 6) + const payload = Buffer.from(pending.subarray(6, 6 + length)) + for (let index = 0; index < payload.length; index++) payload[index] ^= mask[index % 4] + pending = pending.subarray(6 + length) + if (opcode === 0x8) { socket.end(Buffer.from([0x88, 0])); return } + if (opcode === 0x1) socket.write(textFrame(payload.toString())) + } + } + socket.on("data", consume) + socket.on("error", () => {}) + if (head.length) consume(head) +} + +function installedChromium(): string { + if (process.env.CODENOMAD_BROWSER_PATH) { + assert.ok(fs.existsSync(process.env.CODENOMAD_BROWSER_PATH), "CODENOMAD_BROWSER_PATH must exist; this test never installs a browser") + return process.env.CODENOMAD_BROWSER_PATH + } + if (fs.existsSync(chromium.executablePath())) return chromium.executablePath() + const cache = process.env.PLAYWRIGHT_BROWSERS_PATH || (process.platform === "win32" + ? path.join(process.env.LOCALAPPDATA!, "ms-playwright") + : process.platform === "darwin" ? path.join(os.homedir(), "Library", "Caches", "ms-playwright") : path.join(os.homedir(), ".cache", "ms-playwright")) + for (const entry of fs.existsSync(cache) ? fs.readdirSync(cache).filter(name => /^chromium-\d+$/.test(name)).sort().reverse() : []) { + for (const relative of ["chrome-win64/chrome.exe", "chrome-win/chrome.exe", "chrome-linux64/chrome", "chrome-linux/chrome", "chrome-mac/Chromium.app/Contents/MacOS/Chromium"]) { + const candidate = path.join(cache, entry, relative) + if (fs.existsSync(candidate)) return candidate + } + } + throw new Error("No installed Chromium found. Set CODENOMAD_BROWSER_PATH; this regression never downloads browsers.") +} + +before(async () => { + const executablePath = installedChromium() + const tempRoot = process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, "Temp", "opencode") : os.tmpdir() + fs.mkdirSync(tempRoot, { recursive: true }) + directory = fs.mkdtempSync(path.join(tempRoot, "preview-ws-browser-")) + const staticDir = path.join(directory, "ui") + const fixture = fileURLToPath(new URL("./fixtures/preview-ws-browser.tsx", import.meta.url)) + // Compile the actual Solid BrowserFrame once, then serve it from the actual + // production backend origin. No development proxy or mock response sanitizer. + await build({ configFile: false, root: fileURLToPath(new URL("../../../../ui", import.meta.url)), logLevel: "error", + plugins: [solid()], resolve: { dedupe: ["solid-js"] }, css: { postcss: {} }, + build: { outDir: staticDir, emptyOutDir: false, minify: false, + rollupOptions: { input: fixture, output: { entryFileNames: "fixture.js" } } }, + }) + fs.writeFileSync(path.join(staticDir, "index.html"), '
') + auth = new AuthManager({ configPath: path.join(directory, "config.yaml"), username: "fixture", password: "disposable-browser-password", generateToken: false }, logger) + upstream = createUpstreamServer((request, response) => { + const mode = new URL(request.url!, "http://fixture").searchParams.get("mode")! + response.setHeader("content-type", "text/html") + response.end(`
`)
+  })
+  upstream.on("connection", track)
+  upstream.on("upgrade", (request, socket, head) => {
+    const peer = socket as Socket
+    const mode = new URL(request.url!, "http://fixture").searchParams.get("mode")!
+    requests.push({ mode, headers: request.headers })
+    const maliciousCookies = [
+      `sEt-CoOkIe: ${auth.getCookieName()}=attacker-overwrite; Path=/; HttpOnly; SameSite=None; Secure`,
+      "Set-Cookie: preview_extra=attacker; Path=/; SameSite=None; Secure",
+      "SET-COOKIE: preview_copy=attacker; Path=/; SameSite=None; Secure",
+      "sEt-CoOkIe2: preview_legacy=attacker; Path=/",
+    ]
+    if (mode === "redirect") {
+      peer.end(["HTTP/1.1 302 Found", "Location: /api/auth/status", ...maliciousCookies, "Content-Length: 0", "Connection: close", "", ""].join("\r\n"))
+      return
+    }
+    const accept = createHash("sha1").update(`${request.headers["sec-websocket-key"]}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest("base64")
+    const headers = ["HTTP/1.1 101 Switching Protocols", "Upgrade: websocket", "Connection: Upgrade", `Sec-WebSocket-Accept: ${accept}`,
+      ...(mode === "malicious" ? maliciousCookies : []), "", ""].join("\r\n")
+    // Headers and the first complete frame share a single upstream write.
+    peer.write(Buffer.concat([Buffer.from(headers), textFrame("early-coalesced")]))
+    echoFrames(peer, head)
+  })
+  await new Promise(resolve => upstream!.listen(0, "127.0.0.1", resolve))
+  const address = upstream.address()
+  assert.ok(address && typeof address === "object")
+  const preview = previews.create("disposable-browser-session", `http://127.0.0.1:${address.port}`)
+  base = previews.buildProxyBasePath(preview.token)
+  // Unrelated integrations are inert, exactly as in the preview-security fixture.
+  app = createHttpServer({ bindHost: "127.0.0.1", bindPort: 0, defaultPort: 0, protocol: "http", logger,
+    authManager: auth, previewManager: previews, uiStaticDir: staticDir,
+    workspaceManager: {}, settings: {}, fileSystemBrowser: {}, eventBus: {}, serverMeta: {}, instanceStore: {},
+    speechService: {}, sidecarManager: {}, clientConnectionManager: {}, remoteProxySessionManager: {},
+    yoloManager: {}, nativeParent: {}, automationBridgeToken: "disposable-browser-fixture",
+  } as unknown as Parameters[0])
+  app.instance.server.on("connection", track)
+  origin = `http://127.0.0.1:${(await app.start()).port}`
+  browser = await chromium.launch({ executablePath, timeout: 20_000 })
+  console.log(`Disposable preview browser: ${executablePath}`)
+}, { timeout: 60_000 })
+
+after(async () => {
+  // Close the isolated context/browser before stopping the production backend;
+  // destroy only owned listener connections, including upgraded sockets.
+  try {
+    await browser?.close()
+  } finally {
+    for (const socket of sockets) socket.destroy()
+    try {
+      await app?.stop()
+    } finally {
+      try {
+        if (upstream) await new Promise(resolve => upstream!.close(() => resolve()))
+      } finally {
+        if (directory) fs.rmSync(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })
+      }
+    }
+  }
+}, { timeout: 30_000 })
+
+for (const mode of ["malicious", "redirect", "normal"]) {
+  it(`${mode} preview WebSocket preserves the actual browser login cookie and auth status`, { timeout: 30_000 }, async () => {
+    const context = await browser!.newContext()
+    try {
+      // Only provider reads are fixtures. Login, auth status, preview HTTP and
+      // WebSocket traffic still use the real isolated production backend.
+      for (const [endpoint, json] of Object.entries({
+        "/api/storage/config/ui": { settings: { locale: "en" } },
+        "/api/storage/config/server": {},
+        "/api/storage/state/ui": {},
+        "/api/speech/capabilities": { configured: false },
+      })) await context.route(`${origin}${endpoint}`, route => {
+        assert.equal(route.request().method(), "GET")
+        return route.fulfill({ json })
+      })
+      await context.route(`${origin}/api/events?*`, route => route.fulfill({ contentType: "text/event-stream", body: ": disposable fixture\n\n" }))
+      const page = await context.newPage()
+      const pageErrors: string[] = []
+      page.on("pageerror", error => { pageErrors.push(error.message); console.error(`Preview fixture page error: ${error.message}`) })
+      page.setDefaultTimeout(15_000)
+      const login = await context.request.post(`${origin}/api/auth/login`, { data: { username: "fixture", password: "disposable-browser-password" }, timeout: 10_000 })
+      assert.equal(login.status(), 200)
+      const original = await context.cookies(origin)
+      assert.equal(original.length, 1)
+      assert.equal(original[0].name, auth.getCookieName())
+      assert.equal(original[0].httpOnly, true)
+      assert.notEqual(original[0].value, "attacker-overwrite")
+      await page.goto(`${origin}/?preview=${encodeURIComponent(base)}&mode=${mode}`)
+      assert.equal(await page.locator("iframe").getAttribute("sandbox"), "allow-scripts allow-forms allow-modals allow-popups allow-downloads")
+      await page.frameLocator("iframe").locator("#open").waitFor()
+      const frame = page.frames().find(frame => frame.url().startsWith(`${origin}${base}/page`))
+      assert.ok(frame, "production proxy loads the preview document in its sandbox iframe")
+      // Chromium isolates the opaque-origin iframe into its own target. Listen
+      // there before opening the socket, not on the unrelated parent target.
+      const cdp = await context.newCDPSession(frame)
+      await cdp.send("Network.enable")
+      const handshakes: { status: number; headers: Record }[] = []
+      const handshakeErrors: string[] = []
+      cdp.on("Network.webSocketHandshakeResponseReceived", event => handshakes.push(event.response))
+      cdp.on("Network.webSocketFrameError", event => handshakeErrors.push(event.errorMessage))
+      await page.frameLocator("iframe").locator("#open").click()
+      const result = JSON.parse(await page.frameLocator("iframe").locator("#result[data-done=true]").textContent() ?? "null") as {
+        opened: boolean; messages: string[]; failed: boolean; opaque: boolean; timeout?: boolean
+      }
+      assert.equal(result.timeout, undefined, JSON.stringify(result))
+      assert.equal(result.opaque, true, "actual BrowserFrame must deny preview access to parent DOM")
+      assert.equal(result.opened, mode !== "redirect", JSON.stringify(result))
+      assert.equal(result.failed, mode === "redirect", JSON.stringify(result))
+      assert.deepEqual(result.messages, mode === "redirect" ? [] : ["early-coalesced", "browser-echo"])
+      assert.deepEqual(await context.cookies(origin), original, "real HttpOnly login cookie must not change or gain any untrusted cookies")
+      if (mode === "redirect") {
+        // Chromium reports rejected HTTP handshakes through FrameError, not the
+        // successful HandshakeResponse event. Assert its observed status code.
+        assert.equal(handshakes.length, 0, JSON.stringify(handshakes))
+        assert.equal(handshakeErrors.length, 1, JSON.stringify(handshakeErrors))
+        assert.match(handshakeErrors[0], /Unexpected response code: 502/)
+      } else {
+        assert.equal(handshakes.length, 1, JSON.stringify(handshakes))
+        assert.equal(handshakes[0].status, 101)
+        assert.ok(Object.keys(handshakes[0].headers).every(name => !/^set-cookie2?$/i.test(name)), "untrusted cookie response fields must not reach Chromium")
+        assert.equal(Object.keys(handshakes[0].headers).some(name => name.toLowerCase() === "location"), false)
+        assert.deepEqual(handshakeErrors, [])
+      }
+      const status = await page.evaluate(async () => {
+        const response = await fetch("/api/auth/status")
+        return { status: response.status, body: await response.json() as { authenticated: boolean } }
+      })
+      assert.equal(status.status, 200)
+      assert.equal(status.body.authenticated, true)
+      const seen = requests.filter(request => request.mode === mode)
+      assert.equal(seen.length, 1)
+      assert.equal(seen[0].headers.cookie, undefined, "preview upstream never receives backend cookies")
+      assert.deepEqual(pageErrors, [], "production BrowserFrame and its providers must mount without browser errors")
+      console.log(`${mode}: Chromium response ${mode === "redirect" ? 502 : handshakes[0].status}; cookie unchanged; authenticated=true; frames=${result.messages.join(",")}`)
+    } finally {
+      await context.close()
+    }
+  })
+}
diff --git a/packages/server/src/server/http-server.ts b/packages/server/src/server/http-server.ts
index 3a7be35a6..803271bb9 100644
--- a/packages/server/src/server/http-server.ts
+++ b/packages/server/src/server/http-server.ts
@@ -5,8 +5,8 @@ import replyFrom from "@fastify/reply-from"
 import fs from "fs"
 import { connect as connectTcp, isIP, type Socket } from "net"
 import path from "path"
-import type { Readable } from "stream"
-import { connect as connectTls, type TLSSocket } from "tls"
+import { PassThrough, type Readable } from "stream"
+import { checkServerIdentity, connect as connectTls, type TLSSocket } from "tls"
 import { fetch } from "undici"
 import type { Logger } from "../logger"
 import { WorkspaceManager } from "../workspaces/manager"
@@ -51,6 +51,7 @@ import type { SpeechService } from "../speech/service"
 import { ClientConnectionManager } from "../clients/connection-manager"
 import type { SideCarManager } from "../sidecars/manager"
 import type { PreviewManager } from "../previews/manager"
+import { PreviewWebSocketResponse } from "../previews/websocket-response"
 import { buildPreviewRuntimeBridge, rewritePreviewImportMap, rewritePreviewJavaScriptImports } from "../previews/runtime-bridge"
 import { forwardRuntimeRequest } from "../opencode/compatibility/proxy"
 import { requestAdmission } from "./request-admission"
@@ -222,9 +223,15 @@ export function createHttpServer(deps: HttpServerDeps) {
     contentTypesToEncode: [],
     undici: {
       connections: 16,
-      pipelining: 1,
+      // Nested Undici 5.x is affected by GHSA-35p6-xmwp-9g52. Until the
+      // dependency can be replaced, disable reuse of untrusted upstream sockets.
+      pipelining: 0,
       bodyTimeout: 0,
       headersTimeout: 0,
+      // reply-from defaults verification off. Preview targets (including localhost)
+      // must verify both the certificate chain and the requested hostname.
+      connect: { rejectUnauthorized: true, checkServerIdentity },
+      maxRedirections: 0,
     },
   })
 
@@ -1936,7 +1943,7 @@ async function proxyPreviewRequest(args: {
     publicBase,
     logContext: { previewToken: token },
     errorMessage: "Preview proxy failed",
-    rewriteHeaders: (headers) => rewritePreviewResponseHeaders(headers, token, targetUrl.origin, publicBase),
+    rewriteHeaders: (headers) => rewritePreviewResponseHeaders(headers, token, targetUrl, publicBase),
   })
 }
 
@@ -1967,7 +1974,7 @@ async function proxyPreviewAssetRequest(args: {
     publicBase: args.previewManager.buildProxyBasePath(args.token),
     logContext: { previewToken: args.token, previewFallback: true },
     errorMessage: "Preview proxy failed",
-    rewriteHeaders: (headers) => rewritePreviewResponseHeaders(headers, args.token, targetUrl.origin),
+    rewriteHeaders: (headers) => rewritePreviewResponseHeaders(headers, args.token, targetUrl),
   })
 }
 
@@ -1985,7 +1992,7 @@ async function proxyPreviewTargetRequest(args: {
   let responseKind: "html" | "css" | "js" | null = null
   await args.reply.from(args.targetUrl, {
     rewriteRequestHeaders: (_originalRequest, headers) => {
-      const next = sanitizeSideCarProxyRequestHeaders(headers as Record, args.targetOrigin)
+      const next = sanitizeSideCarProxyRequestHeaders(headers as Record, args.targetOrigin, { stripCookies: true })
       delete next["accept-encoding"]
       return next
     },
@@ -2203,8 +2210,14 @@ function proxyTargetWebSocketUpgrade(args: {
 }) {
   const { request, socket, head, targetUrl, logger, logContext, proxyLabel, stripCookies } = args
   const { socket: upstream, readyEvent } = createSideCarUpstreamSocket(targetUrl)
+  const response = stripCookies ? new PreviewWebSocketResponse(request.headers["sec-websocket-key"]) : undefined
+  // Read enough to observe an early client disconnect, but do not send frames
+  // upstream before admission. Backpressure bounds the pending client stream.
+  const pendingClient = response ? new PassThrough({ highWaterMark: 16 * 1024 }) : undefined
 
   const closeBoth = () => {
+    response?.destroy()
+    pendingClient?.destroy()
     if (!socket.destroyed) {
       socket.destroy()
     }
@@ -2215,7 +2228,9 @@ function proxyTargetWebSocketUpgrade(args: {
 
   upstream.once("error", (error) => {
     logger.error({ ...logContext, err: error, targetUrl: targetUrl.toString() }, `Failed to proxy ${proxyLabel} websocket`)
-    rejectUpgrade(socket, 502, "Bad Gateway")
+    if (response?.admitted) closeBoth()
+    else rejectUpgrade(socket, 502, "Bad Gateway")
+    response?.destroy()
     if (!upstream.destroyed) {
       upstream.destroy()
     }
@@ -2223,19 +2238,38 @@ function proxyTargetWebSocketUpgrade(args: {
 
   socket.once("error", (error) => {
     logger.debug({ ...logContext, err: error }, `${proxyLabel} websocket client socket errored`)
-    if (!upstream.destroyed) {
-      upstream.destroy()
-    }
+    closeBoth()
   })
 
+  if (response) {
+    pendingClient!.once("error", closeBoth)
+    if (head.length > 0) pendingClient!.write(head)
+    socket.pipe(pendingClient!)
+    // A WebSocket is not a half-open HTTP stream. A disconnected client must
+    // release even a peer that ignores FIN, before or after admission.
+    socket.once("end", closeBoth)
+    response.once("error", (error) => {
+      logger.error({ ...logContext, err: error }, "Rejected preview websocket response")
+      if (response.admitted) closeBoth()
+      else {
+        rejectUpgrade(socket, 502, "Bad Gateway")
+        upstream.destroy()
+      }
+    })
+    response.once("accepted", () => {
+      if (!socket.destroyed && !upstream.destroyed) pendingClient!.pipe(upstream)
+    })
+    upstream.pipe(response).pipe(socket)
+  }
+
   upstream.once(readyEvent, () => {
     try {
       upstream.write(buildSideCarWebSocketRequest(request, targetUrl, { stripCookies }))
-      if (head.length > 0) {
-        upstream.write(head)
+      if (!response) {
+        if (head.length > 0) upstream.write(head)
+        upstream.pipe(socket)
+        socket.pipe(upstream)
       }
-      upstream.pipe(socket)
-      socket.pipe(upstream)
     } catch (error) {
       logger.error({ ...logContext, err: error, targetUrl: targetUrl.toString() }, `Failed to forward ${proxyLabel} websocket upgrade`)
       closeBoth()
@@ -2243,14 +2277,26 @@ function proxyTargetWebSocketUpgrade(args: {
   })
 
   upstream.once("close", () => {
+    if (response) {
+      if (!response.admitted) {
+        rejectUpgrade(socket, 502, "Bad Gateway")
+        response.destroy()
+      } else if (!upstream.readableEnded) closeBoth()
+      // A graceful EOF must drain the response transform and writable client;
+      // destroying either here would discard frames buffered by backpressure.
+      return
+    }
     if (!socket.destroyed) {
       socket.end()
     }
   })
 
   socket.once("close", () => {
+    response?.destroy()
+    pendingClient?.destroy()
     if (!upstream.destroyed) {
-      upstream.end()
+      if (response) upstream.destroy()
+      else upstream.end()
     }
   })
 }
@@ -2260,9 +2306,11 @@ function createSideCarUpstreamSocket(targetUrl: URL): { socket: Socket | TLSSock
   if (targetUrl.protocol === "https:") {
     return {
       socket: connectTls({
-        host: targetUrl.hostname,
+        host: stripHostBrackets(targetUrl.hostname),
         port,
-        servername: targetUrl.hostname,
+        servername: isIP(stripHostBrackets(targetUrl.hostname)) ? undefined : targetUrl.hostname,
+        rejectUnauthorized: true,
+        checkServerIdentity,
       }),
       readyEvent: "secureConnect",
     }
@@ -2290,7 +2338,7 @@ function buildSideCarWebSocketRequest(
     if (!key || value === undefined) continue
     const lower = key.toLowerCase()
     if (blockedHeaders.has(lower)) continue
-    if (options?.stripCookies && lower === "cookie") continue
+    if (options?.stripCookies && (lower === "cookie" || lower === "cookie2")) continue
     if (lower === "origin") {
       headerLines.push(`Origin: ${targetUrl.origin}\r\n`)
       continue
@@ -2316,11 +2364,11 @@ function isWebSocketUpgradeRequest(request: import("http").IncomingMessage): boo
 }
 
 function rejectUpgrade(socket: Socket, statusCode: number, statusText: string) {
-  if (socket.destroyed) {
+  if (socket.destroyed || socket.writableEnded) {
     return
   }
-  socket.write(`HTTP/1.1 ${statusCode} ${statusText}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n`)
-  socket.destroy()
+  socket.end(`HTTP/1.1 ${statusCode} ${statusText}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n`)
+  socket.destroySoon()
 }
 
 function rewriteSideCarResponseHeaders(
@@ -2362,7 +2410,7 @@ function rewriteSideCarResponseHeaders(
 function rewritePreviewResponseHeaders(
   headers: Record,
   token: string,
-  targetOrigin: string,
+  targetUrl: URL,
   publicBase = `/previews/${encodeURIComponent(token)}`,
 ) {
   const next = { ...headers }
@@ -2371,6 +2419,8 @@ function rewritePreviewResponseHeaders(
   delete next["content-security-policy-report-only"]
   delete next["set-cookie"]
   delete next["set-cookie2"]
+  // Refresh can navigate directly around the credential-stripping proxy.
+  delete next.refresh
 
   const locationHeader = next.location
   const location = Array.isArray(locationHeader) ? locationHeader[0] : locationHeader
@@ -2378,25 +2428,23 @@ function rewritePreviewResponseHeaders(
     return next
   }
 
-  if (location.startsWith("//")) {
-    const parsed = new URL(location, targetOrigin)
-    next.location = parsed.origin === targetOrigin
-      ? `${publicBase}${parsed.pathname}${parsed.search}${parsed.hash}`
-      : parsed.href
-    return next
-  }
-  if (location.startsWith("/")) {
-    next.location = `${publicBase}${location}`
-    return next
-  }
-
   try {
-    const parsed = new URL(location)
-    if (parsed.origin === targetOrigin) {
+    // Resolve all relative, dot-segment and backslash variants against the
+    // actual upstream path before projecting them into the capability path.
+    const parsed = new URL(location, targetUrl)
+    if (parsed.origin === targetUrl.origin) {
       next.location = `${publicBase}${parsed.pathname}${parsed.search}${parsed.hash}`
+    } else {
+      // Browser cookies are scoped by host, not port. A direct redirect to a
+      // different port on CodeNomad's host would leak its Path=/ auth cookie.
+      // Pin redirects to the preview target too: a reverse proxy may rewrite
+      // Host, so the backend cannot reliably identify the browser's cookie host.
+      // A 3xx without Location remains an inert upstream response.
+      delete next.location
     }
   } catch {
-    // Relative redirects should continue to resolve against the current preview path.
+    // Malformed redirect destinations must not escape the proxy boundary.
+    delete next.location
   }
 
   return next
@@ -2405,6 +2453,7 @@ function rewritePreviewResponseHeaders(
 function sanitizeSideCarProxyRequestHeaders(
   headers: Record,
   targetOrigin: string,
+  options?: { stripCookies?: boolean },
 ): Record {
   const blockedHeaders = getBlockedSideCarRequestHeaders()
   const next: Record = {}
@@ -2412,6 +2461,8 @@ function sanitizeSideCarProxyRequestHeaders(
   for (const [key, value] of Object.entries(headers)) {
     if (!value) continue
     if (blockedHeaders.has(key.toLowerCase())) continue
+    // Preview cookies belong to CodeNomad's origin, not the untrusted target.
+    if (options?.stripCookies && (key.toLowerCase() === "cookie" || key.toLowerCase() === "cookie2")) continue
     next[key] = value
   }
 
diff --git a/packages/ui/package.json b/packages/ui/package.json
index e5805ca1b..5134dc2a6 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -34,7 +34,7 @@
     "monaco-editor": "^0.52.2",
     "qrcode": "^1.5.3",
     "shiki": "^3.13.0",
-    "solid-js": "^1.8.0",
+    "solid-js": "^1.9.15",
     "solid-toast": "^0.5.0",
     "virtua": "^0.51.0",
     "yaml": "^2.4.2"
@@ -44,11 +44,11 @@
     "@vite-pwa/assets-generator": "^1.0.2",
     "autoprefixer": "10.4.21",
     "playwright": "1.63.0",
-    "postcss": "8.5.6",
+    "postcss": "8.5.28",
     "tailwindcss": "3",
     "typescript": "^5.3.0",
-    "vite": "^5.0.0",
+    "vite": "^6.4.3",
     "vite-plugin-pwa": "^1.2.0",
-    "vite-plugin-solid": "^2.10.0"
+    "vite-plugin-solid": "^2.11.10"
   }
 }
diff --git a/packages/ui/src/lib/runtime-env-mobile.test.ts b/packages/ui/src/lib/runtime-env-mobile.test.ts
new file mode 100644
index 000000000..9fda82459
--- /dev/null
+++ b/packages/ui/src/lib/runtime-env-mobile.test.ts
@@ -0,0 +1,65 @@
+import assert from "node:assert/strict"
+import { test } from "node:test"
+
+test("hosted mobile overrides fence native APIs even with Tauri globals and user agent", async () => {
+  const previousWindow = Object.getOwnPropertyDescriptor(globalThis, "window")
+  const previousNavigator = Object.getOwnPropertyDescriptor(globalThis, "navigator")
+  const commands: string[] = []
+  const invoke = async (command: string) => {
+    commands.push(command)
+    throw new Error(`Hosted UI must not invoke ${command}`)
+  }
+  Object.defineProperty(globalThis, "window", { configurable: true, value: {
+    __CODENOMAD_RUNTIME_HOST__: "web",
+    __CODENOMAD_WINDOW_CONTEXT__: "remote",
+    __TAURI__: { core: { invoke } },
+    __TAURI_INTERNALS__: { invoke },
+  } })
+  Object.defineProperty(globalThis, "navigator", { configurable: true, value: {
+    userAgent: "Mozilla/5.0 (Linux; Android 15; Mobile) Tauri/2.0",
+    userAgentData: { mobile: true },
+  } })
+  try {
+    // Import after initialization: consumers use both the cached snapshot and
+    // live capability predicates. Neither may infer native authority from IPC.
+    const runtime = await import("./runtime-env")
+    assert.deepEqual(runtime.runtimeEnv, { host: "web", platform: "mobile", windowContext: "remote" })
+    assert.equal(runtime.detectRuntimeEnvironment(), runtime.runtimeEnv)
+    assert.equal(runtime.isWebHost(), true)
+    assert.equal(runtime.isRemoteWindow(), true)
+    assert.equal(runtime.isMobilePlatform(), true)
+    assert.equal(runtime.usesClientState(), true, "Hosted client state is not native authority")
+    for (const capability of [runtime.isElectronHost, runtime.isTauriHost, runtime.isDesktopHost,
+      runtime.isLocalTauriHost, runtime.isLocalWindow, runtime.isNativeApplicationWindow,
+      runtime.canUseNativeDialogs, runtime.canOpenRemoteWindows, runtime.canRestartCli,
+      runtime.canUseDesktopFolderDrop]) {
+      assert.equal(capability(), false)
+    }
+
+    const dialogs = await import("./native/native-functions")
+    assert.equal(dialogs.supportsNativeDialogs(), false)
+    assert.equal(dialogs.supportsNativeDialogsInCurrentWindow(), false)
+    assert.equal(await dialogs.openNativeFolderDialog(), null)
+    assert.equal(await dialogs.openNativeFileDialog(), null)
+    assert.deepEqual(await dialogs.openNativeFileDialogs(), [])
+    const cli = await import("./native/cli")
+    assert.equal(await cli.restartCli(), false)
+    const windowControls = await import("./native/window-controls")
+    for (const action of ["minimize", "maximize", "close"] as const) await windowControls.runNativeWindowAction(action)
+    await windowControls.startNativeWindowDrag()
+    const wakeLock = await import("./native/wake-lock")
+    assert.equal(await wakeLock.setWakeLockDesired(true), false)
+    await wakeLock.setWakeLockDesired(false)
+    const clientState = await import("./native/client-state")
+    assert.deepEqual(await clientState.loadNativeClientState(), { isPrimary: false, restoreEnabled: false, snapshot: null })
+    assert.equal(await clientState.saveNativeClientState({ draft: "local only" }), false)
+    assert.equal(await clientState.clearNativeClientState(), false)
+    assert.equal(await clientState.loadNativeClientStatePartition("fixture"), null)
+    assert.deepEqual(commands, [], "No native calls, including swallowed errors, are allowed")
+  } finally {
+    if (previousWindow) Object.defineProperty(globalThis, "window", previousWindow)
+    else Reflect.deleteProperty(globalThis, "window")
+    if (previousNavigator) Object.defineProperty(globalThis, "navigator", previousNavigator)
+    else Reflect.deleteProperty(globalThis, "navigator")
+  }
+})
diff --git a/packages/ui/tests/browser/auth-recovery.test.ts b/packages/ui/tests/browser/auth-recovery.test.ts
index ca6fee268..f9c82bbc7 100644
--- a/packages/ui/tests/browser/auth-recovery.test.ts
+++ b/packages/ui/tests/browser/auth-recovery.test.ts
@@ -55,7 +55,7 @@ before(async () => {
     plugins: [solid(), { name: "auth-fixture", configureServer(s) {
       s.middlewares.use("/auth-fixture", async (_req, res) => {
         res.setHeader("Content-Type", "text/html")
-        res.end(await s.transformIndexHtml("/auth-fixture", '
')) + res.end(await s.transformIndexHtml("/auth-fixture", '
')) }) } }], resolve: { dedupe: ["solid-js"] }, optimizeDeps: { exclude: ["lucide-solid"] }, server: { host: "127.0.0.1", port: 0, hmr: false, watch: null, proxy: { "/api": target, "/workspaces": target } }, @@ -70,10 +70,18 @@ after(async () => { await server?.close() await backend?.close() }) -async function setup(width = 1100) { +async function setup(width = 1100, companion = false) { offline = false restart() - const page = await browser.newPage({ locale: "en-US", viewport: { width, height: 800 } }) + const page = await browser.newPage({ locale: "en-US", viewport: { width, height: companion ? 640 : 800 }, + ...(companion ? { isMobile: true, hasTouch: true, userAgent: "Mozilla/5.0 (Linux; Android 15; Mobile) Tauri/2.0" } : {}) }) + // Raw script avoids tsx's function-name helpers in serialized init callbacks. + if (companion) await page.addInitScript(` + window.nativeCalls = []; + const invoke = async command => { window.nativeCalls.push(command); throw Error('Unexpected native call: ' + command) }; + Object.assign(window, { __CODENOMAD_RUNTIME_HOST__: 'web', __CODENOMAD_WINDOW_CONTEXT__: 'remote', + __TAURI__: { core: { invoke } }, __TAURI_INTERNALS__: { invoke } }); + `) const errors: string[] = [] page.on("pageerror", error => errors.push(error.message)) await page.request.post(`${url}/api/auth/login`, { data: { username: "fixture", password: "fixture-only" } }) @@ -169,3 +177,38 @@ test("API expiry recovery works above an error dialog and accepts login renewed assert.equal(mutationAttempts, beforeMutations + 1, "Recovery must not replay the failed project creation") } finally { await page.close() } }) + +test("hosted mobile login preserves drafts and reconnects without replaying a failed mutation", async () => { + const { page, errors } = await setup(360, true) + try { + assert.deepEqual(await page.evaluate(() => (window as any).fixture.runtimeEnv), + { host: "web", platform: "mobile", windowContext: "remote" }) + const composer = page.locator(".prompt-input-container textarea").first() + await composer.fill("MOBILE_UNSENT_DRAFT") + await page.evaluate(() => (window as any).fixture.attach()) + const opens = await page.evaluate(() => (window as any).fixture.opens()) + const beforeMutations = mutationAttempts, beforeLogin = loginCount, previousUrl = page.url() + // Expire only the cookie, retaining SSE until API recovery takes ownership. + auth = newAuth() + await page.evaluate(() => (window as any).fixture.openProject()) + const dialog = page.getByRole("dialog", { name: "Sign in to CodeNomad again" }) + await dialog.waitFor() + const box = await dialog.boundingBox() + assert.ok(box && box.x >= 0 && box.x + box.width <= 360 && box.y >= 0 && box.y + box.height <= 640, + `Recovery must fit the mobile viewport: ${JSON.stringify(box)}`) + await dialog.getByLabel("Username", { exact: true }).fill("fixture") + await dialog.getByLabel("Password", { exact: true }).fill("fixture-only") + await dialog.getByRole("button", { name: "Sign in", exact: true }).click() + await dialog.waitFor({ state: "hidden" }) + await page.waitForFunction(n => (window as any).fixture.opens() > n, opens) + await composer.waitFor({ state: "hidden" }) + await page.evaluate(() => (window as any).fixture.seed("mobile-reopened-fixture")) + await page.waitForFunction(() => (document.querySelector(".prompt-input-container textarea") as HTMLTextAreaElement)?.value === "MOBILE_UNSENT_DRAFT") + assert.equal(await page.evaluate(() => (window as any).fixture.attachments()), 1) + assert.equal(page.url(), previousUrl) + assert.equal(loginCount, beforeLogin + 1) + assert.equal(mutationAttempts, beforeMutations + 1, "Explicit login must not replay project creation") + assert.deepEqual(await page.evaluate(() => (window as any).nativeCalls), []) + assert.deepEqual(errors, []) + } finally { await page.close() } +}) diff --git a/packages/ui/tests/browser/device-upload.test.ts b/packages/ui/tests/browser/device-upload.test.ts index ec76c5eee..2bcea3447 100644 --- a/packages/ui/tests/browser/device-upload.test.ts +++ b/packages/ui/tests/browser/device-upload.test.ts @@ -11,14 +11,20 @@ before(async () => { }) after(async () => { await browser?.close(); await server?.close() }) -async function setup(host = "web", context = "remote", params = "") { - const page = await browser.newPage({ viewport: { width: 1100, height: 800 }, locale: "en-US" }) +async function setup(host = "web", context = "remote", params = "", companion = false) { + const page = await browser.newPage({ viewport: { width: companion ? 360 : 1100, height: 800 }, locale: "en-US", + ...(companion ? { isMobile: true, hasTouch: true, userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) Mobile Tauri/2.0" } : {}) }) const prompts: any[] = [], paths: string[] = [], errors: string[] = [] page.on("pageerror", error => errors.push(error.message)) await page.addInitScript(({ host, context }) => { ;(window as any).__CODENOMAD_RUNTIME_HOST__ = host ;(window as any).__CODENOMAD_WINDOW_CONTEXT__ = context }, { host, context }) + if (companion) await page.addInitScript(` + window.nativeCalls = []; + const invoke = async command => { window.nativeCalls.push(command); throw Error('Unexpected native call: ' + command) }; + Object.assign(window, { __TAURI__: { core: { invoke } }, __TAURI_INTERNALS__: { invoke } }); + `) await page.route("**/api/**", route => { const request = route.request(), target = new URL(request.url()) if (target.pathname.endsWith("/prompt")) { @@ -84,6 +90,23 @@ test("device files use native selection and arrive as ordered bytes in the promp } finally { await page.close() } }) +test("hosted mobile attachment selection uses device bytes, not Tauri IPC or server paths", async () => { + const { page, prompts, paths, errors } = await setup("web", "remote", "", true) + try { + await page.locator("textarea:visible").first().fill("Mobile attachment") + const file = { name: "mobile.txt", mimeType: "text/plain", buffer: Buffer.from("DEVICE_BYTES") } + await (await chooseDevice(page)).setFiles(file) + await page.waitForFunction(() => (window as any).fixture.attachments().length === 1) + await page.getByRole("button", { name: "Send message", exact: true }).click() + await page.waitForFunction(() => (window as any).fixture.attachments().length === 0) + assert.equal(prompts.length, 1) + assert.deepEqual(prompts[0].files, [{ name: file.name, uri: `data:text/plain;base64,${file.buffer.toString("base64")}` }]) + assert.deepEqual(paths, []) + assert.deepEqual(await page.evaluate(() => (window as any).nativeCalls), []) + assert.deepEqual(errors, []) + } finally { await page.close() } +}) + test("project references use the real @ picker and retain host paths instead of copying bytes", async () => { const { page, paths, prompts, errors } = await setup("tauri", "local") try { diff --git a/packages/ui/tests/browser/fixtures/auth-recovery.tsx b/packages/ui/tests/browser/fixtures/auth-recovery.tsx index fec84bf86..220132c90 100644 --- a/packages/ui/tests/browser/fixtures/auth-recovery.tsx +++ b/packages/ui/tests/browser/fixtures/auth-recovery.tsx @@ -15,6 +15,7 @@ import { createFileAttachment } from "../../../src/types/attachment" import { addInstance, instances } from "../../../src/stores/instances" import { attachInstanceTab, getInstanceAppTabId, selectAppTab } from "../../../src/stores/app-tabs" import { useAppSessionCapture } from "../../../src/lib/hooks/use-app-session-capture" +import { runtimeEnv } from "../../../src/lib/runtime-env" import "../../../src/index.css" let opens = 0 @@ -34,6 +35,7 @@ render(() => , document.getElementById("root")!) ;(window as any).fixture = { + runtimeEnv, opens: () => opens, seed: (id = "auth-fixture") => { setInstanceId(id) diff --git a/packages/ui/tests/browser/fixtures/device-upload-server.mjs b/packages/ui/tests/browser/fixtures/device-upload-server.mjs index a1a9bd7dc..c4feb9568 100644 --- a/packages/ui/tests/browser/fixtures/device-upload-server.mjs +++ b/packages/ui/tests/browser/fixtures/device-upload-server.mjs @@ -7,7 +7,7 @@ export async function startDeviceUploadFixture() { plugins: [solid(), { name: "device-upload-fixture", configureServer(s) { s.middlewares.use("/device-upload-fixture", async (_req, res) => { res.setHeader("Content-Type", "text/html") - res.end(await s.transformIndexHtml("/device-upload-fixture", '
')) + res.end(await s.transformIndexHtml("/device-upload-fixture", '
')) }) s.middlewares.use("/api", (_req, res) => { res.setHeader("Content-Type", "application/json"); res.end("{}") }) } }], resolve: { dedupe: ["solid-js"] }, optimizeDeps: { exclude: ["lucide-solid"] },