diff --git a/scripts/setup-android-maafw.ps1 b/scripts/setup-android-maafw.ps1 new file mode 100644 index 00000000..de34f6d0 --- /dev/null +++ b/scripts/setup-android-maafw.ps1 @@ -0,0 +1,60 @@ +# MaaFramework Android .so 库集成脚本 +# 用途:将 MaaFramework 的 Android aarch64 .so 文件复制到 jniLibs 目录 +# +# 使用前: +# 1. 从 https://github.com/MaaXYZ/MaaFramework/releases 下载 MAA-android-aarch64-*.tar.gz +# 2. 解压到任意目录 +# 3. 运行此脚本: .\scripts\setup-android-maafw.ps1 -MaaFwDir <解压目录> + +param( + [Parameter(Mandatory=$true)] + [string]$MaaFwDir +) + +$jniLibsDir = Join-Path $PSScriptRoot "..\src-tauri\gen\android\app\src\main\jniLibs\arm64-v8a" + +if (-not (Test-Path $jniLibsDir)) { + New-Item -ItemType Directory -Path $jniLibsDir -Force | Out-Null +} + +$soFiles = @( + "libMaaFramework.so", + "libMaaToolkit.so", + "libonnxruntime.so", + "libfastdeploy_ppocr.so", + "libMaaAgentBinary.so" +) + +$copied = 0 +foreach ($soFile in $soFiles) { + $sourcePath = Join-Path $MaaFwDir $soFile + if (Test-Path $sourcePath) { + Copy-Item -Path $sourcePath -Destination $jniLibsDir -Force + Write-Host " Copied: $soFile" -ForegroundColor Green + $copied++ + } else { + # try lib/ subdirectory + $sourcePath = Join-Path $MaaFwDir "lib" $soFile + if (Test-Path $sourcePath) { + Copy-Item -Path $sourcePath -Destination $jniLibsDir -Force + Write-Host " Copied: $soFile (from lib/)" -ForegroundColor Green + $copied++ + } else { + Write-Host " Not found: $soFile (optional)" -ForegroundColor Yellow + } + } +} + +# Also copy MaaAgentBinary directory if it exists +$agentDir = Join-Path $MaaFwDir "MaaAgentBinary" +if (Test-Path $agentDir) { + $destAgentDir = Join-Path $PSScriptRoot "..\src-tauri\gen\android\app\src\main\assets\MaaAgentBinary" + if (-not (Test-Path $destAgentDir)) { + New-Item -ItemType Directory -Path $destAgentDir -Force | Out-Null + } + Copy-Item -Path "$agentDir\*" -Destination $destAgentDir -Recurse -Force + Write-Host " Copied: MaaAgentBinary directory" -ForegroundColor Green +} + +Write-Host "" +Write-Host "Done! $copied .so files copied to $jniLibsDir" -ForegroundColor Cyan diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 1ccdac8f..a630a05b 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -714,7 +714,7 @@ dependencies = [ "bitflags 2.10.0", "core-foundation 0.10.1", "core-graphics-types", - "foreign-types 0.5.0", + "foreign-types", "libc", ] @@ -1225,15 +1225,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[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" @@ -1241,7 +1232,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared 0.3.1", + "foreign-types-shared", ] [[package]] @@ -1255,12 +1246,6 @@ dependencies = [ "syn 2.0.114", ] -[[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" @@ -1867,22 +1852,6 @@ dependencies = [ "webpki-roots", ] -[[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" @@ -2563,23 +2532,6 @@ dependencies = [ "zip", ] -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "ndk" version = "0.9.0" @@ -2953,50 +2905,6 @@ dependencies = [ "pathdiff", ] -[[package]] -name = "openssl" -version = "0.10.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "foreign-types 0.3.2", - "libc", - "once_cell", - "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.114", -] - -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "openssl-sys" -version = "0.9.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "option-ext" version = "0.2.0" @@ -3798,12 +3706,10 @@ dependencies = [ "http-body-util", "hyper", "hyper-rustls", - "hyper-tls", "hyper-util", "js-sys", "log", "mime", - "native-tls", "percent-encoding", "pin-project-lite", "quinn", @@ -3814,7 +3720,6 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-native-tls", "tokio-rustls", "tokio-util", "tower", @@ -3995,15 +3900,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "schemars" version = "0.8.22" @@ -4067,29 +3963,6 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.10.0", - "core-foundation 0.9.4", - "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.24.0" @@ -5171,16 +5044,6 @@ dependencies = [ "syn 2.0.114", ] -[[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.4" @@ -5559,12 +5422,6 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" -[[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.1" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 30c0e2b8..5620ab91 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -18,15 +18,13 @@ crate-type = ["staticlib", "cdylib", "rlib"] tauri-build = { version = "2", features = [] } [dependencies] -tauri = { version = "2", features = ["image-png", "image-ico", "tray-icon"] } +tauri = { version = "2", features = ["image-png", "image-ico"] } tauri-plugin-opener = "2" tauri-plugin-fs = "2" tauri-plugin-dialog = "2" tauri-plugin-http = "2" tauri-plugin-log = "2" tauri-plugin-process = "2" -tauri-plugin-global-shortcut = "2" -tauri-plugin-autostart = "2" log = "0.4" chrono = "0.4" serde = { version = "1", features = ["derive"] } @@ -37,17 +35,22 @@ zip = "7.2.0" flate2 = "1.0" tar = "0.4" tokio = { version = "1", features = ["rt", "sync"] } -reqwest = { version = "0.12", features = ["stream", "blocking", "json"] } +reqwest = { version = "0.12", default-features = false, features = ["stream", "blocking", "json", "rustls-tls"] } futures-util = "0.3" bytes = "1" libc = "0.2.180" semver = "1.0" os_info = "3" urlencoding = "2.1" -notify-rust = "4" shell-words = "1.1.1" maa-framework = { version = "1", features = ["dynamic"] } +[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] +tauri = { version = "2", features = ["tray-icon"] } +tauri-plugin-global-shortcut = "2" +tauri-plugin-autostart = "2" +notify-rust = "4" + [profile.release] # 保留调试符号以生成 PDB 文件,便于崩溃分析 debug = true diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 087fdbdb..ac703e9c 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -1,7 +1,8 @@ { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", - "description": "Capability for the main window", + "description": "Capability for the desktop main window", + "platforms": ["linux", "macOS", "windows"], "windows": ["main"], "permissions": [ "core:default", diff --git a/src-tauri/capabilities/mobile.json b/src-tauri/capabilities/mobile.json new file mode 100644 index 00000000..523c3841 --- /dev/null +++ b/src-tauri/capabilities/mobile.json @@ -0,0 +1,68 @@ +{ + "$schema": "../gen/schemas/mobile-schema.json", + "identifier": "mobile", + "description": "Capability for the mobile app", + "platforms": ["android", "iOS"], + "windows": ["main"], + "permissions": [ + "core:default", + "core:window:allow-set-title", + "core:window:allow-show", + "core:event:allow-listen", + "core:event:allow-unlisten", + "opener:default", + { + "identifier": "fs:allow-read-file", + "allow": [{ "path": "**/*" }] + }, + { + "identifier": "fs:allow-exists", + "allow": [{ "path": "**/*" }] + }, + { + "identifier": "fs:allow-read-text-file", + "allow": [{ "path": "**/*" }] + }, + { + "identifier": "fs:allow-write-text-file", + "allow": [{ "path": "**/*" }] + }, + { + "identifier": "fs:allow-write-file", + "allow": [{ "path": "**/*" }] + }, + { + "identifier": "fs:allow-mkdir", + "allow": [{ "path": "**/*" }] + }, + { + "identifier": "fs:allow-remove", + "allow": [{ "path": "**/*" }] + }, + { + "identifier": "fs:allow-read-dir", + "allow": [{ "path": "**/*" }] + }, + { + "identifier": "fs:allow-rename", + "allow": [{ "path": "**/*" }] + }, + "fs:allow-appdata-read-recursive", + "fs:allow-appdata-write-recursive", + "dialog:default", + "dialog:allow-open", + "core:path:default", + { + "identifier": "http:default", + "allow": [ + { "url": "https://mirrorchyan.com/**" }, + { "url": "https://mirrorchyan.net/**" }, + { "url": "https://api.github.com/**" }, + { "url": "https://github.com/**" }, + { "url": "https://objects.githubusercontent.com/**" } + ] + }, + "process:allow-restart", + "process:allow-exit" + ] +} diff --git a/src-tauri/gen/android/.editorconfig b/src-tauri/gen/android/.editorconfig new file mode 100644 index 00000000..ebe51d3b --- /dev/null +++ b/src-tauri/gen/android/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = false \ No newline at end of file diff --git a/src-tauri/gen/android/.gitignore b/src-tauri/gen/android/.gitignore new file mode 100644 index 00000000..b2482031 --- /dev/null +++ b/src-tauri/gen/android/.gitignore @@ -0,0 +1,19 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +build +/captures +.externalNativeBuild +.cxx +local.properties +key.properties + +/.tauri +/tauri.settings.gradle \ No newline at end of file diff --git a/src-tauri/gen/android/app/.gitignore b/src-tauri/gen/android/app/.gitignore new file mode 100644 index 00000000..1c854008 --- /dev/null +++ b/src-tauri/gen/android/app/.gitignore @@ -0,0 +1,6 @@ +/src/main/java/com/misteo/mxu/generated +/src/main/jniLibs/**/*.so +/src/main/assets/tauri.conf.json +/tauri.build.gradle.kts +/proguard-tauri.pro +/tauri.properties \ No newline at end of file diff --git a/src-tauri/gen/android/app/build.gradle.kts b/src-tauri/gen/android/app/build.gradle.kts new file mode 100644 index 00000000..d0ef9241 --- /dev/null +++ b/src-tauri/gen/android/app/build.gradle.kts @@ -0,0 +1,70 @@ +import java.util.Properties + +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("rust") +} + +val tauriProperties = Properties().apply { + val propFile = file("tauri.properties") + if (propFile.exists()) { + propFile.inputStream().use { load(it) } + } +} + +android { + compileSdk = 36 + namespace = "com.misteo.mxu" + defaultConfig { + manifestPlaceholders["usesCleartextTraffic"] = "false" + applicationId = "com.misteo.mxu" + minSdk = 24 + targetSdk = 36 + versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt() + versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0") + } + buildTypes { + getByName("debug") { + manifestPlaceholders["usesCleartextTraffic"] = "true" + isDebuggable = true + isJniDebuggable = true + isMinifyEnabled = false + packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so") + jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so") + jniLibs.keepDebugSymbols.add("*/x86/*.so") + jniLibs.keepDebugSymbols.add("*/x86_64/*.so") + } + } + getByName("release") { + isMinifyEnabled = true + proguardFiles( + *fileTree(".") { include("**/*.pro") } + .plus(getDefaultProguardFile("proguard-android-optimize.txt")) + .toList().toTypedArray() + ) + } + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + buildConfig = true + } +} + +rust { + rootDirRel = "../../../" +} + +dependencies { + implementation("androidx.webkit:webkit:1.14.0") + implementation("androidx.appcompat:appcompat:1.7.1") + implementation("androidx.activity:activity-ktx:1.10.1") + implementation("com.google.android.material:material:1.12.0") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.4") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0") +} + +apply(from = "tauri.build.gradle.kts") \ No newline at end of file diff --git a/src-tauri/gen/android/app/proguard-rules.pro b/src-tauri/gen/android/app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/src-tauri/gen/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/AndroidManifest.xml b/src-tauri/gen/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..7be59ded --- /dev/null +++ b/src-tauri/gen/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src-tauri/gen/android/app/src/main/java/com/misteo/mxu/MainActivity.kt b/src-tauri/gen/android/app/src/main/java/com/misteo/mxu/MainActivity.kt new file mode 100644 index 00000000..8d1c1463 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/java/com/misteo/mxu/MainActivity.kt @@ -0,0 +1,11 @@ +package com.misteo.mxu + +import android.os.Bundle +import androidx.activity.enableEdgeToEdge + +class MainActivity : TauriActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + } +} diff --git a/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..2b068d11 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/drawable/ic_launcher_background.xml b/src-tauri/gen/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..07d5da9c --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src-tauri/gen/android/app/src/main/res/layout/activity_main.xml b/src-tauri/gen/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..4fc24441 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..28f1aa11 Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..85d0c88a Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 00000000..28f1aa11 Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..73e48dbf Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..13dd2147 Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000..73e48dbf Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..1d98044f Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..a888b336 Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000..1d98044f Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..08183246 Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..a2a838e7 Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..08183246 Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..b18bceb6 Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..3f8a57f3 Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..b18bceb6 Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/src-tauri/gen/android/app/src/main/res/values-night/themes.xml b/src-tauri/gen/android/app/src/main/res/values-night/themes.xml new file mode 100644 index 00000000..2e17dfd9 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/values-night/themes.xml @@ -0,0 +1,6 @@ + + + + diff --git a/src-tauri/gen/android/app/src/main/res/values/colors.xml b/src-tauri/gen/android/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..f8c6127d --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/values/strings.xml b/src-tauri/gen/android/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..df2cb431 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + mxu + mxu + \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/values/themes.xml b/src-tauri/gen/android/app/src/main/res/values/themes.xml new file mode 100644 index 00000000..2e17dfd9 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/values/themes.xml @@ -0,0 +1,6 @@ + + + + diff --git a/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml b/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 00000000..782d63b9 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src-tauri/gen/android/build.gradle.kts b/src-tauri/gen/android/build.gradle.kts new file mode 100644 index 00000000..d3b07660 --- /dev/null +++ b/src-tauri/gen/android/build.gradle.kts @@ -0,0 +1,26 @@ +buildscript { + repositories { + maven { url = uri("https://mirrors.cloud.tencent.com/nexus/repository/maven-public/") } + maven { url = uri("https://mirrors.cloud.tencent.com/repository/maven-google/") } + google() + mavenCentral() + } + dependencies { + classpath("com.android.tools.build:gradle:8.11.0") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25") + } +} + +allprojects { + repositories { + maven { url = uri("https://mirrors.cloud.tencent.com/nexus/repository/maven-public/") } + maven { url = uri("https://mirrors.cloud.tencent.com/repository/maven-google/") } + google() + mavenCentral() + } +} + +tasks.register("clean").configure { + delete("build") +} + diff --git a/src-tauri/gen/android/buildSrc/build.gradle.kts b/src-tauri/gen/android/buildSrc/build.gradle.kts new file mode 100644 index 00000000..5c55bba7 --- /dev/null +++ b/src-tauri/gen/android/buildSrc/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + `kotlin-dsl` +} + +gradlePlugin { + plugins { + create("pluginsForCoolKids") { + id = "rust" + implementationClass = "RustPlugin" + } + } +} + +repositories { + google() + mavenCentral() +} + +dependencies { + compileOnly(gradleApi()) + implementation("com.android.tools.build:gradle:8.11.0") +} + diff --git a/src-tauri/gen/android/buildSrc/src/main/java/com/misteo/mxu/kotlin/BuildTask.kt b/src-tauri/gen/android/buildSrc/src/main/java/com/misteo/mxu/kotlin/BuildTask.kt new file mode 100644 index 00000000..a3de1256 --- /dev/null +++ b/src-tauri/gen/android/buildSrc/src/main/java/com/misteo/mxu/kotlin/BuildTask.kt @@ -0,0 +1,68 @@ +import java.io.File +import org.apache.tools.ant.taskdefs.condition.Os +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.logging.LogLevel +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction + +open class BuildTask : DefaultTask() { + @Input + var rootDirRel: String? = null + @Input + var target: String? = null + @Input + var release: Boolean? = null + + @TaskAction + fun assemble() { + val executable = """npm"""; + try { + runTauriCli(executable) + } catch (e: Exception) { + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + // Try different Windows-specific extensions + val fallbacks = listOf( + "$executable.exe", + "$executable.cmd", + "$executable.bat", + ) + + var lastException: Exception = e + for (fallback in fallbacks) { + try { + runTauriCli(fallback) + return + } catch (fallbackException: Exception) { + lastException = fallbackException + } + } + throw lastException + } else { + throw e; + } + } + } + + fun runTauriCli(executable: String) { + val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null") + val target = target ?: throw GradleException("target cannot be null") + val release = release ?: throw GradleException("release cannot be null") + val args = listOf("run", "--", "tauri", "android", "android-studio-script"); + + project.exec { + workingDir(File(project.projectDir, rootDirRel)) + executable(executable) + args(args) + if (project.logger.isEnabled(LogLevel.DEBUG)) { + args("-vv") + } else if (project.logger.isEnabled(LogLevel.INFO)) { + args("-v") + } + if (release) { + args("--release") + } + args(listOf("--target", target)) + }.assertNormalExitValue() + } +} \ No newline at end of file diff --git a/src-tauri/gen/android/buildSrc/src/main/java/com/misteo/mxu/kotlin/RustPlugin.kt b/src-tauri/gen/android/buildSrc/src/main/java/com/misteo/mxu/kotlin/RustPlugin.kt new file mode 100644 index 00000000..4aa7fcaf --- /dev/null +++ b/src-tauri/gen/android/buildSrc/src/main/java/com/misteo/mxu/kotlin/RustPlugin.kt @@ -0,0 +1,85 @@ +import com.android.build.api.dsl.ApplicationExtension +import org.gradle.api.DefaultTask +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.get + +const val TASK_GROUP = "rust" + +open class Config { + lateinit var rootDirRel: String +} + +open class RustPlugin : Plugin { + private lateinit var config: Config + + override fun apply(project: Project) = with(project) { + config = extensions.create("rust", Config::class.java) + + val defaultAbiList = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64"); + val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList + + val defaultArchList = listOf("arm64", "arm", "x86", "x86_64"); + val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList + + val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf("aarch64", "armv7", "i686", "x86_64") + + extensions.configure { + @Suppress("UnstableApiUsage") + flavorDimensions.add("abi") + productFlavors { + create("universal") { + dimension = "abi" + ndk { + abiFilters += abiList + } + } + defaultArchList.forEachIndexed { index, arch -> + create(arch) { + dimension = "abi" + ndk { + abiFilters.add(defaultAbiList[index]) + } + } + } + } + } + + afterEvaluate { + for (profile in listOf("debug", "release")) { + val profileCapitalized = profile.replaceFirstChar { it.uppercase() } + val buildTask = tasks.maybeCreate( + "rustBuildUniversal$profileCapitalized", + DefaultTask::class.java + ).apply { + group = TASK_GROUP + description = "Build dynamic library in $profile mode for all targets" + } + + tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask) + + for (targetPair in targetsList.withIndex()) { + val targetName = targetPair.value + val targetArch = archList[targetPair.index] + val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() } + val targetBuildTask = project.tasks.maybeCreate( + "rustBuild$targetArchCapitalized$profileCapitalized", + BuildTask::class.java + ).apply { + group = TASK_GROUP + description = "Build dynamic library in $profile mode for $targetArch" + rootDirRel = config.rootDirRel + target = targetName + release = profile == "release" + } + + buildTask.dependsOn(targetBuildTask) + tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn( + targetBuildTask + ) + } + } + } + } +} \ No newline at end of file diff --git a/src-tauri/gen/android/gradle.properties b/src-tauri/gen/android/gradle.properties new file mode 100644 index 00000000..2a7ec695 --- /dev/null +++ b/src-tauri/gen/android/gradle.properties @@ -0,0 +1,24 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true +android.nonFinalResIds=false \ No newline at end of file diff --git a/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar b/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..e708b1c0 Binary files /dev/null and b/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties b/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..72d5c94e --- /dev/null +++ b/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue May 10 19:22:52 CST 2022 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.14.3-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/src-tauri/gen/android/gradlew b/src-tauri/gen/android/gradlew new file mode 100644 index 00000000..4f906e0c --- /dev/null +++ b/src-tauri/gen/android/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/src-tauri/gen/android/gradlew.bat b/src-tauri/gen/android/gradlew.bat new file mode 100644 index 00000000..107acd32 --- /dev/null +++ b/src-tauri/gen/android/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src-tauri/gen/android/settings.gradle b/src-tauri/gen/android/settings.gradle new file mode 100644 index 00000000..39391166 --- /dev/null +++ b/src-tauri/gen/android/settings.gradle @@ -0,0 +1,3 @@ +include ':app' + +apply from: 'tauri.settings.gradle' diff --git a/src-tauri/src/commands/maa_core.rs b/src-tauri/src/commands/maa_core.rs index adf36cc2..052bf875 100644 --- a/src-tauri/src/commands/maa_core.rs +++ b/src-tauri/src/commands/maa_core.rs @@ -139,7 +139,7 @@ pub fn maa_init(state: State>, lib_dir: Option) -> Result< let name = "MaaFramework.dll"; #[cfg(target_os = "macos")] let name = "libMaaFramework.dylib"; - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "android"))] let name = "libMaaFramework.so"; lib_path.join(name) }; @@ -208,7 +208,7 @@ pub fn maa_check_version(state: State>) -> Result>, class_regex: Option, window_regex: Option, ) -> Result, String> { - info!( - "maa_find_win32_windows called, class_regex: {:?}, window_regex: {:?}", - class_regex, window_regex - ); + #[cfg(mobile)] + { + let _ = (&state, &class_regex, &window_regex); + return Ok(Vec::new()); + } - let state_arc = state.inner().clone(); - let class_re_str = class_regex.clone(); - let window_re_str = window_regex.clone(); + #[cfg(desktop)] + { + info!( + "maa_find_win32_windows called, class_regex: {:?}, window_regex: {:?}", + class_regex, window_regex + ); - tauri::async_runtime::spawn_blocking(move || { - let windows = Toolkit::find_desktop_windows().map_err(|e| e.to_string())?; - - // 编译正则表达式 - let class_re = class_re_str - .as_ref() - .and_then(|r| regex::Regex::new(r).ok()); - let window_re = window_re_str - .as_ref() - .and_then(|r| regex::Regex::new(r).ok()); - - let mut result_windows = Vec::new(); - - for w in windows { - // 过滤 - if let Some(re) = &class_re { - if !re.is_match(&w.class_name) { - continue; + let state_arc = state.inner().clone(); + let class_re_str = class_regex.clone(); + let window_re_str = window_regex.clone(); + + tauri::async_runtime::spawn_blocking(move || { + let windows = Toolkit::find_desktop_windows().map_err(|e| e.to_string())?; + + // 编译正则表达式 + let class_re = class_re_str + .as_ref() + .and_then(|r| regex::Regex::new(r).ok()); + let window_re = window_re_str + .as_ref() + .and_then(|r| regex::Regex::new(r).ok()); + + let mut result_windows = Vec::new(); + + for w in windows { + // 过滤 + if let Some(re) = &class_re { + if !re.is_match(&w.class_name) { + continue; + } } - } - if let Some(re) = &window_re { - if !re.is_match(&w.window_name) { - continue; + if let Some(re) = &window_re { + if !re.is_match(&w.window_name) { + continue; + } } - } - result_windows.push(Win32Window { - handle: w.hwnd as u64, - class_name: w.class_name, - window_name: w.window_name, - }); - } + result_windows.push(Win32Window { + handle: w.hwnd as u64, + class_name: w.class_name, + window_name: w.window_name, + }); + } - // 缓存搜索结果 - if let Ok(mut cached) = state_arc.cached_win32_windows.lock() { - *cached = result_windows.clone(); - } + // 缓存搜索结果 + if let Ok(mut cached) = state_arc.cached_win32_windows.lock() { + *cached = result_windows.clone(); + } - info!("Returning {} filtered window(s)", result_windows.len()); - Ok(result_windows) - }) - .await - .map_err(|e| e.to_string())? + info!("Returning {} filtered window(s)", result_windows.len()); + Ok(result_windows) + }) + .await + .map_err(|e| e.to_string())? + } } // ============================================================================ diff --git a/src-tauri/src/commands/system.rs b/src-tauri/src/commands/system.rs index 9d755b5d..57c330f2 100644 --- a/src-tauri/src/commands/system.rs +++ b/src-tauri/src/commands/system.rs @@ -2,7 +2,9 @@ //! //! 提供权限检查、系统信息查询、全局选项设置等功能 -use log::{info, warn}; +use log::info; +#[cfg(desktop)] +use log::warn; use std::sync::atomic::{AtomicBool, Ordering}; use super::types::SystemInfo; @@ -57,7 +59,12 @@ pub fn is_elevated() -> bool { } } - #[cfg(not(windows))] + #[cfg(target_os = "android")] + { + false + } + + #[cfg(not(any(windows, target_os = "android")))] { // 非 Windows 平台:检查是否为 root unsafe { libc::geteuid() == 0 } @@ -166,6 +173,13 @@ pub async fn open_file(file_path: String) -> Result<(), String> { .map_err(|e| format!("Failed to open file: {}", e))?; } + #[cfg(target_os = "android")] + { + // Android 上通过 Intent 打开文件由前端处理, + // Rust 侧仅记录日志 + info!("open_file on Android: delegating to frontend for {}", file_path); + } + Ok(()) } @@ -196,230 +210,240 @@ pub async fn run_and_wait(file_path: String) -> Result { /// 检查指定程序是否正在运行(通过完整路径比较,避免同名程序误判) /// 公共工具函数,可被其他模块调用 pub fn check_process_running(program: &str) -> bool { - use std::path::PathBuf; - - let resolved_path = PathBuf::from(program); - - // 尝试规范化路径用于精确比较 - let canonical_target = resolved_path - .canonicalize() - .unwrap_or_else(|_| resolved_path.clone()); - - // 提取文件名用于 Windows 下的初步筛选 - #[cfg(windows)] - let file_name = match resolved_path.file_name() { - Some(name) => name.to_string_lossy().to_string(), - None => { - log::warn!( - "check_process_running: cannot extract filename from '{}'", - program - ); - return false; - } - }; - - #[cfg(windows)] + #[cfg(target_os = "android")] { - use windows::Win32::Foundation::CloseHandle; - use windows::Win32::System::Diagnostics::ToolHelp::{ - CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W, - TH32CS_SNAPPROCESS, - }; - use windows::Win32::System::Threading::{ - OpenProcess, QueryFullProcessImageNameW, PROCESS_NAME_FORMAT, - PROCESS_QUERY_LIMITED_INFORMATION, - }; + let _ = program; + false + } - let file_name_lower = file_name.to_lowercase(); - - /// 动态扩容获取进程完整路径,处理长路径(>MAX_PATH)场景 - unsafe fn query_process_image_path( - process: windows::Win32::Foundation::HANDLE, - ) -> Option { - let mut capacity: u32 = 512; - loop { - let mut buf = vec![0u16; capacity as usize]; - let mut size = capacity; - let result = QueryFullProcessImageNameW( - process, - PROCESS_NAME_FORMAT(0), - windows::core::PWSTR(buf.as_mut_ptr()), - &mut size, + #[cfg(not(target_os = "android"))] + { + use std::path::PathBuf; + let resolved_path = PathBuf::from(program); + + // 尝试规范化路径用于精确比较 + let canonical_target = resolved_path + .canonicalize() + .unwrap_or_else(|_| resolved_path.clone()); + + // 提取文件名用于 Windows 下的初步筛选 + #[cfg(windows)] + let file_name = match resolved_path.file_name() { + Some(name) => name.to_string_lossy().to_string(), + None => { + log::warn!( + "check_process_running: cannot extract filename from '{}'", + program ); - if result.is_ok() { - return Some(String::from_utf16_lossy(&buf[..size as usize])); - } - // ERROR_INSUFFICIENT_BUFFER 对应 HRESULT 0x8007007A,仅此错误时扩容重试 - let err = windows::core::Error::from_win32(); - if err.code().0 as u32 != 0x8007007A || capacity >= 32768 { - // 非缓冲区不足错误或已达上限,放弃 - return None; - } - capacity *= 2; + return false; } - } + }; - unsafe { - let snapshot = match CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) { - Ok(h) => h, - Err(e) => { - log::error!( - "check_process_running: CreateToolhelp32Snapshot failed: {}", - e - ); - return false; - } + #[cfg(windows)] + { + use windows::Win32::Foundation::CloseHandle; + use windows::Win32::System::Diagnostics::ToolHelp::{ + CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W, + TH32CS_SNAPPROCESS, }; - - let mut entry = PROCESSENTRY32W { - dwSize: std::mem::size_of::() as u32, - ..Default::default() + use windows::Win32::System::Threading::{ + OpenProcess, QueryFullProcessImageNameW, PROCESS_NAME_FORMAT, + PROCESS_QUERY_LIMITED_INFORMATION, }; - let target_lower = canonical_target.to_string_lossy().to_lowercase(); + let file_name_lower = file_name.to_lowercase(); - if Process32FirstW(snapshot, &mut entry).is_ok() { + /// 动态扩容获取进程完整路径,处理长路径(>MAX_PATH)场景 + unsafe fn query_process_image_path( + process: windows::Win32::Foundation::HANDLE, + ) -> Option { + let mut capacity: u32 = 512; loop { - // 从 szExeFile (UTF-16) 提取进程名 - let len = entry - .szExeFile - .iter() - .position(|&c| c == 0) - .unwrap_or(entry.szExeFile.len()); - let exe_name = String::from_utf16_lossy(&entry.szExeFile[..len]).to_lowercase(); - - // 先按文件名筛选 - if exe_name == file_name_lower { - // 尝试获取完整路径 - if let Ok(process) = OpenProcess( - PROCESS_QUERY_LIMITED_INFORMATION, - false, - entry.th32ProcessID, - ) { - if let Some(running_path) = query_process_image_path(process) { - let running_canonical = PathBuf::from(&running_path) - .canonicalize() - .map(|p| p.to_string_lossy().to_lowercase()) - .unwrap_or_else(|_| running_path.to_lowercase()); - - if running_canonical == target_lower { - let _ = CloseHandle(process); - let _ = CloseHandle(snapshot); - info!( - "check_process_running: '{}' -> true (matched: {})", - program, running_path - ); - return true; + let mut buf = vec![0u16; capacity as usize]; + let mut size = capacity; + let result = QueryFullProcessImageNameW( + process, + PROCESS_NAME_FORMAT(0), + windows::core::PWSTR(buf.as_mut_ptr()), + &mut size, + ); + if result.is_ok() { + return Some(String::from_utf16_lossy(&buf[..size as usize])); + } + // ERROR_INSUFFICIENT_BUFFER 对应 HRESULT 0x8007007A,仅此错误时扩容重试 + let err = windows::core::Error::from_win32(); + if err.code().0 as u32 != 0x8007007A || capacity >= 32768 { + // 非缓冲区不足错误或已达上限,放弃 + return None; + } + capacity *= 2; + } + } + + unsafe { + let snapshot = match CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) { + Ok(h) => h, + Err(e) => { + log::error!( + "check_process_running: CreateToolhelp32Snapshot failed: {}", + e + ); + return false; + } + }; + + let mut entry = PROCESSENTRY32W { + dwSize: std::mem::size_of::() as u32, + ..Default::default() + }; + + let target_lower = canonical_target.to_string_lossy().to_lowercase(); + + if Process32FirstW(snapshot, &mut entry).is_ok() { + loop { + // 从 szExeFile (UTF-16) 提取进程名 + let len = entry + .szExeFile + .iter() + .position(|&c| c == 0) + .unwrap_or(entry.szExeFile.len()); + let exe_name = + String::from_utf16_lossy(&entry.szExeFile[..len]).to_lowercase(); + + // 先按文件名筛选 + if exe_name == file_name_lower { + // 尝试获取完整路径 + if let Ok(process) = OpenProcess( + PROCESS_QUERY_LIMITED_INFORMATION, + false, + entry.th32ProcessID, + ) { + if let Some(running_path) = query_process_image_path(process) { + let running_canonical = PathBuf::from(&running_path) + .canonicalize() + .map(|p| p.to_string_lossy().to_lowercase()) + .unwrap_or_else(|_| running_path.to_lowercase()); + + if running_canonical == target_lower { + let _ = CloseHandle(process); + let _ = CloseHandle(snapshot); + info!( + "check_process_running: '{}' -> true (matched: {})", + program, running_path + ); + return true; + } } + let _ = CloseHandle(process); } - let _ = CloseHandle(process); } + + if Process32NextW(snapshot, &mut entry).is_err() { + break; + } + } + } + + // 缓存搜索结果 + let _ = CloseHandle(snapshot); + info!("check_process_running: '{}' -> false", program); + false + } + } + + #[cfg(target_os = "linux")] + { + // 遍历 /proc//exe 读取真实可执行路径进行比较 + if let Ok(proc_dir) = std::fs::read_dir("/proc") { + for entry in proc_dir.flatten() { + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + if !name_str.chars().all(|c| c.is_ascii_digit()) { + continue; } - if Process32NextW(snapshot, &mut entry).is_err() { - break; + let exe_link = entry.path().join("exe"); + if let Ok(resolved) = std::fs::read_link(&exe_link) { + let canonical = resolved.canonicalize().unwrap_or(resolved); + if canonical == canonical_target { + info!( + "check_process_running: '{}' -> true (pid: {})", + program, name_str + ); + return true; + } } } } - let _ = CloseHandle(snapshot); info!("check_process_running: '{}' -> false", program); false } - } - #[cfg(target_os = "linux")] - { - // 遍历 /proc//exe 读取真实可执行路径进行比较 - if let Ok(proc_dir) = std::fs::read_dir("/proc") { - for entry in proc_dir.flatten() { - let name = entry.file_name(); - let name_str = name.to_string_lossy(); - if !name_str.chars().all(|c| c.is_ascii_digit()) { - continue; - } + #[cfg(target_os = "macos")] + { + // macOS 没有 /proc,通过 libproc API 获取每个进程的可执行路径进行比较 + extern "C" { + fn proc_listallpids(buffer: *mut i32, buffersize: i32) -> i32; + fn proc_pidpath(pid: i32, buffer: *mut u8, buffersize: u32) -> i32; + } - let exe_link = entry.path().join("exe"); - if let Ok(resolved) = std::fs::read_link(&exe_link) { - let canonical = resolved.canonicalize().unwrap_or(resolved); - if canonical == canonical_target { + unsafe { + // proc_listallpids 返回填入的 PID 数量。 + // 从合理初始容量开始,若缓冲区不足则扩容重试,避免多余的探测调用。 + let mut capacity = 1024usize; + let num_pids; + let mut pids; + loop { + pids = vec![0i32; capacity]; + let buf_size = (capacity * std::mem::size_of::()) as i32; + let actual = proc_listallpids(pids.as_mut_ptr(), buf_size); + if actual <= 0 { info!( - "check_process_running: '{}' -> true (pid: {})", - program, name_str + "check_process_running: '{}' -> false (list failed)", + program ); - return true; + return false; } + if actual as usize >= capacity { + // 缓冲区已满,可能被截断,扩容后重试 + capacity *= 2; + continue; + } + num_pids = actual as usize; + break; } - } - } - - info!("check_process_running: '{}' -> false", program); - false - } - - #[cfg(target_os = "macos")] - { - // macOS 没有 /proc,通过 libproc API 获取每个进程的可执行路径进行比较 - extern "C" { - fn proc_listallpids(buffer: *mut i32, buffersize: i32) -> i32; - fn proc_pidpath(pid: i32, buffer: *mut u8, buffersize: u32) -> i32; - } - - unsafe { - // proc_listallpids 返回填入的 PID 数量。 - // 从合理初始容量开始,若缓冲区不足则扩容重试,避免多余的探测调用。 - let mut capacity = 1024usize; - let num_pids; - let mut pids; - loop { - pids = vec![0i32; capacity]; - let buf_size = (capacity * std::mem::size_of::()) as i32; - let actual = proc_listallpids(pids.as_mut_ptr(), buf_size); - if actual <= 0 { - info!( - "check_process_running: '{}' -> false (list failed)", - program - ); - return false; - } - if actual as usize >= capacity { - // 缓冲区已满,可能被截断,扩容后重试 - capacity *= 2; - continue; - } - num_pids = actual as usize; - break; - } - // PROC_PIDPATHINFO_MAXSIZE = 4096 - let mut path_buf = [0u8; 4096]; + // PROC_PIDPATHINFO_MAXSIZE = 4096 + let mut path_buf = [0u8; 4096]; - for &pid in &pids[..num_pids] { - if pid == 0 { - continue; - } + for &pid in &pids[..num_pids] { + if pid == 0 { + continue; + } - let ret = proc_pidpath(pid, path_buf.as_mut_ptr(), path_buf.len() as u32); - if ret <= 0 { - continue; - } + let ret = proc_pidpath(pid, path_buf.as_mut_ptr(), path_buf.len() as u32); + if ret <= 0 { + continue; + } - if let Ok(path_str) = std::str::from_utf8(&path_buf[..ret as usize]) { - let pid_path = PathBuf::from(path_str); - let canonical = pid_path.canonicalize().unwrap_or(pid_path); - if canonical == canonical_target { - info!( - "check_process_running: '{}' -> true (pid: {})", - program, pid - ); - return true; + if let Ok(path_str) = std::str::from_utf8(&path_buf[..ret as usize]) { + let pid_path = PathBuf::from(path_str); + let canonical = pid_path.canonicalize().unwrap_or(pid_path); + if canonical == canonical_target { + info!( + "check_process_running: '{}' -> true (pid: {})", + program, pid + ); + return true; + } } } } - } - info!("check_process_running: '{}' -> false", program); - false + info!("check_process_running: '{}' -> false", program); + false + } } } @@ -442,55 +466,64 @@ pub async fn run_action( cwd: Option, wait_for_exit: bool, ) -> Result { - use std::process::Command; - - info!( - "run_action: program={}, args={}, wait={}", - program, args, wait_for_exit - ); - - // 使用 shell 语义解析参数至数组(支持引号) - let args_vec: Vec = if args.trim().is_empty() { - vec![] - } else { - shell_words::split(&args).map_err(|e| format!("Failed to parse args: {}", e))? - }; + #[cfg(target_os = "android")] + { + let _ = (program, args, cwd, wait_for_exit); + Err("run_action is not supported on Android".to_string()) + } - let mut cmd = Command::new(&program); + #[cfg(not(target_os = "android"))] + { + use std::process::Command; - // 添加参数 - if !args_vec.is_empty() { - cmd.args(&args_vec); - } + info!( + "run_action: program={}, args={}, wait={}", + program, args, wait_for_exit + ); - // 设置工作目录 - if let Some(ref dir) = cwd { - cmd.current_dir(dir); - } else { - // 默认使用程序所在目录作为工作目录 - if let Some(parent) = std::path::Path::new(&program).parent() { - if parent.exists() { - cmd.current_dir(parent); - } - } - } + // 使用 shell 语义解析参数至数组(支持引号) + let args_vec: Vec = if args.trim().is_empty() { + vec![] + } else { + shell_words::split(&args).map_err(|e| format!("Failed to parse args: {}", e))? + }; - if wait_for_exit { - // 等待进程退出 - let status = cmd - .status() - .map_err(|e| format!("Failed to run action: {} - {}", program, e))?; + let mut cmd = Command::new(&program); - let exit_code = status.code().unwrap_or(-1); - info!("run_action finished with exit code: {}", exit_code); - Ok(exit_code) - } else { - // 不等待,启动后立即返回 - cmd.spawn() - .map_err(|e| format!("Failed to spawn action: {} - {}", program, e))?; + // 添加参数 + if !args_vec.is_empty() { + cmd.args(&args_vec); + } + + // 设置工作目录 + if let Some(ref dir) = cwd { + cmd.current_dir(dir); + } else { + // 默认使用程序所在目录作为工作目录 + if let Some(parent) = std::path::Path::new(&program).parent() { + if parent.exists() { + cmd.current_dir(parent); + } + } + } - info!("run_action spawned (not waiting)"); - Ok(0) // 不等待时返回 0 + if wait_for_exit { + // 等待进程退出 + let status = cmd + .status() + .map_err(|e| format!("Failed to run action: {} - {}", program, e))?; + + let exit_code = status.code().unwrap_or(-1); + info!("run_action finished with exit code: {}", exit_code); + Ok(exit_code) + } else { + // 不等待,启动后立即返回 + cmd.spawn() + .map_err(|e| format!("Failed to spawn action: {} - {}", program, e))?; + + info!("run_action spawned (not waiting)"); + Ok(0) // 不等待时返回 0 + } } } @@ -509,7 +542,7 @@ pub async fn retry_load_maa_library() -> Result { let dll_path = maafw_dir.join("MaaFramework.dll"); #[cfg(target_os = "macos")] let dll_path = maafw_dir.join("libMaaFramework.dylib"); - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "android"))] let dll_path = maafw_dir.join("libMaaFramework.so"); maa_framework::load_library(&dll_path).map_err(|e| e.to_string())?; @@ -537,6 +570,7 @@ pub fn is_autostart() -> bool { } /// 自动迁移旧版注册表自启动到任务计划程序 +//TODO:26年2月写的,应该过几个月这自动迁移就能去除了,等旧版的都更上来 #[cfg(windows)] pub fn migrate_legacy_autostart() { if has_legacy_registry_autostart() { @@ -572,7 +606,7 @@ fn create_schtask_autostart() -> Result<(), String> { &format!("\"{}\" --autostart", exe), "/sc", "onlogon", - // 登录后延迟 30 秒再启动,降低桌面会话尚未完全就绪时的白屏/卡死概率 + // 登录后延迟 30 秒再启动,降低桌面会话尚未就绪时的白屏/卡死概率 "/delay", "0000:30", // 强制交互式运行,确保进程绑定到用户桌面会话,避免登录早期会话未就绪导致 WebView 白屏 @@ -774,16 +808,17 @@ pub fn get_system_info() -> SystemInfo { /// 获取当前使用的 WebView2 目录 #[tauri::command] pub fn get_webview2_dir() -> WebView2DirInfo { - if let Ok(folder) = std::env::var("WEBVIEW2_BROWSER_EXECUTABLE_FOLDER") { - WebView2DirInfo { - path: folder, - system: false, - } - } else { - // 没有设置自定义目录,使用系统 WebView2 - WebView2DirInfo { - path: String::new(), - system: true, + #[cfg(desktop)] + { + if let Ok(folder) = std::env::var("WEBVIEW2_BROWSER_EXECUTABLE_FOLDER") { + return WebView2DirInfo { + path: folder, + system: false, + }; } } + WebView2DirInfo { + path: String::new(), + system: true, + } } diff --git a/src-tauri/src/commands/tray.rs b/src-tauri/src/commands/tray.rs index ed4d6411..87be44d7 100644 --- a/src-tauri/src/commands/tray.rs +++ b/src-tauri/src/commands/tray.rs @@ -1,28 +1,48 @@ //! 托盘相关命令 - -use crate::tray; +//! +//! 桌面端委托给 crate::tray 模块,移动端提供空实现 /// 设置关闭时是否最小化到托盘 #[tauri::command] pub fn set_minimize_to_tray(enabled: bool) { - tray::set_minimize_to_tray(enabled); - log::info!("Minimize to tray: {}", enabled); + #[cfg(desktop)] + { + crate::tray::set_minimize_to_tray(enabled); + log::info!("Minimize to tray: {}", enabled); + } + #[cfg(mobile)] + let _ = enabled; } /// 获取关闭时是否最小化到托盘的设置 #[tauri::command] pub fn get_minimize_to_tray() -> bool { - tray::get_minimize_to_tray() + #[cfg(desktop)] + return crate::tray::get_minimize_to_tray(); + #[cfg(mobile)] + false } /// 更新托盘图标 #[tauri::command] pub fn update_tray_icon(icon_path: String) -> Result<(), String> { - tray::update_tray_icon(&icon_path) + #[cfg(desktop)] + return crate::tray::update_tray_icon(&icon_path); + #[cfg(mobile)] + { + let _ = icon_path; + Ok(()) + } } /// 更新托盘 tooltip #[tauri::command] pub fn update_tray_tooltip(tooltip: String) -> Result<(), String> { - tray::update_tray_tooltip(&tooltip) + #[cfg(desktop)] + return crate::tray::update_tray_tooltip(&tooltip); + #[cfg(mobile)] + { + let _ = tooltip; + Ok(()) + } } diff --git a/src-tauri/src/commands/utils.rs b/src-tauri/src/commands/utils.rs index e8467ea3..f24588a2 100644 --- a/src-tauri/src/commands/utils.rs +++ b/src-tauri/src/commands/utils.rs @@ -19,6 +19,7 @@ pub fn emit_callback_event>(app: &AppHandle, message: S, details /// 获取应用数据目录 /// - macOS: ~/Library/Application Support/MXU/ +/// - Android: /data/data//files/ (通过环境变量或回退路径) /// - Windows/Linux: exe 所在目录(保持便携式部署) pub fn get_app_data_dir() -> Result { #[cfg(target_os = "macos")] @@ -31,13 +32,38 @@ pub fn get_app_data_dir() -> Result { Ok(path) } - #[cfg(not(target_os = "macos"))] + #[cfg(target_os = "android")] + { + get_android_data_dir() + } + + #[cfg(not(any(target_os = "macos", target_os = "android")))] { // Windows/Linux 保持便携式,使用 exe 所在目录 get_exe_directory() } } +/// Android 数据目录获取 +/// 优先使用 TAURI_ANDROID_DATA_DIR (由 Tauri 框架在启动时设置), +/// 回退到 /data/data/com.misteo.mxu/files +#[cfg(target_os = "android")] +fn get_android_data_dir() -> Result { + if let Ok(dir) = std::env::var("TAURI_ANDROID_DATA_DIR") { + return Ok(PathBuf::from(dir)); + } + // 回退路径:Android app 内部存储 + let fallback = PathBuf::from("/data/data/com.misteo.mxu/files"); + if fallback.exists() { + return Ok(fallback); + } + // 最终回退:使用 Android 标准 app data 目录模式 + if let Ok(home) = std::env::var("HOME") { + return Ok(PathBuf::from(home)); + } + Err("无法确定 Android 数据目录".to_string()) +} + /// 规范化路径:移除冗余的 `.`、处理 `..`、统一分隔符 /// 使用 Path::components() 解析,不需要路径实际存在 pub fn normalize_path(path: &str) -> PathBuf { @@ -82,17 +108,55 @@ pub fn get_logs_dir() -> PathBuf { } /// 获取 exe 所在目录路径(内部使用) +/// Android 上回退到数据目录 pub fn get_exe_directory() -> Result { - let exe_path = std::env::current_exe().map_err(|e| format!("获取 exe 路径失败: {}", e))?; - exe_path - .parent() - .map(|p| p.to_path_buf()) - .ok_or_else(|| "无法获取 exe 所在目录".to_string()) + #[cfg(not(target_os = "android"))] + { + let exe_path = + std::env::current_exe().map_err(|e| format!("获取 exe 路径失败: {}", e))?; + exe_path + .parent() + .map(|p| p.to_path_buf()) + .ok_or_else(|| "无法获取 exe 所在目录".to_string()) + } + + #[cfg(target_os = "android")] + { + get_app_data_dir() + } } -/// 获取可执行文件所在目录下的 maafw 子目录 +/// 获取 MaaFramework 库目录 +/// - 桌面端: exe 目录下的 maafw 子目录 +/// - Android: native library 目录 (jniLibs 解压后的路径) pub fn get_maafw_dir() -> Result { - Ok(get_exe_directory()?.join("maafw")) + #[cfg(not(target_os = "android"))] + { + Ok(get_exe_directory()?.join("maafw")) + } + + #[cfg(target_os = "android")] + { + // Android 上 .so 库打包在 APK 的 jniLibs 中, + // 安装后位于 nativeLibraryDir + if let Ok(lib_dir) = std::env::var("TAURI_ANDROID_NATIVE_LIB_DIR") { + return Ok(PathBuf::from(lib_dir)); + } + // 回退:尝试从 /proc/self/maps 中推断 native lib 路径 + if let Ok(maps) = std::fs::read_to_string("/proc/self/maps") { + for line in maps.lines() { + if line.contains("libmxu_lib.so") { + if let Some(path) = line.split_whitespace().last() { + if let Some(parent) = std::path::Path::new(path).parent() { + return Ok(parent.to_path_buf()); + } + } + } + } + } + // 最终回退:应用数据目录下的 maafw + Ok(get_app_data_dir()?.join("maafw")) + } } /// 构建 User-Agent 字符串 @@ -101,5 +165,8 @@ pub fn build_user_agent() -> String { let os = std::env::consts::OS; let arch = std::env::consts::ARCH; let tauri_version = tauri::VERSION; - format!("MXU/{} ({}; {}) Tauri/{}", version, os, arch, tauri_version) + format!( + "MXU/{} ({}; {}) Tauri/{}", + version, os, arch, tauri_version + ) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 504e4114..89883c72 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,5 +1,6 @@ pub mod commands; mod mxu_actions; +#[cfg(desktop)] mod tray; use commands::MaaState; @@ -20,17 +21,13 @@ pub fn run() { #[cfg(windows)] commands::system::migrate_legacy_autostart(); - tauri::Builder::default() + #[allow(unused_mut)] + let mut builder = tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_process::init()) - .plugin(tauri_plugin_global_shortcut::Builder::new().build()) - .plugin(tauri_plugin_autostart::init( - tauri_plugin_autostart::MacosLauncher::LaunchAgent, - Some(vec!["--autostart".into()]), - )) .plugin( tauri_plugin_log::Builder::new() .targets([ @@ -45,7 +42,19 @@ pub fn run() { .timezone_strategy(TimezoneStrategy::UseLocal) .level(log::LevelFilter::Debug) .build(), - ) + ); + + #[cfg(desktop)] + { + builder = builder + .plugin(tauri_plugin_global_shortcut::Builder::new().build()) + .plugin(tauri_plugin_autostart::init( + tauri_plugin_autostart::MacosLauncher::LaunchAgent, + Some(vec!["--autostart".into()]), + )); + } + + builder .setup(|app| { // 创建 MaaState 并注册为 Tauri 管理状态 let maa_state = Arc::new(MaaState::default()); @@ -82,38 +91,14 @@ pub fn run() { } // 启动时自动加载 MaaFramework DLL - if let Ok(maafw_dir) = commands::get_maafw_dir() { - if maafw_dir.exists() { - #[cfg(windows)] - let dll_path = maafw_dir.join("MaaFramework.dll"); - #[cfg(target_os = "macos")] - let dll_path = maafw_dir.join("libMaaFramework.dylib"); - #[cfg(target_os = "linux")] - let dll_path = maafw_dir.join("libMaaFramework.so"); - - match maa_framework::load_library(&dll_path) { - Ok(()) => log::info!("MaaFramework loaded from {:?}", dll_path), - Err(e) => { - log::error!("Failed to load MaaFramework: {}", e); - // 检查是否是 DLL 存在但加载失败的情况(可能是运行库缺失) - if dll_path.exists() { - log::warn!( - "DLLs exist but failed to load, possibly missing VC++ runtime: {}", - e - ); - // 设置标记,前端加载完成后会查询此标记 - commands::system::set_vcredist_missing(true); - } - } - } - } else { - log::warn!("MaaFramework directory not found: {:?}", maafw_dir); - } - } + load_maa_framework(); // 初始化系统托盘 - if let Err(e) = tray::init_tray(app.handle()) { - log::error!("Failed to initialize system tray: {}", e); + #[cfg(desktop)] + { + if let Err(e) = tray::init_tray(app.handle()) { + log::error!("Failed to initialize system tray: {}", e); + } } Ok(()) @@ -199,9 +184,12 @@ pub fn run() { match event { // 窗口关闭请求:检查是否最小化到托盘 tauri::WindowEvent::CloseRequested { api, .. } => { + #[cfg(desktop)] if tray::handle_close_requested(window.app_handle()) { api.prevent_close(); } + #[cfg(mobile)] + let _ = (window, api); } // 窗口销毁时清理所有 agent 子进程 tauri::WindowEvent::Destroyed => { @@ -215,3 +203,42 @@ pub fn run() { .run(tauri::generate_context!()) .expect("error while running tauri application"); } + +fn load_maa_framework() { + let maafw_dir = match commands::get_maafw_dir() { + Ok(dir) => dir, + Err(e) => { + log::warn!("Failed to get MaaFramework dir: {}", e); + return; + } + }; + + if !maafw_dir.exists() { + log::warn!("MaaFramework directory not found: {:?}", maafw_dir); + return; + } + + #[cfg(windows)] + let dll_path = maafw_dir.join("MaaFramework.dll"); + #[cfg(target_os = "macos")] + let dll_path = maafw_dir.join("libMaaFramework.dylib"); + #[cfg(any(target_os = "linux", target_os = "android"))] + let dll_path = maafw_dir.join("libMaaFramework.so"); + + match maa_framework::load_library(&dll_path) { + Ok(()) => log::info!("MaaFramework loaded from {:?}", dll_path), + Err(e) => { + log::error!("Failed to load MaaFramework: {}", e); + // 检查是否是 DLL 存在但加载失败的情况(可能是运行库缺失) + #[cfg(desktop)] + if dll_path.exists() { + log::warn!( + "DLLs exist but failed to load, possibly missing runtime: {}", + e + ); + // 设置标记,前端加载完成后会查询此标记 + commands::system::set_vcredist_missing(true); + } + } + } +} diff --git a/src-tauri/src/mxu_actions.rs b/src-tauri/src/mxu_actions.rs index db7fcccf..387fc046 100644 --- a/src-tauri/src/mxu_actions.rs +++ b/src-tauri/src/mxu_actions.rs @@ -51,7 +51,6 @@ fn mxu_sleep_action_fn( let param_str = args.param; info!("[MXU_SLEEP] Received param: {}", param_str); - // 解析 JSON 获取 sleep_time let sleep_seconds: u64 = match serde_json::from_str::(param_str) { Ok(json) => json.get("sleep_time").and_then(|v| v.as_u64()).unwrap_or(5), Err(e) => { @@ -65,7 +64,6 @@ fn mxu_sleep_action_fn( info!("[MXU_SLEEP] Sleeping for {} seconds...", sleep_seconds); - // 执行可中断睡眠(响应 stop) if !wait_with_stop_check(ctx, sleep_seconds) { warn!("[MXU_SLEEP] Interrupted by stop request"); return false; @@ -79,12 +77,8 @@ fn mxu_sleep_action_fn( // MXU_WAITUNTIL Custom Action // ============================================================================ -/// MXU_WAITUNTIL 动作名称常量 const MXU_WAITUNTIL_ACTION: &str = "MXU_WAITUNTIL_ACTION"; -/// MXU_WAITUNTIL custom action 回调函数 -/// 从 custom_action_param 中读取 target_time(HH:MM 格式),等待到该时间点 -/// 仅支持 24 小时内:若目标时间已过则等待到次日该时间 fn mxu_waituntil_action_fn( ctx: &maa_framework::context::Context, args: &maa_framework::custom::ActionArgs, @@ -107,7 +101,6 @@ fn mxu_waituntil_action_fn( }; let target_time = target_time.to_string(); - // 解析 HH:MM 格式 let parts: Vec<&str> = target_time.split(':').collect(); if parts.len() < 2 { warn!("[MXU_WAITUNTIL] Invalid time format: {}", target_time); @@ -130,7 +123,6 @@ fn mxu_waituntil_action_fn( } }; - // 计算当前时间与目标时间的差值 let now = chrono::Local::now(); let Some(today_target) = now.date_naive().and_hms_opt(target_hour, target_minute, 0) else { warn!( @@ -154,7 +146,6 @@ fn mxu_waituntil_action_fn( let wait_duration = if today_target > now { today_target - now } else { - // 目标时间已过,等到明天 let tomorrow_target = today_target + chrono::Duration::days(1); tomorrow_target - now }; @@ -175,14 +166,11 @@ fn mxu_waituntil_action_fn( } // ============================================================================ -// MXU_LAUNCH Custom Action +// MXU_LAUNCH Custom Action (desktop only) // ============================================================================ -/// MXU_LAUNCH 动作名称常量 const MXU_LAUNCH_ACTION: &str = "MXU_LAUNCH_ACTION"; -/// MXU_LAUNCH custom action 回调函数 -/// 从 custom_action_param 中读取 program, args, wait_for_exit,启动外部程序 fn mxu_launch_action_fn( _ctx: &maa_framework::context::Context, args: &maa_framework::custom::ActionArgs, @@ -190,103 +178,110 @@ fn mxu_launch_action_fn( let param_str = args.param; info!("[MXU_LAUNCH] Received param: {}", param_str); - let json: serde_json::Value = match serde_json::from_str(param_str) { - Ok(v) => v, - Err(e) => { - warn!("[MXU_LAUNCH] Failed to parse param JSON: {}", e); - return false; - } - }; + #[cfg(mobile)] + { + warn!("[MXU_LAUNCH] Not supported on mobile platforms"); + return false; + } - let program = match json.get("program").and_then(|v| v.as_str()) { - Some(p) if !p.trim().is_empty() => p.to_string(), - _ => { - warn!("[MXU_LAUNCH] Missing or empty 'program' parameter"); - return false; - } - }; + #[cfg(desktop)] + { + let json: serde_json::Value = match serde_json::from_str(param_str) { + Ok(v) => v, + Err(e) => { + warn!("[MXU_LAUNCH] Failed to parse param JSON: {}", e); + return false; + } + }; - let args_str = json - .get("args") - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(); + let program = match json.get("program").and_then(|v| v.as_str()) { + Some(p) if !p.trim().is_empty() => p.to_string(), + _ => { + warn!("[MXU_LAUNCH] Missing or empty 'program' parameter"); + return false; + } + }; - let wait_for_exit = json - .get("wait_for_exit") - .and_then(|v| v.as_bool()) - .unwrap_or(false); - - let skip_if_running = json - .get("skip_if_running") - .and_then(|v| v.as_bool()) - .unwrap_or(false); - - // 如果启用了跳过检查且程序已在运行,直接返回成功 - if skip_if_running { - if crate::commands::system::check_process_running(&program) { - info!( - "[MXU_LAUNCH] Program '{}' is already running, skipping launch", - program - ); - return true; + let args_str = json + .get("args") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + + let wait_for_exit = json + .get("wait_for_exit") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + let skip_if_running = json + .get("skip_if_running") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + if skip_if_running { + if crate::commands::system::check_process_running(&program) { + info!( + "[MXU_LAUNCH] Program '{}' is already running, skipping launch", + program + ); + return true; + } } - } - info!( - "[MXU_LAUNCH] Launching: program={}, args={}, wait_for_exit={}", - program, args_str, wait_for_exit - ); + info!( + "[MXU_LAUNCH] Launching: program={}, args={}, wait_for_exit={}", + program, args_str, wait_for_exit + ); - let args_vec: Vec = if args_str.trim().is_empty() { - Vec::new() - } else { - match shell_words::split(&args_str) { - Ok(parsed) => parsed, - Err(e) => { - warn!( + let args_vec: Vec = if args_str.trim().is_empty() { + Vec::new() + } else { + match shell_words::split(&args_str) { + Ok(parsed) => parsed, + Err(e) => { + warn!( "[MXU_LAUNCH] Failed to parse arguments with shell_words ({}); falling back to whitespace split: {}", e, args_str ); - args_str.split_whitespace().map(|s| s.to_string()).collect() + args_str.split_whitespace().map(|s| s.to_string()).collect() + } } - } - }; - - let mut cmd = std::process::Command::new(&program); + }; - if !args_vec.is_empty() { - cmd.args(&args_vec); - } + let mut cmd = std::process::Command::new(&program); - // 默认使用程序所在目录作为工作目录 - if let Some(parent) = std::path::Path::new(&program).parent() { - if parent.exists() { - cmd.current_dir(parent); + if !args_vec.is_empty() { + cmd.args(&args_vec); } - } - if wait_for_exit { - match cmd.status() { - Ok(status) => { - let exit_code = status.code().unwrap_or(-1); - info!("[MXU_LAUNCH] Process exited with code: {}", exit_code); - true - } - Err(e) => { - log::error!("[MXU_LAUNCH] Failed to run program: {}", e); - false + if let Some(parent) = std::path::Path::new(&program).parent() { + if parent.exists() { + cmd.current_dir(parent); } } - } else { - match cmd.spawn() { - Ok(_) => { - info!("[MXU_LAUNCH] Process spawned (not waiting)"); - true + + if wait_for_exit { + match cmd.status() { + Ok(status) => { + let exit_code = status.code().unwrap_or(-1); + info!("[MXU_LAUNCH] Process exited with code: {}", exit_code); + true + } + Err(e) => { + log::error!("[MXU_LAUNCH] Failed to run program: {}", e); + false + } } - Err(e) => { - log::error!("[MXU_LAUNCH] Failed to spawn program: {}", e); - false + } else { + match cmd.spawn() { + Ok(_) => { + info!("[MXU_LAUNCH] Process spawned (not waiting)"); + true + } + Err(e) => { + log::error!("[MXU_LAUNCH] Failed to spawn program: {}", e); + false + } } } } @@ -296,11 +291,8 @@ fn mxu_launch_action_fn( // MXU_WEBHOOK Custom Action // ============================================================================ -/// MXU_WEBHOOK 动作名称常量 const MXU_WEBHOOK_ACTION: &str = "MXU_WEBHOOK_ACTION"; -/// MXU_WEBHOOK custom action 回调函数 -/// 从 custom_action_param 中读取 url,执行 HTTP GET 请求 fn mxu_webhook_action_fn( _ctx: &maa_framework::context::Context, args: &maa_framework::custom::ActionArgs, @@ -341,12 +333,10 @@ fn mxu_webhook_action_fn( Ok(resp) => { let status = resp.status(); info!("[MXU_WEBHOOK] Response status: {}", status); - if status.is_success() { - true - } else { + if !status.is_success() { warn!("[MXU_WEBHOOK] Non-success status code: {}", status); - true // 仍然返回成功,只要请求发出去了 } + true } Err(e) => { log::error!("[MXU_WEBHOOK] Request failed: {}", e); @@ -359,11 +349,8 @@ fn mxu_webhook_action_fn( // MXU_NOTIFY Custom Action // ============================================================================ -/// MXU_NOTIFY 动作名称常量 const MXU_NOTIFY_ACTION: &str = "MXU_NOTIFY_ACTION"; -/// MXU_NOTIFY custom action 回调函数 -/// 从 custom_action_param 中读取 title, body,发送系统通知 fn mxu_notify_action_fn( _ctx: &maa_framework::context::Context, args: &maa_framework::custom::ActionArgs, @@ -396,31 +383,38 @@ fn mxu_notify_action_fn( title, body ); - match notify_rust::Notification::new() - .summary(&title) - .body(&body) - .show() + #[cfg(desktop)] { - Ok(_) => { - info!("[MXU_NOTIFY] Notification sent successfully"); - true - } - Err(e) => { - log::error!("[MXU_NOTIFY] Failed to send notification: {}", e); - false + match notify_rust::Notification::new() + .summary(&title) + .body(&body) + .show() + { + Ok(_) => { + info!("[MXU_NOTIFY] Notification sent successfully"); + true + } + Err(e) => { + log::error!("[MXU_NOTIFY] Failed to send notification: {}", e); + false + } } } + + #[cfg(mobile)] + { + // 移动端通知由前端 WebView 的 Notification API 处理 + info!("[MXU_NOTIFY] Mobile: notification logged (title={}, body={})", title, body); + true + } } // ============================================================================ // MXU_KILLPROC Custom Action // ============================================================================ -/// MXU_KILLPROC 动作名称常量 const MXU_KILLPROC_ACTION: &str = "MXU_KILLPROC_ACTION"; -/// MXU_KILLPROC custom action 回调函数 -/// 从 custom_action_param 中读取 kill_self, process_name,结束进程 fn mxu_killproc_action_fn( _ctx: &maa_framework::context::Context, args: &maa_framework::custom::ActionArgs, @@ -428,48 +422,56 @@ fn mxu_killproc_action_fn( let param_str = args.param; info!("[MXU_KILLPROC] Received param: {}", param_str); - let json: serde_json::Value = match serde_json::from_str(param_str) { - Ok(v) => v, - Err(e) => { - warn!("[MXU_KILLPROC] Failed to parse param JSON: {}", e); - return false; - } - }; + #[cfg(mobile)] + { + warn!("[MXU_KILLPROC] Not supported on mobile platforms"); + return false; + } - let kill_self = json - .get("kill_self") - .and_then(|v| v.as_bool()) - .unwrap_or(true); - - if kill_self { - info!("[MXU_KILLPROC] Killing self process"); - // 获取当前可执行文件名 - let exe_name = std::env::current_exe() - .ok() - .and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string())); - - if let Some(name) = exe_name { - info!("[MXU_KILLPROC] Current exe: {}", name); - kill_process_by_name(&name) - } else { - warn!("[MXU_KILLPROC] Could not determine current exe name, using process::exit"); - std::process::exit(0); - } - } else { - let process_name = match json.get("process_name").and_then(|v| v.as_str()) { - Some(p) if !p.trim().is_empty() => p.to_string(), - _ => { - warn!("[MXU_KILLPROC] Missing or empty 'process_name' parameter"); + #[cfg(desktop)] + { + let json: serde_json::Value = match serde_json::from_str(param_str) { + Ok(v) => v, + Err(e) => { + warn!("[MXU_KILLPROC] Failed to parse param JSON: {}", e); return false; } }; - info!("[MXU_KILLPROC] Killing process: {}", process_name); - kill_process_by_name(&process_name) + let kill_self = json + .get("kill_self") + .and_then(|v| v.as_bool()) + .unwrap_or(true); + + if kill_self { + info!("[MXU_KILLPROC] Killing self process"); + let exe_name = std::env::current_exe() + .ok() + .and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string())); + + if let Some(name) = exe_name { + info!("[MXU_KILLPROC] Current exe: {}", name); + kill_process_by_name(&name) + } else { + warn!("[MXU_KILLPROC] Could not determine current exe name, using process::exit"); + std::process::exit(0); + } + } else { + let process_name = match json.get("process_name").and_then(|v| v.as_str()) { + Some(p) if !p.trim().is_empty() => p.to_string(), + _ => { + warn!("[MXU_KILLPROC] Missing or empty 'process_name' parameter"); + return false; + } + }; + + info!("[MXU_KILLPROC] Killing process: {}", process_name); + kill_process_by_name(&process_name) + } } } -/// 按名称结束进程 +#[cfg(desktop)] fn kill_process_by_name(name: &str) -> bool { use std::process::Command; @@ -500,7 +502,6 @@ fn kill_process_by_name(name: &str) -> bool { #[cfg(not(windows))] { - // macOS / Linux: 使用 killall,失败则 fallback 到 pkill match Command::new("killall").arg(name).output() { Ok(output) => { if output.status.success() { @@ -529,14 +530,11 @@ fn kill_process_by_name(name: &str) -> bool { } // ============================================================================ -// MXU_POWER Custom Action +// MXU_POWER Custom Action (desktop only) // ============================================================================ -/// MXU_POWER 动作名称常量 const MXU_POWER_ACTION: &str = "MXU_POWER_ACTION"; -/// MXU_POWER custom action 回调函数 -/// 从 custom_action_param 中读取 power_action,执行关机/重启/息屏/睡眠操作 fn mxu_power_action_fn( _ctx: &maa_framework::context::Context, args: &maa_framework::custom::ActionArgs, @@ -544,33 +542,43 @@ fn mxu_power_action_fn( let param_str = args.param; info!("[MXU_POWER] Received param: {}", param_str); - let json: serde_json::Value = match serde_json::from_str(param_str) { - Ok(v) => v, - Err(e) => { - warn!("[MXU_POWER] Failed to parse param JSON: {}", e); - return false; - } - }; + #[cfg(mobile)] + { + warn!("[MXU_POWER] Not supported on mobile platforms"); + return false; + } - let action = json - .get("power_action") - .and_then(|v| v.as_str()) - .unwrap_or("shutdown"); + #[cfg(desktop)] + { + let json: serde_json::Value = match serde_json::from_str(param_str) { + Ok(v) => v, + Err(e) => { + warn!("[MXU_POWER] Failed to parse param JSON: {}", e); + return false; + } + }; - info!("[MXU_POWER] Executing power action: {}", action); + let action = json + .get("power_action") + .and_then(|v| v.as_str()) + .unwrap_or("shutdown"); - match action { - "shutdown" => execute_power_shutdown(), - "restart" => execute_power_restart(), - "screenoff" => execute_power_screenoff(), - "sleep" => execute_power_sleep(), - _ => { - warn!("[MXU_POWER] Unknown power action: {}", action); - false + info!("[MXU_POWER] Executing power action: {}", action); + + match action { + "shutdown" => execute_power_shutdown(), + "restart" => execute_power_restart(), + "screenoff" => execute_power_screenoff(), + "sleep" => execute_power_sleep(), + _ => { + warn!("[MXU_POWER] Unknown power action: {}", action); + false + } } } } +#[cfg(desktop)] fn execute_power_shutdown() -> bool { use std::process::Command; @@ -623,6 +631,7 @@ fn execute_power_shutdown() -> bool { } } +#[cfg(desktop)] fn execute_power_restart() -> bool { use std::process::Command; @@ -675,22 +684,22 @@ fn execute_power_restart() -> bool { } } +#[cfg(desktop)] fn execute_power_screenoff() -> bool { #[cfg(windows)] { use windows::Win32::Foundation::HWND; use windows::Win32::UI::WindowsAndMessaging::SendMessageW; - // WM_SYSCOMMAND = 0x0112, SC_MONITORPOWER = 0xF170, LPARAM(2) = turn off const WM_SYSCOMMAND: u32 = 0x0112; const SC_MONITORPOWER: usize = 0xF170; unsafe { SendMessageW( - HWND(0xFFFF as *mut std::ffi::c_void), // HWND_BROADCAST + HWND(0xFFFF as *mut std::ffi::c_void), WM_SYSCOMMAND, windows::Win32::Foundation::WPARAM(SC_MONITORPOWER), - windows::Win32::Foundation::LPARAM(2), // 2 = turn off monitor + windows::Win32::Foundation::LPARAM(2), ); } info!("[MXU_POWER] Screen off command issued (Windows)"); @@ -715,7 +724,10 @@ fn execute_power_screenoff() -> bool { #[cfg(not(any(windows, target_os = "macos")))] { use std::process::Command; - match Command::new("xset").args(["dpms", "force", "off"]).spawn() { + match Command::new("xset") + .args(["dpms", "force", "off"]) + .spawn() + { Ok(_) => { info!("[MXU_POWER] Screen off command issued (Linux)"); true @@ -728,6 +740,7 @@ fn execute_power_screenoff() -> bool { } } +#[cfg(desktop)] fn execute_power_sleep() -> bool { use std::process::Command; @@ -781,12 +794,9 @@ fn execute_power_sleep() -> bool { // 注册入口 // ============================================================================ -/// 为资源注册所有 MXU 内置 custom actions -/// 在资源创建后调用此函数 pub fn register_all_mxu_actions(resource: &Resource) -> Result<(), String> { let mut failed_count = 0; - // 定义一个局部宏打印日志并统计失败 macro_rules! reg_action { ($name:expr, $fn_name:expr) => { let wrapper = move |ctx: &maa_framework::context::Context, diff --git a/src/App.tsx b/src/App.tsx index e5f6e4f1..41b9aa6f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,13 +32,15 @@ import { } from '@/services/updateService'; import { useTranslation } from 'react-i18next'; import { invoke } from '@tauri-apps/api/core'; -import { register, unregisterAll } from '@tauri-apps/plugin-global-shortcut'; +import type { unregisterAll as UnregisterAllFn } from '@tauri-apps/plugin-global-shortcut'; import { loggers } from '@/utils/logger'; import { useMaaCallbackLogger, useMaaAgentLogger } from '@/utils/useMaaCallbackLogger'; import { getInterfaceLangKey } from '@/i18n'; import { applyTheme, resolveThemeMode } from '@/themes'; import { isTauri, + isMobile, + isDesktop, isValidWindowSize, setWindowTitle, setWindowSize, @@ -420,8 +422,7 @@ function App() { setWindowTitle(title); - // 同时更新托盘 tooltip(只显示项目名称) - if (isTauri()) { + if (isTauri() && isDesktop()) { invoke('update_tray_tooltip', { tooltip: projectInterface.name }).catch((err) => { log.warn('设置托盘 tooltip 失败:', err); }); @@ -452,11 +453,12 @@ function App() { log.warn('设置窗口图标失败:', err); } - // 同时更新托盘图标 - try { - await invoke('update_tray_icon', { iconPath: fullIconPath }); - } catch (err) { - log.warn('设置托盘图标失败:', err); + if (isDesktop()) { + try { + await invoke('update_tray_icon', { iconPath: fullIconPath }); + } catch (err) { + log.warn('设置托盘图标失败:', err); + } } }; @@ -499,11 +501,10 @@ function App() { importConfig(config); } - // 应用保存的窗口大小和位置 - if (config.settings.windowSize) { + if (isDesktop() && config.settings.windowSize) { await setWindowSize(config.settings.windowSize.width, config.settings.windowSize.height); } - if (config.settings.windowPosition) { + if (isDesktop() && config.settings.windowPosition) { const { x, y } = config.settings.windowPosition; // 先检查位置是否在可见显示器范围内 try { @@ -778,16 +779,14 @@ function App() { const mode = resolveThemeMode(initialTheme); applyTheme(mode, initialAccent); - // 先检查程序路径,有问题就弹窗不继续加载 const initApp = async () => { - if (isTauri()) { + if (isTauri() && isDesktop()) { try { const pathIssue = await invoke('check_exe_path'); if (pathIssue) { log.warn('检测到程序路径问题:', pathIssue); setBadPathType(pathIssue as BadPathType); setShowBadPathModal(true); - // 路径有问题就不继续加载了,但仍需显示窗口 showWindow(); return; } @@ -803,9 +802,9 @@ function App() { initApp(); }, []); - // 检查 VC++ 运行库缺失(在加载完成后检查) + // 检查 VC++ 运行库缺失(仅桌面端,加载完成后检查) const checkVCRedistMissing = useCallback(async () => { - if (!isTauri()) return; + if (!isTauri() || isMobile()) return; try { const missing = await invoke('check_vcredist_missing'); @@ -862,9 +861,9 @@ function App() { } }, [downloadStatus, setShowInstallConfirmModal]); - // 监听窗口大小和位置变化 + // 监听窗口大小和位置变化(仅桌面端) useEffect(() => { - if (!isTauri()) return; + if (!isTauri() || isMobile()) return; let unlistenResize: (() => void) | null = null; let unlistenMove: (() => void) | null = null; @@ -1078,22 +1077,28 @@ function App() { return () => document.removeEventListener('keydown', handleKeyDown); }, [devMode]); - // 全局快捷键(窗口失焦时也生效) + // 全局快捷键(窗口失焦时也生效,仅桌面端) const hotkeys = useAppStore((state) => state.hotkeys); useEffect(() => { - if (!hotkeys?.globalEnabled) return; + if (!hotkeys?.globalEnabled || isMobile()) return; const startKey = hotkeys.startTasks || 'F10'; const stopKey = hotkeys.stopTasks || 'F11'; - // Ctrl -> CommandOrControl const toTauriKey = (k: string) => k.replace(/^Ctrl\+/i, 'CommandOrControl+'); const GLOBAL_HOTKEY_THROTTLE_MS = 1000; let lastStartTime = 0; + let unregisterAllFn: typeof UnregisterAllFn | null = null; + const registerKeys = async () => { try { - await register(toTauriKey(startKey), () => { + const { register: registerShortcut, unregisterAll: unregAll } = await import( + '@tauri-apps/plugin-global-shortcut' + ); + unregisterAllFn = unregAll; + + await registerShortcut(toTauriKey(startKey), () => { const now = Date.now(); if (now - lastStartTime < GLOBAL_HOTKEY_THROTTLE_MS) return; lastStartTime = now; @@ -1103,9 +1108,8 @@ function App() { }), ); }); - // 避免重复注册相同的键 if (stopKey !== startKey) { - await register(toTauriKey(stopKey), () => { + await registerShortcut(toTauriKey(stopKey), () => { document.dispatchEvent( new CustomEvent('mxu-stop-tasks', { detail: { source: 'global-hotkey', combo: stopKey }, @@ -1121,13 +1125,13 @@ function App() { registerKeys(); return () => { - unregisterAll().catch(() => {}); + if (unregisterAllFn) unregisterAllFn().catch(() => {}); }; }, [hotkeys?.globalEnabled, hotkeys?.startTasks, hotkeys?.stopTasks]); - // 监听托盘菜单事件(开始/停止任务) + // 监听托盘菜单事件(开始/停止任务,仅桌面端) useEffect(() => { - if (!isTauri()) return; + if (!isTauri() || isMobile()) return; let unlistenStart: (() => void) | null = null; let unlistenStop: (() => void) | null = null; @@ -1316,17 +1320,14 @@ function App() { ) : ( - /* 主内容区 */ -
- {/* 左侧任务列表区 */} +
+ {/* 左侧/上方任务列表区 */}
- {/* 任务列表 */} - {/* 添加任务面板 - 使用 grid 动画实现平滑展开/折叠 */}
- {/* 底部工具栏 */} setShowAddTaskPanel(!showAddTaskPanel)} />
- {/* 分隔条 Resizer */} -
- {/* 可视化把手 */} -
-
+ {/* 分隔条 Resizer (仅桌面端) */} + {isDesktop() && ( +
+
+
+ )} - {/* 右侧信息面板 */} - {!rightPanelCollapsed && ( + {/* 右侧/下方信息面板 */} + {(isMobile() || !rightPanelCollapsed) && (
- {/* 连接设置和实时截图(可折叠)- 使用 grid 动画 */}
- {/* 连接设置(设备/资源选择) */} - - {/* 实时截图 */}
- {/* 运行日志 */}
)} diff --git a/src/components/TitleBar.tsx b/src/components/TitleBar.tsx index 17182ceb..1b77ecd1 100644 --- a/src/components/TitleBar.tsx +++ b/src/components/TitleBar.tsx @@ -7,8 +7,7 @@ import { loadIconAsDataUrl } from '@/services/contentResolver'; import { loggers } from '@/utils/logger'; import { isTauri } from '@/utils/paths'; -// 平台类型 -type Platform = 'windows' | 'macos' | 'linux' | 'unknown'; +type Platform = 'windows' | 'macos' | 'linux' | 'android' | 'unknown'; export function TitleBar() { const { t } = useTranslation(); @@ -25,7 +24,8 @@ export function TitleBar() { // 检测平台(通过 userAgent) useEffect(() => { const ua = navigator.userAgent.toLowerCase(); - if (ua.includes('win')) setPlatform('windows'); + if (ua.includes('android')) setPlatform('android'); + else if (ua.includes('win')) setPlatform('windows'); else if (ua.includes('mac')) setPlatform('macos'); else if (ua.includes('linux')) setPlatform('linux'); }, []); @@ -112,9 +112,7 @@ export function TitleBar() { return version ? `${projectInterface.name} ${version}` : projectInterface.name; }; - // macOS/Linux 使用原生标题栏,不渲染自定义标题栏 - // 仅 Windows 使用自定义标题栏 - if (platform === 'macos' || platform === 'linux') { + if (platform === 'macos' || platform === 'linux' || platform === 'android') { return null; } diff --git a/src/components/settings/DebugSection.tsx b/src/components/settings/DebugSection.tsx index 684226df..19296b54 100644 --- a/src/components/settings/DebugSection.tsx +++ b/src/components/settings/DebugSection.tsx @@ -5,7 +5,7 @@ import { Bug, RefreshCw, FolderOpen, ScrollText, Network, Archive } from 'lucide import { useAppStore } from '@/stores/appStore'; import { maaService } from '@/services/maaService'; import { loggers } from '@/utils/logger'; -import { isTauri, getDebugDir, getConfigDir, openDirectory } from '@/utils/paths'; +import { isTauri, isDesktop, getDebugDir, getConfigDir, openDirectory } from '@/utils/paths'; import { useExportLogs } from '@/utils/useExportLogs'; import { SwitchButton } from '@/components/FormControls'; import { ExportLogsModal } from './ExportLogsModal'; @@ -68,17 +68,21 @@ export function DebugSection() { if (isTauri()) { try { const { invoke } = await import('@tauri-apps/api/core'); - const [exeDirResult, cwdResult, sysInfo, webview2DirResult] = await Promise.all([ + const [exeDirResult, cwdResult, sysInfo] = await Promise.all([ invoke('get_exe_dir'), invoke('get_cwd'), invoke<{ os: string; os_version: string; arch: string; tauri_version: string }>( 'get_system_info', ), - invoke<{ path: string; system: boolean }>('get_webview2_dir'), ]); setExeDir(exeDirResult); setCwd(cwdResult); - setWebview2Dir(webview2DirResult); + if (isDesktop()) { + const webview2DirResult = await invoke<{ path: string; system: boolean }>( + 'get_webview2_dir', + ); + setWebview2Dir(webview2DirResult); + } setSystemInfo({ os: sysInfo.os, osVersion: sysInfo.os_version, @@ -199,7 +203,7 @@ export function DebugSection() { {exeDir}

)} -

