diff --git a/README.md b/README.md index 04fa9e7..e74590f 100644 --- a/README.md +++ b/README.md @@ -45,31 +45,56 @@ web_android_shell/ ### 运行已有品牌 +建议优先使用仓库内 `.fvm` 的 Flutter / Dart,避免全局版本不一致。 + +Windows PowerShell: + +```powershell +cd apps/aixue +..\..\.fvm\flutter_sdk\bin\flutter.bat run +``` + +macOS / zsh: + ```bash cd apps/aixue -flutter run +../../.fvm/flutter_sdk/bin/flutter run ``` 也可以替换为: -```bash +```powershell cd apps/test -flutter run +..\..\.fvm\flutter_sdk\bin\flutter.bat run ``` ```bash cd apps/yunxiao -flutter run +../../.fvm/flutter_sdk/bin/flutter run ``` ### 生成或重建品牌应用 -```bash -dart run tool/generate_app.dart aixue -dart run tool/generate_app.dart test -dart run tool/generate_app.dart yunxiao +请在仓库根目录执行。 + +Windows PowerShell: + +```powershell +.\.fvm\flutter_sdk\bin\dart.bat run tool\generate_app.dart aixue +.\.fvm\flutter_sdk\bin\dart.bat run tool\generate_app.dart test +.\.fvm\flutter_sdk\bin\dart.bat run tool\generate_app.dart yunxiao ``` +macOS / zsh: + +```bash +./.fvm/flutter_sdk/bin/dart run tool/generate_app.dart aixue +./.fvm/flutter_sdk/bin/dart run tool/generate_app.dart test +./.fvm/flutter_sdk/bin/dart run tool/generate_app.dart yunxiao +``` + +注意:生成脚本会删除并重建已有的 `apps/<品牌名>/` 目录。 + 生成脚本会自动完成: - 创建 Flutter Android 应用到 `apps/<品牌名>/` diff --git a/apps/aixue/android/app/build.gradle.kts b/apps/aixue/android/app/build.gradle.kts index 360d292..4c6ba76 100644 --- a/apps/aixue/android/app/build.gradle.kts +++ b/apps/aixue/android/app/build.gradle.kts @@ -5,15 +5,40 @@ plugins { id("dev.flutter.flutter-gradle-plugin") } +import java.io.File import java.util.Properties import java.io.FileInputStream val keystoreProperties = Properties() val keystorePropertiesFile = rootProject.file("key.properties") if (keystorePropertiesFile.exists()) { - keystoreProperties.load(FileInputStream(keystorePropertiesFile)) + FileInputStream(keystorePropertiesFile).use { keystoreProperties.load(it) } } +val defaultReleaseStoreFile = rootProject.file("../../../tool/key.jks") +fun resolveKeystoreFile(rawPath: String?): File { + val candidate = rawPath?.trim().orEmpty() + if (candidate.isEmpty()) { + return defaultReleaseStoreFile + } + + val expandedHome = if (candidate.startsWith("~/") || candidate == "~") { + candidate.replaceFirst("~", System.getProperty("user.home")) + } else { + candidate + } + + val normalized = expandedHome.replace('\\', File.separatorChar).replace('/', File.separatorChar) + val storeFile = File(normalized) + return if (storeFile.isAbsolute) storeFile else rootProject.file(normalized) +} + +val releaseStoreFile = + resolveKeystoreFile(keystoreProperties["storeFile"] as String?) +val releaseKeyAlias = keystoreProperties["keyAlias"] as String? ?: "my-key-alias" +val releaseKeyPassword = keystoreProperties["keyPassword"] as String? ?: "123456" +val releaseStorePassword = keystoreProperties["storePassword"] as String? ?: "123456" + android { namespace = "com.yuanxuan.aixue" @@ -41,10 +66,10 @@ android { } signingConfigs { create("release") { - keyAlias = keystoreProperties["keyAlias"] as String? - keyPassword = keystoreProperties["keyPassword"] as String? - storeFile = keystoreProperties["storeFile"]?.let { file(it as String) } - storePassword = keystoreProperties["storePassword"] as String? + keyAlias = releaseKeyAlias + keyPassword = releaseKeyPassword + storeFile = releaseStoreFile + storePassword = releaseStorePassword } } diff --git a/apps/aixue/android/app/src/main/res/drawable-hdpi/android12splash.png b/apps/aixue/android/app/src/main/res/drawable-hdpi/android12splash.png index 2fd976f..b5bb8a0 100644 Binary files a/apps/aixue/android/app/src/main/res/drawable-hdpi/android12splash.png and b/apps/aixue/android/app/src/main/res/drawable-hdpi/android12splash.png differ diff --git a/apps/aixue/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/apps/aixue/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png index b734432..2d2d0f4 100644 Binary files a/apps/aixue/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png and b/apps/aixue/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png differ diff --git a/apps/aixue/android/app/src/main/res/drawable-hdpi/splash.png b/apps/aixue/android/app/src/main/res/drawable-hdpi/splash.png index 2fd976f..b5bb8a0 100644 Binary files a/apps/aixue/android/app/src/main/res/drawable-hdpi/splash.png and b/apps/aixue/android/app/src/main/res/drawable-hdpi/splash.png differ diff --git a/apps/aixue/android/app/src/main/res/drawable-mdpi/android12splash.png b/apps/aixue/android/app/src/main/res/drawable-mdpi/android12splash.png index c494b3f..cf93b0b 100644 Binary files a/apps/aixue/android/app/src/main/res/drawable-mdpi/android12splash.png and b/apps/aixue/android/app/src/main/res/drawable-mdpi/android12splash.png differ diff --git a/apps/aixue/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/apps/aixue/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png index 7a74e8d..8e61003 100644 Binary files a/apps/aixue/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png and b/apps/aixue/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png differ diff --git a/apps/aixue/android/app/src/main/res/drawable-mdpi/splash.png b/apps/aixue/android/app/src/main/res/drawable-mdpi/splash.png index c494b3f..cf93b0b 100644 Binary files a/apps/aixue/android/app/src/main/res/drawable-mdpi/splash.png and b/apps/aixue/android/app/src/main/res/drawable-mdpi/splash.png differ diff --git a/apps/aixue/android/app/src/main/res/drawable-night-hdpi/android12splash.png b/apps/aixue/android/app/src/main/res/drawable-night-hdpi/android12splash.png index 2fd976f..b5bb8a0 100644 Binary files a/apps/aixue/android/app/src/main/res/drawable-night-hdpi/android12splash.png and b/apps/aixue/android/app/src/main/res/drawable-night-hdpi/android12splash.png differ diff --git a/apps/aixue/android/app/src/main/res/drawable-night-mdpi/android12splash.png b/apps/aixue/android/app/src/main/res/drawable-night-mdpi/android12splash.png index c494b3f..cf93b0b 100644 Binary files a/apps/aixue/android/app/src/main/res/drawable-night-mdpi/android12splash.png and b/apps/aixue/android/app/src/main/res/drawable-night-mdpi/android12splash.png differ diff --git a/apps/aixue/android/app/src/main/res/drawable-night-xhdpi/android12splash.png b/apps/aixue/android/app/src/main/res/drawable-night-xhdpi/android12splash.png index 46944e4..b8dc3d4 100644 Binary files a/apps/aixue/android/app/src/main/res/drawable-night-xhdpi/android12splash.png and b/apps/aixue/android/app/src/main/res/drawable-night-xhdpi/android12splash.png differ diff --git a/apps/aixue/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png b/apps/aixue/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png index 76f3628..1f49db5 100644 Binary files a/apps/aixue/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png and b/apps/aixue/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png differ diff --git a/apps/aixue/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png b/apps/aixue/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png index 2c50553..22cb434 100644 Binary files a/apps/aixue/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png and b/apps/aixue/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png differ diff --git a/apps/aixue/android/app/src/main/res/drawable-xhdpi/android12splash.png b/apps/aixue/android/app/src/main/res/drawable-xhdpi/android12splash.png index 46944e4..b8dc3d4 100644 Binary files a/apps/aixue/android/app/src/main/res/drawable-xhdpi/android12splash.png and b/apps/aixue/android/app/src/main/res/drawable-xhdpi/android12splash.png differ diff --git a/apps/aixue/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/apps/aixue/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png index 42b32a2..453bce7 100644 Binary files a/apps/aixue/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png and b/apps/aixue/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png differ diff --git a/apps/aixue/android/app/src/main/res/drawable-xhdpi/splash.png b/apps/aixue/android/app/src/main/res/drawable-xhdpi/splash.png index 46944e4..b8dc3d4 100644 Binary files a/apps/aixue/android/app/src/main/res/drawable-xhdpi/splash.png and b/apps/aixue/android/app/src/main/res/drawable-xhdpi/splash.png differ diff --git a/apps/aixue/android/app/src/main/res/drawable-xxhdpi/android12splash.png b/apps/aixue/android/app/src/main/res/drawable-xxhdpi/android12splash.png index 76f3628..1f49db5 100644 Binary files a/apps/aixue/android/app/src/main/res/drawable-xxhdpi/android12splash.png and b/apps/aixue/android/app/src/main/res/drawable-xxhdpi/android12splash.png differ diff --git a/apps/aixue/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/apps/aixue/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png index 0506a2f..3aaed00 100644 Binary files a/apps/aixue/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png and b/apps/aixue/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png differ diff --git a/apps/aixue/android/app/src/main/res/drawable-xxhdpi/splash.png b/apps/aixue/android/app/src/main/res/drawable-xxhdpi/splash.png index 76f3628..1f49db5 100644 Binary files a/apps/aixue/android/app/src/main/res/drawable-xxhdpi/splash.png and b/apps/aixue/android/app/src/main/res/drawable-xxhdpi/splash.png differ diff --git a/apps/aixue/android/app/src/main/res/drawable-xxxhdpi/android12splash.png b/apps/aixue/android/app/src/main/res/drawable-xxxhdpi/android12splash.png index 2c50553..22cb434 100644 Binary files a/apps/aixue/android/app/src/main/res/drawable-xxxhdpi/android12splash.png and b/apps/aixue/android/app/src/main/res/drawable-xxxhdpi/android12splash.png differ diff --git a/apps/aixue/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/apps/aixue/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png index 78a0b13..5825d9b 100644 Binary files a/apps/aixue/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png and b/apps/aixue/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png differ diff --git a/apps/aixue/android/app/src/main/res/drawable-xxxhdpi/splash.png b/apps/aixue/android/app/src/main/res/drawable-xxxhdpi/splash.png index 2c50553..22cb434 100644 Binary files a/apps/aixue/android/app/src/main/res/drawable-xxxhdpi/splash.png and b/apps/aixue/android/app/src/main/res/drawable-xxxhdpi/splash.png differ diff --git a/apps/aixue/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/apps/aixue/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index 653b7d5..18f8ec7 100644 Binary files a/apps/aixue/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/apps/aixue/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/apps/aixue/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/apps/aixue/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index cc128db..220b7b9 100644 Binary files a/apps/aixue/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/apps/aixue/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/apps/aixue/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/apps/aixue/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index b0d97fb..ecb0646 100644 Binary files a/apps/aixue/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/apps/aixue/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/apps/aixue/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/apps/aixue/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 57d6901..22841e6 100644 Binary files a/apps/aixue/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/apps/aixue/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/apps/aixue/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/apps/aixue/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index d325a5a..6d7e899 100644 Binary files a/apps/aixue/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/apps/aixue/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/apps/aixue/assets/branding/icon.png b/apps/aixue/assets/branding/icon.png index a682c42..1269853 100644 Binary files a/apps/aixue/assets/branding/icon.png and b/apps/aixue/assets/branding/icon.png differ diff --git a/apps/aixue/assets/branding/icon_foreground.png b/apps/aixue/assets/branding/icon_foreground.png index a682c42..9c234e6 100644 Binary files a/apps/aixue/assets/branding/icon_foreground.png and b/apps/aixue/assets/branding/icon_foreground.png differ diff --git a/apps/aixue/assets/branding/splash.png b/apps/aixue/assets/branding/splash.png index 64abd98..9c234e6 100644 Binary files a/apps/aixue/assets/branding/splash.png and b/apps/aixue/assets/branding/splash.png differ diff --git a/apps/aixue/assets/config/bootstrap.json b/apps/aixue/assets/config/bootstrap.json index 52df66c..4032efd 100644 --- a/apps/aixue/assets/config/bootstrap.json +++ b/apps/aixue/assets/config/bootstrap.json @@ -4,4 +4,4 @@ "portraitUp", "portraitDown" ] -} \ No newline at end of file +} diff --git a/apps/aixue/devtools_options.yaml b/apps/aixue/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/apps/aixue/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/apps/test/android/app/build.gradle.kts b/apps/test/android/app/build.gradle.kts index 39e71f1..ae2b079 100644 --- a/apps/test/android/app/build.gradle.kts +++ b/apps/test/android/app/build.gradle.kts @@ -5,15 +5,40 @@ plugins { id("dev.flutter.flutter-gradle-plugin") } +import java.io.File import java.util.Properties import java.io.FileInputStream val keystoreProperties = Properties() val keystorePropertiesFile = rootProject.file("key.properties") if (keystorePropertiesFile.exists()) { - keystoreProperties.load(FileInputStream(keystorePropertiesFile)) + FileInputStream(keystorePropertiesFile).use { keystoreProperties.load(it) } } +val defaultReleaseStoreFile = rootProject.file("../../../tool/key.jks") +fun resolveKeystoreFile(rawPath: String?): File { + val candidate = rawPath?.trim().orEmpty() + if (candidate.isEmpty()) { + return defaultReleaseStoreFile + } + + val expandedHome = if (candidate.startsWith("~/") || candidate == "~") { + candidate.replaceFirst("~", System.getProperty("user.home")) + } else { + candidate + } + + val normalized = expandedHome.replace('\\', File.separatorChar).replace('/', File.separatorChar) + val storeFile = File(normalized) + return if (storeFile.isAbsolute) storeFile else rootProject.file(normalized) +} + +val releaseStoreFile = + resolveKeystoreFile(keystoreProperties["storeFile"] as String?) +val releaseKeyAlias = keystoreProperties["keyAlias"] as String? ?: "my-key-alias" +val releaseKeyPassword = keystoreProperties["keyPassword"] as String? ?: "123456" +val releaseStorePassword = keystoreProperties["storePassword"] as String? ?: "123456" + android { namespace = "com.yuanxuan.test_shell" @@ -41,10 +66,10 @@ android { } signingConfigs { create("release") { - keyAlias = keystoreProperties["keyAlias"] as String? - keyPassword = keystoreProperties["keyPassword"] as String? - storeFile = keystoreProperties["storeFile"]?.let { file(it as String) } - storePassword = keystoreProperties["storePassword"] as String? + keyAlias = releaseKeyAlias + keyPassword = releaseKeyPassword + storeFile = releaseStoreFile + storePassword = releaseStorePassword } } diff --git a/apps/test/assets/config/bootstrap.json b/apps/test/assets/config/bootstrap.json index 54be618..f4b6d76 100644 --- a/apps/test/assets/config/bootstrap.json +++ b/apps/test/assets/config/bootstrap.json @@ -1,5 +1,5 @@ { - "initialUrl": "http://192.168.2.57:8080/test_bridge.html", + "initialUrl": "http://192.168.2.54:8080/test_bridge.html", "preferredOrientations": [ "portraitUp", "portraitDown" diff --git a/apps/test/devtools_options.yaml b/apps/test/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/apps/test/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/apps/yunxiao/android/app/build.gradle.kts b/apps/yunxiao/android/app/build.gradle.kts index 46078ef..03dc2ed 100644 --- a/apps/yunxiao/android/app/build.gradle.kts +++ b/apps/yunxiao/android/app/build.gradle.kts @@ -5,15 +5,40 @@ plugins { id("dev.flutter.flutter-gradle-plugin") } +import java.io.File import java.util.Properties import java.io.FileInputStream val keystoreProperties = Properties() val keystorePropertiesFile = rootProject.file("key.properties") if (keystorePropertiesFile.exists()) { - keystoreProperties.load(FileInputStream(keystorePropertiesFile)) + FileInputStream(keystorePropertiesFile).use { keystoreProperties.load(it) } } +val defaultReleaseStoreFile = rootProject.file("../../../tool/key.jks") +fun resolveKeystoreFile(rawPath: String?): File { + val candidate = rawPath?.trim().orEmpty() + if (candidate.isEmpty()) { + return defaultReleaseStoreFile + } + + val expandedHome = if (candidate.startsWith("~/") || candidate == "~") { + candidate.replaceFirst("~", System.getProperty("user.home")) + } else { + candidate + } + + val normalized = expandedHome.replace('\\', File.separatorChar).replace('/', File.separatorChar) + val storeFile = File(normalized) + return if (storeFile.isAbsolute) storeFile else rootProject.file(normalized) +} + +val releaseStoreFile = + resolveKeystoreFile(keystoreProperties["storeFile"] as String?) +val releaseKeyAlias = keystoreProperties["keyAlias"] as String? ?: "my-key-alias" +val releaseKeyPassword = keystoreProperties["keyPassword"] as String? ?: "123456" +val releaseStorePassword = keystoreProperties["storePassword"] as String? ?: "123456" + android { namespace = "com.yuanxuan.yunxiao" @@ -41,10 +66,10 @@ android { } signingConfigs { create("release") { - keyAlias = keystoreProperties["keyAlias"] as String? - keyPassword = keystoreProperties["keyPassword"] as String? - storeFile = keystoreProperties["storeFile"]?.let { file(it as String) } - storePassword = keystoreProperties["storePassword"] as String? + keyAlias = releaseKeyAlias + keyPassword = releaseKeyPassword + storeFile = releaseStoreFile + storePassword = releaseStorePassword } } diff --git a/apps/yunxiao/devtools_options.yaml b/apps/yunxiao/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/apps/yunxiao/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/flavors/aixue/icon.png b/flavors/aixue/icon.png index a682c42..1269853 100644 Binary files a/flavors/aixue/icon.png and b/flavors/aixue/icon.png differ diff --git a/flavors/aixue/icon_foreground.png b/flavors/aixue/icon_foreground.png index a682c42..9c234e6 100644 Binary files a/flavors/aixue/icon_foreground.png and b/flavors/aixue/icon_foreground.png differ diff --git a/flavors/aixue/splash.png b/flavors/aixue/splash.png index 64abd98..9c234e6 100644 Binary files a/flavors/aixue/splash.png and b/flavors/aixue/splash.png differ diff --git a/packages/web_android_shell/android/app/build.gradle.kts b/packages/web_android_shell/android/app/build.gradle.kts index 5dfc1c2..3c2cc7e 100644 --- a/packages/web_android_shell/android/app/build.gradle.kts +++ b/packages/web_android_shell/android/app/build.gradle.kts @@ -5,6 +5,39 @@ plugins { id("dev.flutter.flutter-gradle-plugin") } +import java.io.File +import java.io.FileInputStream +import java.util.Properties + +val keystoreProperties = Properties() +val keystorePropertiesFile = rootProject.file("key.properties") +if (keystorePropertiesFile.exists()) { + FileInputStream(keystorePropertiesFile).use { keystoreProperties.load(it) } +} + +val defaultReleaseStoreFile = rootProject.file("../../../tool/key.jks") +fun resolveKeystoreFile(rawPath: String?): File { + val candidate = rawPath?.trim().orEmpty() + if (candidate.isEmpty()) { + return defaultReleaseStoreFile + } + + val expandedHome = if (candidate.startsWith("~/") || candidate == "~") { + candidate.replaceFirst("~", System.getProperty("user.home")) + } else { + candidate + } + + val normalized = expandedHome.replace('\\', File.separatorChar).replace('/', File.separatorChar) + val storeFile = File(normalized) + return if (storeFile.isAbsolute) storeFile else rootProject.file(normalized) +} + +val releaseStoreFile = resolveKeystoreFile(keystoreProperties["storeFile"] as String?) +val releaseKeyAlias = keystoreProperties["keyAlias"] as String? ?: "my-key-alias" +val releaseKeyPassword = keystoreProperties["keyPassword"] as String? ?: "123456" +val releaseStorePassword = keystoreProperties["storePassword"] as String? ?: "123456" + android { namespace = "com.yuanxuan.webshell.web_android_shell" compileSdk = flutter.compileSdkVersion @@ -31,11 +64,18 @@ android { versionName = flutter.versionName } + signingConfigs { + create("release") { + keyAlias = releaseKeyAlias + keyPassword = releaseKeyPassword + storeFile = releaseStoreFile + storePassword = releaseStorePassword + } + } + buildTypes { release { - // 待补充:为 release 构建配置正式签名。 - // 当前先使用 debug 签名,确保 `flutter run --release` 可直接运行。 - signingConfig = signingConfigs.getByName("debug") + signingConfig = signingConfigs.getByName("release") } } } diff --git a/packages/web_shell_core/devtools_options.yaml b/packages/web_shell_core/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/packages/web_shell_core/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/tool/generate_app.dart b/tool/generate_app.dart index e8ac6d7..a8ebde1 100644 --- a/tool/generate_app.dart +++ b/tool/generate_app.dart @@ -8,8 +8,14 @@ const List _defaultPreferredOrientations = [ 'portraitUp', 'portraitDown', ]; +final Directory _repoRootDir = File.fromUri(Platform.script).parent.parent; +final String _currentDartExecutable = Platform.resolvedExecutable; +final String _flutterExecutable = _resolveFlutterExecutable(); +final String _dartExecutable = _resolveDartExecutable(); Future main(List args) async { + Directory.current = _repoRootDir.path; + if (args.isEmpty) { print('\x1B[31m用法:dart run tool/generate_app.dart <品牌名>\x1B[0m'); print('\x1B[33m示例:dart run tool/generate_app.dart aixue\x1B[0m'); @@ -29,6 +35,8 @@ Future main(List args) async { } print('\x1B[34m[信息] 正在为品牌生成应用:$brand...\x1B[0m'); + print('\x1B[34m[信息] Flutter 命令:$_flutterExecutable\x1B[0m'); + print('\x1B[34m[信息] Dart 命令:$_dartExecutable\x1B[0m'); final yamlString = await configFile.readAsString(); final config = loadYaml(yamlString) as YamlMap; @@ -77,7 +85,180 @@ Future main(List args) async { print('\x1B[32m✔ 应用 $brand 已生成到 $appDir!\x1B[0m'); print('\x1B[34m构建应用请执行:\x1B[0m'); - print(' cd $appDir && flutter build apk'); + print(' cd $appDir'); + print(' ${_buildApkCommandForCurrentPlatform()}'); +} + +String _resolveFlutterExecutable() { + final candidates = [ + ..._toolCandidatesFromFlutterRoot(_flutterToolRelativePaths), + ..._toolCandidatesFromRepo(_flutterToolRelativePaths), + ..._flutterCandidatesFromCurrentDart(), + 'flutter', + ]; + + return _firstAvailableExecutable(candidates, fallback: 'flutter'); +} + +String _resolveDartExecutable() { + final candidates = [ + ..._toolCandidatesFromFlutterRoot(_dartToolRelativePaths), + ..._toolCandidatesFromRepo(_dartToolRelativePaths), + _currentDartExecutable, + 'dart', + ]; + + return _firstAvailableExecutable(candidates, fallback: 'dart'); +} + +List _flutterCandidatesFromCurrentDart() { + final dartExecutable = File(_currentDartExecutable); + final dartBinDir = dartExecutable.parent; + final dartSdkDir = dartBinDir.parent; + final cacheDir = dartSdkDir.parent; + final flutterBinDir = cacheDir.parent; + + if (_basename(dartSdkDir.path) != 'dart-sdk' || + _basename(cacheDir.path) != 'cache' || + !_isSameDirectoryName(flutterBinDir.path, 'bin')) { + return []; + } + + final flutterBat = File.fromUri(flutterBinDir.uri.resolve('flutter.bat')); + final flutterShell = File.fromUri(flutterBinDir.uri.resolve('flutter')); + + return [ + if (flutterBat.existsSync()) flutterBat.path, + if (flutterShell.existsSync()) flutterShell.path, + ]; +} + +List get _flutterToolRelativePaths => Platform.isWindows + ? ['bin/flutter.bat', 'bin/flutter'] + : ['bin/flutter', 'bin/flutter.bat']; + +List get _dartToolRelativePaths => Platform.isWindows + ? [ + 'bin/dart.bat', + 'bin/cache/dart-sdk/bin/dart.exe', + 'bin/dart', + 'bin/cache/dart-sdk/bin/dart', + ] + : ['bin/dart', 'bin/cache/dart-sdk/bin/dart']; + +List _toolCandidatesFromFlutterRoot(List relativePaths) { + final flutterRoot = Platform.environment['FLUTTER_ROOT']; + if (flutterRoot == null || flutterRoot.isEmpty) { + return []; + } + + return [ + for (final relativePath in relativePaths) + if (_fileExists(flutterRoot, relativePath)) + _joinPath(flutterRoot, relativePath), + ]; +} + +List _toolCandidatesFromRepo(List relativePaths) { + return [ + for (final relativePath in relativePaths) + if (_repoFile(relativePath).existsSync()) _repoFile(relativePath).path, + ]; +} + +String _firstAvailableExecutable( + List candidates, { + required String fallback, +}) { + for (final candidate in candidates) { + if (candidate == fallback || File(candidate).existsSync()) { + return candidate; + } + } + return fallback; +} + +bool _fileExists(String? basePath, String childPath) { + if (basePath == null || basePath.isEmpty) { + return false; + } + return File(_joinPath(basePath, childPath)).existsSync(); +} + +String _joinPath(String basePath, String childPath) { + final normalizedBase = basePath.replaceAll('\\', '/'); + final normalizedChild = childPath.replaceAll('\\', '/'); + return File.fromUri( + Directory(normalizedBase).uri.resolve(normalizedChild), + ).path; +} + +File _repoFile(String relativePath) { + return File.fromUri(_repoRootDir.uri.resolve(relativePath)); +} + +String _basename(String path) { + final normalized = path + .replaceAll('\\', '/') + .replaceFirst(RegExp(r'/+$'), ''); + final segments = normalized.split('/'); + return segments.isEmpty ? normalized : segments.last; +} + +bool _isSameDirectoryName(String path, String name) { + return _basename(path).toLowerCase() == name.toLowerCase(); +} + +String _buildApkCommandForCurrentPlatform() { + final fvmFlutter = _repoFile( + Platform.isWindows + ? '.fvm/flutter_sdk/bin/flutter.bat' + : '.fvm/flutter_sdk/bin/flutter', + ); + + if (fvmFlutter.existsSync()) { + final commandPath = Platform.isWindows + ? '../../.fvm/flutter_sdk/bin/flutter.bat' + : '../../.fvm/flutter_sdk/bin/flutter'; + return '$commandPath build apk'; + } + + return 'flutter build apk'; +} + +Future _runProcess( + String executable, + List arguments, { + String? workingDirectory, +}) async { + try { + return await Process.run( + executable, + arguments, + workingDirectory: workingDirectory, + runInShell: + Platform.isWindows && executable.toLowerCase().endsWith('.bat'), + ); + } on ProcessException catch (error) { + print('\x1B[31m[错误] 外部命令启动失败:$executable\x1B[0m'); + print('\x1B[33m命令参数:${arguments.join(' ')}\x1B[0m'); + print('\x1B[33m系统信息:${error.message}\x1B[0m'); + + if (executable == _flutterExecutable || executable == 'flutter') { + print('\x1B[33m建议先执行:\x1B[0m'); + if (Platform.isWindows) { + print( + ' \$env:PATH = "${_repoFile('.fvm/flutter_sdk/bin').path};\$env:PATH"', + ); + } else { + print( + ' export PATH="${_repoFile('.fvm/flutter_sdk/bin').path}:\$PATH"', + ); + } + } + + exit(1); + } } List _readPreferredOrientations(Object? raw) { @@ -123,7 +304,7 @@ Future _createFlutterApp( final segments = applicationId.split('.'); final org = segments.sublist(0, segments.length - 1).join('.'); - final result = await Process.run('flutter', [ + final result = await _runProcess(_flutterExecutable, [ 'create', '--org', org, @@ -144,7 +325,7 @@ Future _createFlutterApp( Future _addCoreDependency(String appDir) async { print('\x1B[34m[信息] 正在添加 web_shell_core 依赖...\x1B[0m'); - final result = await Process.run('flutter', [ + final result = await _runProcess(_flutterExecutable, [ 'pub', 'add', 'web_shell_core', @@ -316,7 +497,7 @@ Future _generateBrandingAssets( print('\x1B[32m✔ 品牌资源已复制到 ${brandTargetDir.path}\x1B[0m'); print('\x1B[34m[信息] 正在添加资源生成器依赖...\x1B[0m'); - final addDevDepsResult = await Process.run('flutter', [ + final addDevDepsResult = await _runProcess(_flutterExecutable, [ 'pub', 'add', '--dev', @@ -328,7 +509,7 @@ Future _generateBrandingAssets( exit(1); } - await Process.run('flutter', [ + await _runProcess(_flutterExecutable, [ 'pub', 'get', ], workingDirectory: appDir); @@ -360,7 +541,7 @@ flutter_native_splash: await File('$appDir/flutter_native_splash.yaml').writeAsString(splashYaml); print('\x1B[34m[信息] 正在生成应用图标...\x1B[0m'); - final iconsResult = await Process.run('dart', [ + final iconsResult = await _runProcess(_dartExecutable, [ 'run', 'flutter_launcher_icons', '-f', @@ -373,7 +554,7 @@ flutter_native_splash: } print('\x1B[34m[信息] 正在生成启动页...\x1B[0m'); - final splashResult = await Process.run('dart', [ + final splashResult = await _runProcess(_dartExecutable, [ 'run', 'flutter_native_splash:create', '--path=flutter_native_splash.yaml', @@ -385,7 +566,7 @@ flutter_native_splash: } print('\x1B[34m[信息] 正在移除资源生成器依赖...\x1B[0m'); - await Process.run('flutter', [ + await _runProcess(_flutterExecutable, [ 'pub', 'remove', 'flutter_launcher_icons', @@ -428,7 +609,7 @@ Future _configureSigning(String appDir, YamlMap config) async { final keyPassword = signing?['key_password'] as String? ?? '123456'; final storePassword = signing?['store_password'] as String? ?? '123456'; final storeFile = - signing?['store_file'] as String? ?? '../../../../tool/key.jks'; + signing?['store_file'] as String? ?? '../../../tool/key.jks'; final keyPropsContent = ''' @@ -448,15 +629,40 @@ storeFile=$storeFile var content = await gradleFile.readAsString(); if (!content.contains('val keystoreProperties = Properties()')) { content = content.replaceFirst('android {', ''' +import java.io.File import java.util.Properties import java.io.FileInputStream val keystoreProperties = Properties() val keystorePropertiesFile = rootProject.file("key.properties") if (keystorePropertiesFile.exists()) { - keystoreProperties.load(FileInputStream(keystorePropertiesFile)) + FileInputStream(keystorePropertiesFile).use { keystoreProperties.load(it) } } +val defaultReleaseStoreFile = rootProject.file("../../../tool/key.jks") +fun resolveKeystoreFile(rawPath: String?): File { + val candidate = rawPath?.trim().orEmpty() + if (candidate.isEmpty()) { + return defaultReleaseStoreFile + } + + val expandedHome = if (candidate.startsWith("~/") || candidate == "~") { + candidate.replaceFirst("~", System.getProperty("user.home")) + } else { + candidate + } + + val normalized = expandedHome.replace('\\', File.separatorChar).replace('/', File.separatorChar) + val storeFile = File(normalized) + return if (storeFile.isAbsolute) storeFile else rootProject.file(normalized) +} + +val releaseStoreFile = + resolveKeystoreFile(keystoreProperties["storeFile"] as String?) +val releaseKeyAlias = keystoreProperties["keyAlias"] as String? ?: "my-key-alias" +val releaseKeyPassword = keystoreProperties["keyPassword"] as String? ?: "123456" +val releaseStorePassword = keystoreProperties["storePassword"] as String? ?: "123456" + android { '''); } @@ -465,10 +671,10 @@ android { const newBuildTypes = ''' signingConfigs { create("release") { - keyAlias = keystoreProperties["keyAlias"] as String? - keyPassword = keystoreProperties["keyPassword"] as String? - storeFile = keystoreProperties["storeFile"]?.let { file(it as String) } - storePassword = keystoreProperties["storePassword"] as String? + keyAlias = releaseKeyAlias + keyPassword = releaseKeyPassword + storeFile = releaseStoreFile + storePassword = releaseStorePassword } }