+ {isDesktop() &&

{t('debug.webview2Dir')}:{' '} {webview2Dir @@ -208,7 +212,7 @@ export function DebugSection() { : webview2Dir.path : '-'} -

+

}
)} diff --git a/src/components/settings/GeneralSection.tsx b/src/components/settings/GeneralSection.tsx index 57d8037a..91f3025d 100644 --- a/src/components/settings/GeneralSection.tsx +++ b/src/components/settings/GeneralSection.tsx @@ -16,7 +16,7 @@ import { import { invoke } from '@tauri-apps/api/core'; import { useAppStore } from '@/stores/appStore'; import { defaultAddTaskPanelHeight, defaultWindowSize } from '@/types/config'; -import { isTauri } from '@/utils/paths'; +import { isTauri, isDesktop } from '@/utils/paths'; import { SwitchButton } from '@/components/FormControls'; import { FrameRateSelector } from '../FrameRateSelector'; @@ -140,8 +140,8 @@ export function GeneralSection() { {t('settings.general')} - {/* ① 开机自启动 */} - {isTauri() && ( + {/* ① 开机自启动 (桌面端) */} + {isTauri() && isDesktop() && (
@@ -253,19 +253,25 @@ export function GeneralSection() {
- {/* ④ 最小化到托盘 */} -
-
-
- -
- {t('settings.minimizeToTray')} -

{t('settings.minimizeToTrayHint')}

+ {/* ④ 最小化到托盘 (桌面端) */} + {isDesktop() && ( +
+
+
+ +
+ + {t('settings.minimizeToTray')} + +

+ {t('settings.minimizeToTrayHint')} +

+
+ setMinimizeToTray(v)} />
- setMinimizeToTray(v)} />
-
+ )} {/* ⑤ 显示选项预览 */}
@@ -306,8 +312,8 @@ export function GeneralSection() {
- {/* ⑧ 重置窗口布局 */} - {isTauri() && ( + {/* ⑧ 重置窗口布局 (桌面端) */} + {isTauri() && isDesktop() && (
diff --git a/src/components/settings/HotkeySection.tsx b/src/components/settings/HotkeySection.tsx index 73bb80b1..7553cf44 100644 --- a/src/components/settings/HotkeySection.tsx +++ b/src/components/settings/HotkeySection.tsx @@ -2,11 +2,14 @@ import { useTranslation } from 'react-i18next'; import { Key, Play, StopCircle, AlertCircle, Globe } from 'lucide-react'; import { useAppStore } from '@/stores/appStore'; import { SwitchButton } from '@/components/FormControls'; +import { isDesktop } from '@/utils/paths'; export function HotkeySection() { const { t } = useTranslation(); const { hotkeys, setHotkeys } = useAppStore(); + if (!isDesktop()) return null; + // 生成统一的快捷键组合字符串 const buildCombo = (e: React.KeyboardEvent): string | null => { const parts: string[] = []; diff --git a/src/components/settings/UpdateSection.tsx b/src/components/settings/UpdateSection.tsx index 493ff1a6..e15032c2 100644 --- a/src/components/settings/UpdateSection.tsx +++ b/src/components/settings/UpdateSection.tsx @@ -31,8 +31,10 @@ import { resolveI18nText } from '@/services/contentResolver'; import { getInterfaceLangKey } from '@/i18n'; import { loggers } from '@/utils/logger'; import { ReleaseNotes, DownloadProgressBar } from '../UpdateInfoCard'; +import { isDesktop } from '@/utils/paths'; export function UpdateSection() { + if (!isDesktop()) return null; const { t } = useTranslation(); const { projectInterface, diff --git a/src/index.css b/src/index.css index e67c080f..4b59687a 100644 --- a/src/index.css +++ b/src/index.css @@ -506,3 +506,16 @@ body { .has-background-image .bg-bg-primary { background-color: color-mix(in srgb, var(--color-bg-primary) 45%, transparent) !important; } + +/* 移动端适配 */ +@media (max-width: 768px) { + button, [role="button"], select, input[type="checkbox"] { + min-height: 44px; + min-width: 44px; + } + + .select-none { + -webkit-touch-callout: none; + -webkit-user-select: none; + } +} diff --git a/src/services/interfaceLoader.ts b/src/services/interfaceLoader.ts index fc53a6e7..be38962e 100644 --- a/src/services/interfaceLoader.ts +++ b/src/services/interfaceLoader.ts @@ -295,21 +295,25 @@ async function processImports( // 平台过滤 // ============================================================================ -// 检测当前操作系统 const isWindows = navigator.platform.toLowerCase().includes('win'); const isMacOS = navigator.platform.toLowerCase().includes('mac'); +const isAndroidPlatform = /android/i.test(navigator.userAgent); -/** - * 获取当前平台不支持的控制器类型集合 - */ function getUnsupportedControllerTypes(): Set { const unsupported = new Set(); - // 非 Windows 系统不支持 Win32 和 Gamepad + + // Android: 仅支持 Adb 控制器 + if (isAndroidPlatform) { + unsupported.add('Win32'); + unsupported.add('Gamepad'); + unsupported.add('PlayCover'); + return unsupported; + } + if (!isWindows) { unsupported.add('Win32'); unsupported.add('Gamepad'); } - // 非 macOS 系统不支持 PlayCover if (!isMacOS) { unsupported.add('PlayCover'); } diff --git a/src/utils/paths.ts b/src/utils/paths.ts index 1ce338ef..61581d88 100644 --- a/src/utils/paths.ts +++ b/src/utils/paths.ts @@ -1,6 +1,6 @@ /** * 路径工具函数 - * 统一管理应用数据目录的获取 + * 统一管理应用数据目录的获取和平台检测 */ // 目录常量 @@ -13,6 +13,18 @@ export const isTauri = () => { return typeof window !== 'undefined' && '__TAURI__' in window; }; +export const isAndroid = (): boolean => { + return typeof navigator !== 'undefined' && /android/i.test(navigator.userAgent); +}; + +export const isMobile = (): boolean => { + return isAndroid(); +}; + +export const isDesktop = (): boolean => { + return !isMobile(); +}; + // 缓存数据目录路径 let cachedDataPath: string | null = null; diff --git a/src/utils/windowUtils.ts b/src/utils/windowUtils.ts index d5d7bcac..a40864e4 100644 --- a/src/utils/windowUtils.ts +++ b/src/utils/windowUtils.ts @@ -1,8 +1,7 @@ import { loggers } from './logger'; -import { isTauri } from './paths'; +import { isTauri, isMobile, isDesktop, isAndroid } from './paths'; -// 重新导出 isTauri,保持向后兼容 -export { isTauri }; +export { isTauri, isMobile, isDesktop, isAndroid }; const log = loggers.app;