feat: 重构启动配置与升级配置分离,修复 WebView 错误浮层竞态条件
- 将 ShellRemoteConfig 拆分为 ShellBootstrapConfig(启动配置)和 ShellUpgradeConfig(升级配置) - ShellBootstrapConfig 支持 initialUrl 和 preferredOrientations - ShellUpgradeService 改为独立获取升级配置,不再依赖启动配置 - ShellEnvironment 新增 bootstrapConfigAsset / bootstrapConfigUrl / upgradeConfigUrl / copyWith - 修复 WebView 错误浮层被 onPageFinished 重置的竞态条件 - 修复 Offstage 导致 Stack 坍缩为零尺寸的白屏问题,改用 Visibility - 增强 recovery.dart 对原始 net::ERR_* 字符串的友好翻译 - 重新生成 3 个品牌应用(aixue / test / yunxiao)的图标和启动页资源 - aixue 应用改用 bootstrapConfigAsset 加载本地启动配置 - 移除 launch.json 中已废弃的 quanxue 配置
This commit is contained in:
parent
bf0f0e082f
commit
0d259334ee
|
|
@ -29,26 +29,6 @@
|
|||
"type": "dart",
|
||||
"flutterMode": "release"
|
||||
},
|
||||
{
|
||||
"name": "quanxue",
|
||||
"cwd": "apps/quanxue",
|
||||
"request": "launch",
|
||||
"type": "dart"
|
||||
},
|
||||
{
|
||||
"name": "quanxue (profile mode)",
|
||||
"cwd": "apps/quanxue",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "profile"
|
||||
},
|
||||
{
|
||||
"name": "quanxue (release mode)",
|
||||
"cwd": "apps/quanxue",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "release"
|
||||
},
|
||||
{
|
||||
"name": "test",
|
||||
"cwd": "apps/test",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"initialUrl": "http://xszy.lzzneng.com/login.html",
|
||||
"preferredOrientations": [
|
||||
"portraitUp",
|
||||
"portraitDown"
|
||||
]
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ void main() {
|
|||
textColor: const Color(0xFF1F2937),
|
||||
mutedTextColor: const Color(0xFF6B7280),
|
||||
splashImage: const AssetImage('assets/branding/splash.png'),
|
||||
initialUrl: 'http://xszy.lzzneng.com/login.html',
|
||||
bootstrapConfigAsset: 'assets/config/bootstrap.json',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,14 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
code_assets:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_assets
|
||||
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -73,6 +81,14 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.3.5+2"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -105,6 +121,22 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.0.3"
|
||||
dio:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio
|
||||
sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.9.2"
|
||||
dio_web_adapter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio_web_adapter
|
||||
sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -182,6 +214,11 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_localizations:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -200,6 +237,22 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
hooks:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hooks
|
||||
sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -280,6 +333,14 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
intl:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: intl
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.20.2"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -312,6 +373,14 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -344,6 +413,14 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
native_toolchain_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: native_toolchain_c
|
||||
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.17.6"
|
||||
nm:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -352,6 +429,30 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
objective_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: objective_c
|
||||
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "9.3.0"
|
||||
package_info_plus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "9.0.0"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -360,6 +461,54 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.22"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.6.0"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
permission_handler:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -416,6 +565,14 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.0.2"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.6"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -424,6 +581,70 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
shared_preferences:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.5.4"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "8374d6200ab33ac99031a852eba4c8eb2170c4bf20778b3e2c9eccb45384fb41"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.21"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.5.6"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
|
@ -628,6 +849,14 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -636,6 +865,23 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.6.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
yx_app_upgrade_flutter:
|
||||
dependency: transitive
|
||||
description:
|
||||
path: "."
|
||||
ref: "2.0.6"
|
||||
resolved-ref: e6e7f8e951368b6fdd30c2fd237fd3e00252e568
|
||||
url: "https://gitea.23544.com/wangyang/yx_app_upgrade_flutter.git"
|
||||
source: git
|
||||
version: "1.0.5"
|
||||
sdks:
|
||||
dart: ">=3.11.0 <4.0.0"
|
||||
flutter: ">=3.38.0"
|
||||
flutter: ">=3.38.4"
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ dev_dependencies:
|
|||
flutter:
|
||||
assets:
|
||||
- assets/branding/
|
||||
- assets/config/
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
# the material Icons class.
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ if (keystorePropertiesFile.exists()) {
|
|||
|
||||
android {
|
||||
|
||||
namespace = "com.yuanxuan.test"
|
||||
namespace = "com.yuanxuan.test_shell"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.yuanxuan.test"
|
||||
applicationId = "com.yuanxuan.test_shell"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
package com.yuanxuan.test;
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity;
|
||||
|
||||
public class MainActivity extends FlutterActivity {
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"initialUrl": "http://192.168.2.57:8080/test_bridge.html",
|
||||
"preferredOrientations": [
|
||||
"portraitUp",
|
||||
"portraitDown"
|
||||
]
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ void main() {
|
|||
textColor: const Color(0xFFFFFFFF),
|
||||
mutedTextColor: const Color(0xFF9CA3AF),
|
||||
splashImage: const AssetImage('assets/branding/splash.png'),
|
||||
initialUrl: 'http://192.168.2.57:8080/test_bridge.html',
|
||||
bootstrapConfigAsset: 'assets/config/bootstrap.json',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,14 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
code_assets:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_assets
|
||||
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -73,6 +81,14 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.3.5+2"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -105,6 +121,22 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.0.3"
|
||||
dio:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio
|
||||
sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.9.2"
|
||||
dio_web_adapter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio_web_adapter
|
||||
sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -182,6 +214,11 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_localizations:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -200,6 +237,22 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
hooks:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hooks
|
||||
sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -280,6 +333,14 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
intl:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: intl
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.20.2"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -312,6 +373,14 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -344,6 +413,14 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
native_toolchain_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: native_toolchain_c
|
||||
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.17.6"
|
||||
nm:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -352,6 +429,30 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
objective_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: objective_c
|
||||
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "9.3.0"
|
||||
package_info_plus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "9.0.0"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -360,6 +461,54 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.22"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.6.0"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
permission_handler:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -416,6 +565,14 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.0.2"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.6"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -424,6 +581,70 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
shared_preferences:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.5.4"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "8374d6200ab33ac99031a852eba4c8eb2170c4bf20778b3e2c9eccb45384fb41"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.21"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.5.6"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
|
@ -628,6 +849,14 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -636,6 +865,23 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.6.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
yx_app_upgrade_flutter:
|
||||
dependency: transitive
|
||||
description:
|
||||
path: "."
|
||||
ref: "2.0.6"
|
||||
resolved-ref: e6e7f8e951368b6fdd30c2fd237fd3e00252e568
|
||||
url: "https://gitea.23544.com/wangyang/yx_app_upgrade_flutter.git"
|
||||
source: git
|
||||
version: "1.0.5"
|
||||
sdks:
|
||||
dart: ">=3.11.0 <4.0.0"
|
||||
flutter: ">=3.38.0"
|
||||
flutter: ">=3.38.4"
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ dev_dependencies:
|
|||
flutter:
|
||||
assets:
|
||||
- assets/branding/
|
||||
- assets/config/
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
# the material Icons class.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"initialUrl": "https://h5.jingdaka.com/pages/user/login/login?sourcePage=%252Fpages%252Fuser_sub%252Fuser_homework%252Fhomework_list%252Fhomework_list%253FcourseId%253D1821860%2526type%253Dredirect%2526domain_name%253Dmjunysod",
|
||||
"preferredOrientations": [
|
||||
"portraitUp",
|
||||
"portraitDown"
|
||||
]
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ void main() {
|
|||
textColor: const Color(0xFF1F2937),
|
||||
mutedTextColor: const Color(0xFF6B7280),
|
||||
splashImage: const AssetImage('assets/branding/splash.png'),
|
||||
initialUrl: 'https://h5.jingdaka.com/pages/user/login/login?sourcePage=%252Fpages%252Fuser_sub%252Fuser_homework%252Fhomework_list%252Fhomework_list%253FcourseId%253D1821860%2526type%253Dredirect%2526domain_name%253Dmjunysod',
|
||||
bootstrapConfigAsset: 'assets/config/bootstrap.json',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,14 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
code_assets:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_assets
|
||||
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -73,6 +81,14 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.3.5+2"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -105,6 +121,22 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.0.3"
|
||||
dio:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio
|
||||
sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.9.2"
|
||||
dio_web_adapter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio_web_adapter
|
||||
sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -182,6 +214,11 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_localizations:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -200,6 +237,22 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
hooks:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hooks
|
||||
sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -280,6 +333,14 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
intl:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: intl
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.20.2"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -312,6 +373,14 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -344,6 +413,14 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
native_toolchain_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: native_toolchain_c
|
||||
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.17.6"
|
||||
nm:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -352,6 +429,30 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
objective_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: objective_c
|
||||
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "9.3.0"
|
||||
package_info_plus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "9.0.0"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -360,6 +461,54 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.22"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.6.0"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
permission_handler:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -416,6 +565,14 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.0.2"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.6"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -424,6 +581,70 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
shared_preferences:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.5.4"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "8374d6200ab33ac99031a852eba4c8eb2170c4bf20778b3e2c9eccb45384fb41"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.21"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.5.6"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
|
@ -628,6 +849,14 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -636,6 +865,23 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.6.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
yx_app_upgrade_flutter:
|
||||
dependency: transitive
|
||||
description:
|
||||
path: "."
|
||||
ref: "2.0.6"
|
||||
resolved-ref: e6e7f8e951368b6fdd30c2fd237fd3e00252e568
|
||||
url: "https://gitea.23544.com/wangyang/yx_app_upgrade_flutter.git"
|
||||
source: git
|
||||
version: "1.0.5"
|
||||
sdks:
|
||||
dart: ">=3.11.0 <4.0.0"
|
||||
flutter: ">=3.38.0"
|
||||
flutter: ">=3.38.4"
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ dev_dependencies:
|
|||
flutter:
|
||||
assets:
|
||||
- assets/branding/
|
||||
- assets/config/
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
# the material Icons class.
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@ app_name: "爱学蝶变"
|
|||
application_id: "com.yuanxuan.aixue"
|
||||
app_key: "aixue_prod"
|
||||
default_url: "http://xszy.lzzneng.com/login.html"
|
||||
bootstrap_config_url: ""
|
||||
upgrade_config_url: ""
|
||||
preferred_orientations:
|
||||
- "portraitUp"
|
||||
- "portraitDown"
|
||||
theme:
|
||||
accent_color: "0xFFF97316"
|
||||
bg_color: "0xFFFFFFFF"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@ app_name: "测试壳工程"
|
|||
application_id: "com.yuanxuan.test_shell"
|
||||
app_key: "test_shell"
|
||||
default_url: "http://192.168.2.57:8080/test_bridge.html"
|
||||
bootstrap_config_url: ""
|
||||
upgrade_config_url: ""
|
||||
preferred_orientations:
|
||||
- "portraitUp"
|
||||
- "portraitDown"
|
||||
theme:
|
||||
accent_color: "0xFF10B981"
|
||||
bg_color: "0xFF1F2937"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@ app_name: "云校嗨学"
|
|||
application_id: "com.yuanxuan.yunxiao"
|
||||
app_key: "yunxiao_prod"
|
||||
default_url: "https://h5.jingdaka.com/pages/user/login/login?sourcePage=%252Fpages%252Fuser_sub%252Fuser_homework%252Fhomework_list%252Fhomework_list%253FcourseId%253D1821860%2526type%253Dredirect%2526domain_name%253Dmjunysod"
|
||||
bootstrap_config_url: ""
|
||||
upgrade_config_url: ""
|
||||
preferred_orientations:
|
||||
- "portraitUp"
|
||||
- "portraitDown"
|
||||
theme:
|
||||
accent_color: "0xFF4F46E5"
|
||||
bg_color: "0xFFFFFFFF"
|
||||
|
|
|
|||
|
|
@ -14,10 +14,26 @@ Android 平板专用的 H5 壳核心库。所有品牌应用共享此库,只
|
|||
| **权限服务** | camera · microphone · location · photos · videos · storage 统一映射 |
|
||||
| **导航服务** | URL scheme 白名单路由,非 WebView 协议自动跳转外部应用 |
|
||||
| **壳层 UI** | 启动加载动画 · 错误恢复页 · 进度条 · 不支持平台兜底页 |
|
||||
| **启动配置** | 支持本地默认启动配置文件 + 远程启动配置缓存 |
|
||||
| **版本检查** | 升级配置独立请求,不再与启动配置共用一个 JSON |
|
||||
|
||||
## 使用方式
|
||||
|
||||
### 1. 准备本地默认启动配置文件
|
||||
|
||||
`assets/config/bootstrap.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"initialUrl": "https://example.com/login",
|
||||
"preferredOrientations": ["portraitUp", "portraitDown"]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 在应用入口传入环境配置
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:web_shell_core/web_shell_core.dart';
|
||||
|
||||
void main() {
|
||||
|
|
@ -29,12 +45,40 @@ void main() {
|
|||
backgroundColor: Color(0xFFFFFFFF),
|
||||
textColor: Color(0xFF1F2937),
|
||||
mutedTextColor: Color(0xFF6B7280),
|
||||
initialUrl: 'example.com/login', // 可选,不传使用默认地址
|
||||
bootstrapConfigAsset: 'assets/config/bootstrap.json',
|
||||
bootstrapConfigUrl: 'https://example.com/bootstrap.json',
|
||||
upgradeConfigUrl: 'https://example.com/upgrade.json',
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 远程启动配置格式
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"initialUrl": "https://example.com/login",
|
||||
"preferredOrientations": ["portraitUp", "portraitDown"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 远程升级配置格式
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"versionName": "1.0.1",
|
||||
"version": 101,
|
||||
"isForce": 0,
|
||||
"remark": "1. 修复已知问题\n2. 优化启动体验",
|
||||
"filePath": "https://example.com/app-release.apk",
|
||||
"fileSize": 25000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 代码结构
|
||||
|
||||
```
|
||||
|
|
@ -53,9 +97,11 @@ lib/
|
|||
│ ├── bridge_actions.dart # Action handler(占位)
|
||||
│ └── legacy_camera_compat.dart # 旧相机 JS 兼容层
|
||||
├── services/
|
||||
│ ├── config_service.dart # 启动配置读取与缓存
|
||||
│ ├── media_service.dart # 相机/图库/文件 + 序列化
|
||||
│ ├── permission_service.dart # 权限类型映射
|
||||
│ └── navigation_service.dart # URL 路由 + 外链跳转
|
||||
│ ├── navigation_service.dart # URL 路由 + 外链跳转
|
||||
│ └── upgrade_service.dart # 升级配置请求与弹窗转换
|
||||
├── ui/
|
||||
│ ├── shell_app.dart # MaterialApp 入口
|
||||
│ ├── shell_page.dart # WebView 主页面
|
||||
|
|
@ -74,10 +120,10 @@ cd packages/web_shell_core
|
|||
flutter test
|
||||
```
|
||||
|
||||
当前 **67 个测试用例**,覆盖:
|
||||
当前测试覆盖:
|
||||
- 平台检测 · URL 解析 · 兼容性策略 · 错误映射
|
||||
- Bridge 注入/响应/异常处理 · 媒体序列化 · 权限映射
|
||||
- 导航路由 · 所有独立 UI 组件
|
||||
- 导航路由 · 启动配置解析 · 方向配置 · 组件渲染
|
||||
|
||||
## 平台约束
|
||||
|
||||
|
|
|
|||
|
|
@ -11,11 +11,17 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:webview_flutter_android/webview_flutter_android.dart';
|
||||
import 'package:yx_app_upgrade_flutter/yx_app_upgrade_flutter.dart';
|
||||
|
||||
export 'package:yx_app_upgrade_flutter/yx_app_upgrade_flutter.dart'
|
||||
show AppUpgradeVersion;
|
||||
|
||||
// ── 配置 ──
|
||||
part 'src/config/shell_environment.dart';
|
||||
|
|
@ -31,18 +37,20 @@ part 'src/bridge/bridge_actions.dart';
|
|||
part 'src/bridge/legacy_camera_compat.dart';
|
||||
|
||||
// ── 服务 ──
|
||||
part 'src/services/config_service.dart';
|
||||
part 'src/services/media_service.dart';
|
||||
part 'src/services/permission_service.dart';
|
||||
part 'src/services/navigation_service.dart';
|
||||
part 'src/services/permission_service.dart';
|
||||
part 'src/services/upgrade_service.dart';
|
||||
|
||||
// ── 界面 ──
|
||||
part 'src/testing/test_hooks.dart';
|
||||
part 'src/ui/error_overlay.dart';
|
||||
part 'src/ui/launch_overlay.dart';
|
||||
part 'src/ui/progress_bar.dart';
|
||||
part 'src/ui/shell_app.dart';
|
||||
part 'src/ui/shell_page.dart';
|
||||
part 'src/ui/launch_overlay.dart';
|
||||
part 'src/ui/error_overlay.dart';
|
||||
part 'src/ui/progress_bar.dart';
|
||||
part 'src/ui/unsupported_platform_page.dart';
|
||||
part 'src/testing/test_hooks.dart';
|
||||
|
||||
// ── 全局环境 ──
|
||||
late ShellEnvironment _env;
|
||||
|
|
@ -51,23 +59,79 @@ Color get _shellAccentColor => _env.accentColor;
|
|||
Color get _shellBackgroundColor => _env.backgroundColor;
|
||||
Color get _shellTextColor => _env.textColor;
|
||||
Color get _shellMutedTextColor => _env.mutedTextColor;
|
||||
List<DeviceOrientation> get _shellPreferredOrientations =>
|
||||
_env.preferredOrientations ??
|
||||
const <DeviceOrientation>[
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown,
|
||||
];
|
||||
|
||||
// ── 入口 ──
|
||||
|
||||
/// 启动壳应用的唯一入口。
|
||||
/// 各品牌应用的 `main.dart` 调用此函数,并传入自己的 [ShellEnvironment]。
|
||||
Future<void> runShellApp(ShellEnvironment environment) async {
|
||||
_env = environment;
|
||||
_initializeUrls();
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await SystemChrome.setPreferredOrientations(const [
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown,
|
||||
]);
|
||||
_env = environment;
|
||||
|
||||
await _applyBootstrapConfig();
|
||||
ShellUpgradeService.instance.setupConfigUrl(_env.upgradeConfigUrl);
|
||||
|
||||
_initializeUrls();
|
||||
await SystemChrome.setPreferredOrientations(_shellPreferredOrientations);
|
||||
await _enterImmersiveMode();
|
||||
runApp(const ShellApp());
|
||||
}
|
||||
|
||||
Future<void> _applyBootstrapConfig() async {
|
||||
final bootstrapAsset = _env.bootstrapConfigAsset;
|
||||
if (bootstrapAsset != null && bootstrapAsset.isNotEmpty) {
|
||||
debugPrint('WebShell 正在读取本地启动配置: $bootstrapAsset');
|
||||
final localConfig = await ShellBootstrapConfigService.loadDefaultConfig(
|
||||
bootstrapAsset,
|
||||
);
|
||||
if (localConfig != null) {
|
||||
_mergeBootstrapConfig(localConfig, source: '本地启动配置');
|
||||
}
|
||||
}
|
||||
|
||||
final bootstrapConfigUrl = _env.bootstrapConfigUrl;
|
||||
if (bootstrapConfigUrl != null && bootstrapConfigUrl.isNotEmpty) {
|
||||
debugPrint('WebShell 正在获取远程启动配置: $bootstrapConfigUrl');
|
||||
final remoteConfig = await ShellBootstrapConfigService.fetchConfig(
|
||||
bootstrapConfigUrl,
|
||||
);
|
||||
if (remoteConfig != null) {
|
||||
_mergeBootstrapConfig(remoteConfig, source: '远程启动配置');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _mergeBootstrapConfig(
|
||||
ShellBootstrapConfig config, {
|
||||
required String source,
|
||||
}) {
|
||||
final initialUrl = config.initialUrl?.trim();
|
||||
final preferredOrientations = config.preferredOrientations;
|
||||
|
||||
if (initialUrl != null && initialUrl.isNotEmpty) {
|
||||
debugPrint('WebShell $source 覆盖了初始地址: $initialUrl');
|
||||
}
|
||||
if (preferredOrientations != null) {
|
||||
debugPrint(
|
||||
'WebShell $source 覆盖了首屏方向: '
|
||||
'${preferredOrientations.map((item) => item.name).join(', ')}',
|
||||
);
|
||||
}
|
||||
|
||||
_env = _env.copyWith(
|
||||
initialUrl: initialUrl != null && initialUrl.isNotEmpty
|
||||
? initialUrl
|
||||
: _env.initialUrl,
|
||||
preferredOrientations: preferredOrientations ?? _env.preferredOrientations,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _enterImmersiveMode() async {
|
||||
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
|
|
|
|||
|
|
@ -13,29 +13,77 @@ class ShellEnvironment {
|
|||
required this.mutedTextColor,
|
||||
this.splashImage,
|
||||
this.initialUrl,
|
||||
this.bootstrapConfigAsset,
|
||||
this.bootstrapConfigUrl,
|
||||
this.upgradeConfigUrl,
|
||||
this.preferredOrientations,
|
||||
});
|
||||
|
||||
/// 应用显示名称(如 "全学通"、"点智学")
|
||||
/// 应用显示名称(如 "全学通"、"点智学")。
|
||||
final String appName;
|
||||
|
||||
/// 应用唯一标识符(用于后端区分品牌)
|
||||
/// 应用唯一标识符(用于后端区分品牌)。
|
||||
final String appKey;
|
||||
|
||||
/// 品牌主题强调色
|
||||
/// 品牌主题强调色。
|
||||
final Color accentColor;
|
||||
|
||||
/// 品牌背景色
|
||||
/// 品牌背景色。
|
||||
final Color backgroundColor;
|
||||
|
||||
/// 品牌正文文字色
|
||||
/// 品牌正文文字色。
|
||||
final Color textColor;
|
||||
|
||||
/// 品牌次要文字色
|
||||
/// 品牌次要文字色。
|
||||
final Color mutedTextColor;
|
||||
|
||||
/// 可选的品牌启动页图标;为空时使用默认图标。
|
||||
final ImageProvider? splashImage;
|
||||
|
||||
/// 可选的初始地址;为空时使用默认地址。
|
||||
/// 可选的初始地址;作为本地默认启动配置缺省时的兜底值。
|
||||
final String? initialUrl;
|
||||
|
||||
/// 可选的本地默认启动配置文件路径。
|
||||
final String? bootstrapConfigAsset;
|
||||
|
||||
/// 可选的远程启动配置地址;配置中可指定 `initialUrl` 及方向锁定。
|
||||
final String? bootstrapConfigUrl;
|
||||
|
||||
/// 可选的远程升级配置地址。
|
||||
final String? upgradeConfigUrl;
|
||||
|
||||
/// 可选的页面首屏方向锁定配置;为空时使用默认竖屏。
|
||||
final List<DeviceOrientation>? preferredOrientations;
|
||||
|
||||
/// 返回带局部覆盖的新配置对象。
|
||||
ShellEnvironment copyWith({
|
||||
String? appName,
|
||||
String? appKey,
|
||||
Color? accentColor,
|
||||
Color? backgroundColor,
|
||||
Color? textColor,
|
||||
Color? mutedTextColor,
|
||||
ImageProvider? splashImage,
|
||||
String? initialUrl,
|
||||
String? bootstrapConfigAsset,
|
||||
String? bootstrapConfigUrl,
|
||||
String? upgradeConfigUrl,
|
||||
List<DeviceOrientation>? preferredOrientations,
|
||||
}) {
|
||||
return ShellEnvironment(
|
||||
appName: appName ?? this.appName,
|
||||
appKey: appKey ?? this.appKey,
|
||||
accentColor: accentColor ?? this.accentColor,
|
||||
backgroundColor: backgroundColor ?? this.backgroundColor,
|
||||
textColor: textColor ?? this.textColor,
|
||||
mutedTextColor: mutedTextColor ?? this.mutedTextColor,
|
||||
splashImage: splashImage ?? this.splashImage,
|
||||
initialUrl: initialUrl ?? this.initialUrl,
|
||||
bootstrapConfigAsset: bootstrapConfigAsset ?? this.bootstrapConfigAsset,
|
||||
bootstrapConfigUrl: bootstrapConfigUrl ?? this.bootstrapConfigUrl,
|
||||
upgradeConfigUrl: upgradeConfigUrl ?? this.upgradeConfigUrl,
|
||||
preferredOrientations:
|
||||
preferredOrientations ?? this.preferredOrientations,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,9 +23,24 @@ String _friendlyErrorMessage(WebResourceError error) {
|
|||
WebResourceErrorType.connect ||
|
||||
WebResourceErrorType.io => '没有成功连接到服务器,请检查网络后重试。',
|
||||
WebResourceErrorType.failedSslHandshake => '当前站点证书校验失败,请稍后再试。',
|
||||
_ =>
|
||||
error.description.trim().isEmpty
|
||||
? '请稍后重新加载页面。'
|
||||
: error.description.trim(),
|
||||
_ => _parseRawErrorDescription(error.description),
|
||||
};
|
||||
}
|
||||
|
||||
String _parseRawErrorDescription(String description) {
|
||||
final cleanDesc = description.trim();
|
||||
if (cleanDesc.isEmpty) {
|
||||
return '请稍后重新加载页面。';
|
||||
}
|
||||
final lower = cleanDesc.toLowerCase();
|
||||
if (lower.contains('err_internet_disconnected') || lower.contains('err_address_unreachable') || lower.contains('err_name_not_resolved')) {
|
||||
return '没有成功连接到服务器,请检查网络后重试。';
|
||||
}
|
||||
if (lower.contains('err_connection_timed_out') || lower.contains('err_timed_out')) {
|
||||
return '当前网络较慢,请稍后重新加载。';
|
||||
}
|
||||
if (lower.contains('err_cert_') || lower.contains('err_ssl_')) {
|
||||
return '当前站点证书校验失败,请稍后再试。';
|
||||
}
|
||||
return cleanDesc;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,132 @@
|
|||
part of '../../core_app.dart';
|
||||
|
||||
/// 壳应用的启动配置模型。
|
||||
class ShellBootstrapConfig {
|
||||
/// 远程或本地配置指定的首页地址。
|
||||
final String? initialUrl;
|
||||
|
||||
/// 应用首屏锁定方向;`null` 表示沿用宿主默认配置。
|
||||
final List<DeviceOrientation>? preferredOrientations;
|
||||
|
||||
ShellBootstrapConfig({
|
||||
this.initialUrl,
|
||||
this.preferredOrientations,
|
||||
});
|
||||
|
||||
factory ShellBootstrapConfig.fromJson(Map<String, dynamic> json) {
|
||||
return ShellBootstrapConfig(
|
||||
initialUrl: json['initialUrl']?.toString(),
|
||||
preferredOrientations: _parsePreferredOrientations(
|
||||
json['preferredOrientations'] ?? json['orientations'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static List<DeviceOrientation>? _parsePreferredOrientations(Object? value) {
|
||||
if (value is! List) {
|
||||
return null;
|
||||
}
|
||||
if (value.isEmpty) {
|
||||
return const <DeviceOrientation>[];
|
||||
}
|
||||
|
||||
final orientations = <DeviceOrientation>[];
|
||||
for (final item in value) {
|
||||
final orientation = _parseDeviceOrientation(item?.toString());
|
||||
if (orientation != null && !orientations.contains(orientation)) {
|
||||
orientations.add(orientation);
|
||||
}
|
||||
}
|
||||
return orientations.isEmpty ? null : orientations;
|
||||
}
|
||||
|
||||
static DeviceOrientation? _parseDeviceOrientation(String? raw) {
|
||||
final normalized = raw?.trim();
|
||||
if (normalized == null || normalized.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return switch (normalized) {
|
||||
'portraitUp' ||
|
||||
'DeviceOrientation.portraitUp' => DeviceOrientation.portraitUp,
|
||||
'portraitDown' ||
|
||||
'DeviceOrientation.portraitDown' => DeviceOrientation.portraitDown,
|
||||
'landscapeLeft' ||
|
||||
'DeviceOrientation.landscapeLeft' => DeviceOrientation.landscapeLeft,
|
||||
'landscapeRight' ||
|
||||
'DeviceOrientation.landscapeRight' => DeviceOrientation.landscapeRight,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 负责读取本地默认启动配置,并获取远程启动配置。
|
||||
class ShellBootstrapConfigService {
|
||||
static const String _cacheKey = 'webshell_bootstrap_config_cache';
|
||||
|
||||
/// 从本地 asset 读取默认启动配置。
|
||||
static Future<ShellBootstrapConfig?> loadDefaultConfig(
|
||||
String assetPath,
|
||||
) async {
|
||||
try {
|
||||
final content = await rootBundle.loadString(assetPath);
|
||||
return _parseConfigString(content);
|
||||
} catch (e) {
|
||||
debugPrint('读取 WebShell 本地启动配置失败: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取并解析远程启动配置。如果网络请求失败,则尝试返回本地缓存。
|
||||
static Future<ShellBootstrapConfig?> fetchConfig(String url) async {
|
||||
try {
|
||||
final uri = Uri.tryParse(url);
|
||||
if (uri == null) {
|
||||
return _loadFromCache();
|
||||
}
|
||||
|
||||
final response = await http.get(uri).timeout(const Duration(seconds: 10));
|
||||
if (response.statusCode == 200) {
|
||||
final content = utf8.decode(response.bodyBytes);
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_cacheKey, content);
|
||||
return _parseConfigString(content);
|
||||
}
|
||||
return _loadFromCache();
|
||||
} catch (e) {
|
||||
debugPrint('获取 WebShell 启动配置失败(网络异常/超时),尝试读取缓存: $e');
|
||||
return _loadFromCache();
|
||||
}
|
||||
}
|
||||
|
||||
/// 从本地缓存读取上次成功获取的启动配置。
|
||||
static Future<ShellBootstrapConfig?> _loadFromCache() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final cachedContent = prefs.getString(_cacheKey);
|
||||
if (cachedContent != null && cachedContent.isNotEmpty) {
|
||||
debugPrint('成功读取到本地缓存的 WebShell 启动配置');
|
||||
return _parseConfigString(cachedContent);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('读取 WebShell 启动配置缓存失败: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 解析字符串格式的 JSON 启动配置。
|
||||
static ShellBootstrapConfig? _parseConfigString(String content) {
|
||||
try {
|
||||
final dynamic jsonMap = jsonDecode(content);
|
||||
if (jsonMap is Map<String, dynamic>) {
|
||||
final data = jsonMap['data'] is Map<String, dynamic>
|
||||
? jsonMap['data'] as Map<String, dynamic>
|
||||
: jsonMap;
|
||||
return ShellBootstrapConfig.fromJson(data);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('解析 WebShell 启动配置异常: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
part of '../../core_app.dart';
|
||||
|
||||
/// 远程升级配置模型。
|
||||
class ShellUpgradeConfig {
|
||||
final String? versionName;
|
||||
final int? version;
|
||||
final int? isForce;
|
||||
final String? remark;
|
||||
final String? filePath;
|
||||
final int? fileSize;
|
||||
|
||||
ShellUpgradeConfig({
|
||||
this.versionName,
|
||||
this.version,
|
||||
this.isForce,
|
||||
this.remark,
|
||||
this.filePath,
|
||||
this.fileSize,
|
||||
});
|
||||
|
||||
factory ShellUpgradeConfig.fromJson(Map<String, dynamic> json) {
|
||||
return ShellUpgradeConfig(
|
||||
versionName: json['versionName']?.toString(),
|
||||
version: json['version'] as int?,
|
||||
isForce: (json['isForce'] ?? json['isforce']) as int?,
|
||||
remark: json['remark']?.toString(),
|
||||
filePath: json['filePath']?.toString(),
|
||||
fileSize: json['fileSize'] as int?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 管理壳应用的升级逻辑,依赖 `yx_app_upgrade_flutter`。
|
||||
class ShellUpgradeService {
|
||||
static final ShellUpgradeService instance = ShellUpgradeService._();
|
||||
ShellUpgradeService._();
|
||||
|
||||
String? _configUrl;
|
||||
|
||||
/// 注入升级配置地址。
|
||||
void setupConfigUrl(String? configUrl) {
|
||||
_configUrl = configUrl?.trim();
|
||||
}
|
||||
|
||||
/// 检查应用版本,若配置中有更新则会弹出升级弹窗。
|
||||
Future<void> checkVersion(
|
||||
BuildContext context, {
|
||||
bool showNoUpdateToast = false,
|
||||
}) async {
|
||||
final configUrl = _configUrl;
|
||||
if (configUrl == null || configUrl.isEmpty) {
|
||||
if (showNoUpdateToast) {
|
||||
_showToastFromBridge(context, {'message': '未配置升级地址'});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final remoteConfig = await _fetchConfig(configUrl);
|
||||
if (remoteConfig == null) {
|
||||
if (showNoUpdateToast) {
|
||||
_showToastFromBridge(context, {'message': '未获取到版本配置'});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
UpgradeAuxiliaryUtils.instance.initiateVersionCheck(
|
||||
context,
|
||||
showNoUpdateToast: showNoUpdateToast,
|
||||
future: (int upType) async {
|
||||
return _convertToAppUpgradeVersion(remoteConfig);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<ShellUpgradeConfig?> _fetchConfig(String url) async {
|
||||
try {
|
||||
final uri = Uri.tryParse(url);
|
||||
if (uri == null) {
|
||||
return null;
|
||||
}
|
||||
debugPrint('WebShell 正在获取升级配置: $url');
|
||||
final response = await http.get(uri).timeout(const Duration(seconds: 10));
|
||||
if (response.statusCode != 200) {
|
||||
debugPrint('获取 WebShell 升级配置失败,状态码=${response.statusCode}');
|
||||
return null;
|
||||
}
|
||||
final content = utf8.decode(response.bodyBytes);
|
||||
return _parseConfigString(content);
|
||||
} catch (e) {
|
||||
debugPrint('获取 WebShell 升级配置失败(网络异常/超时): $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
ShellUpgradeConfig? _parseConfigString(String content) {
|
||||
try {
|
||||
final dynamic jsonMap = jsonDecode(content);
|
||||
if (jsonMap is Map<String, dynamic>) {
|
||||
final data = jsonMap['data'] is Map<String, dynamic>
|
||||
? jsonMap['data'] as Map<String, dynamic>
|
||||
: jsonMap;
|
||||
return ShellUpgradeConfig.fromJson(data);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('解析 WebShell 升级配置异常: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 将远程 JSON 配置转换为升级弹窗所需的数据模型。
|
||||
AppUpgradeVersion? _convertToAppUpgradeVersion(ShellUpgradeConfig config) {
|
||||
if (config.version == null || config.versionName == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final apkSizeBytes = config.fileSize != null
|
||||
? config.fileSize! * 1024
|
||||
: null;
|
||||
final filePath = (config.filePath != null && config.filePath!.isNotEmpty)
|
||||
? config.filePath
|
||||
: null;
|
||||
|
||||
return AppUpgradeVersion(
|
||||
versionName: config.versionName,
|
||||
versionBuildNumber: config.version,
|
||||
isForce: config.isForce == 1,
|
||||
updateContent: config.remark ?? '',
|
||||
downloadUrl: filePath,
|
||||
appStoreUrl: filePath,
|
||||
apkSize: apkSizeBytes,
|
||||
supportedMethods: [
|
||||
AppUpgradeMethod.browser,
|
||||
AppUpgradeMethod.inApp,
|
||||
AppUpgradeMethod.market,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,20 @@ class ShellCoreTestHooks {
|
|||
/// 返回当前初始 URI。
|
||||
Uri get initialUri => _initialUri;
|
||||
|
||||
/// 返回当前生效的首屏方向配置。
|
||||
List<DeviceOrientation> get preferredOrientations =>
|
||||
_shellPreferredOrientations;
|
||||
|
||||
/// 解析字符串格式的启动配置。
|
||||
ShellBootstrapConfig? parseBootstrapConfigString(String content) {
|
||||
return ShellBootstrapConfigService._parseConfigString(content);
|
||||
}
|
||||
|
||||
/// 读取本地默认启动配置。
|
||||
Future<ShellBootstrapConfig?> loadDefaultBootstrapConfig(String assetPath) {
|
||||
return ShellBootstrapConfigService.loadDefaultConfig(assetPath);
|
||||
}
|
||||
|
||||
/// 为测试初始化全局壳环境与初始地址。
|
||||
void initializeEnvironment(ShellEnvironment environment) {
|
||||
_env = environment;
|
||||
|
|
|
|||
|
|
@ -57,6 +57,8 @@ class _WebShellPageState extends State<WebShellPage>
|
|||
}
|
||||
_hasTriggeredInitialLoad = true;
|
||||
unawaited(_loadInitialPage());
|
||||
// 触发版本检测(如果配置了升级地址才会弹窗)
|
||||
unawaited(ShellUpgradeService.instance.checkVersion(context));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -339,7 +341,9 @@ class _WebShellPageState extends State<WebShellPage>
|
|||
}
|
||||
_recordWebViewEvent('页面加载完成:$url');
|
||||
_cancelStartupWatchdog();
|
||||
unawaited(_injectAppShellBridge(_controller, url));
|
||||
if (!_hasMainFrameError) {
|
||||
unawaited(_injectAppShellBridge(_controller, url));
|
||||
}
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -349,8 +353,9 @@ class _WebShellPageState extends State<WebShellPage>
|
|||
_progress = 100;
|
||||
_isLoadingPage = false;
|
||||
_hasBootstrapped = true;
|
||||
_hasMainFrameError = false;
|
||||
_startupRetryCount = 0;
|
||||
if (!_hasMainFrameError) {
|
||||
_startupRetryCount = 0;
|
||||
}
|
||||
});
|
||||
},
|
||||
onHttpError: (error) {
|
||||
|
|
@ -1038,7 +1043,10 @@ class _WebShellPageState extends State<WebShellPage>
|
|||
),
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
_webViewWidget,
|
||||
Visibility(
|
||||
visible: !_hasMainFrameError,
|
||||
child: _webViewWidget,
|
||||
),
|
||||
if (showProgressBar)
|
||||
Positioned(
|
||||
top: 0,
|
||||
|
|
@ -1050,16 +1058,20 @@ class _WebShellPageState extends State<WebShellPage>
|
|||
),
|
||||
),
|
||||
if (showLaunchOverlay)
|
||||
LaunchOverlay(
|
||||
progress: _progress,
|
||||
hasMeasuredProgress: _hasMeasuredProgress,
|
||||
Positioned.fill(
|
||||
child: LaunchOverlay(
|
||||
progress: _progress,
|
||||
hasMeasuredProgress: _hasMeasuredProgress,
|
||||
),
|
||||
),
|
||||
if (_hasMainFrameError)
|
||||
ErrorOverlay(
|
||||
title: _errorTitle,
|
||||
message: _errorMessage,
|
||||
currentUrl: _currentUrl,
|
||||
onRetry: _reloadPage,
|
||||
Positioned.fill(
|
||||
child: ErrorOverlay(
|
||||
title: _errorTitle,
|
||||
message: _errorMessage,
|
||||
currentUrl: _currentUrl,
|
||||
onRetry: _reloadPage,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -1068,4 +1080,5 @@ class _WebShellPageState extends State<WebShellPage>
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
// coverage:ignore-end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
name: web_shell_core
|
||||
description: "Android 平板专用 H5 壳核心库,提供 WebView 引擎、JS Bridge 和宿主服务。"
|
||||
version: 0.0.1
|
||||
publish_to: none
|
||||
homepage:
|
||||
|
||||
environment:
|
||||
|
|
@ -13,12 +14,18 @@ dependencies:
|
|||
file_picker: ^10.3.10
|
||||
flutter:
|
||||
sdk: flutter
|
||||
http: ^1.2.0
|
||||
image_picker: ^1.2.1
|
||||
permission_handler: ^12.0.1
|
||||
plugin_platform_interface: ^2.0.2
|
||||
shared_preferences: ^2.3.2
|
||||
url_launcher: ^6.3.2
|
||||
webview_flutter: ^4.13.1
|
||||
webview_flutter_android: ^4.10.13
|
||||
yx_app_upgrade_flutter:
|
||||
git:
|
||||
url: https://gitea.23544.com/wangyang/yx_app_upgrade_flutter.git
|
||||
ref: 2.0.6
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -35,10 +37,14 @@ void main() {
|
|||
late _FakeImagePickerPlatform fakeImagePickerPlatform;
|
||||
late _FakeUrlLauncherPlatform fakeUrlLauncherPlatform;
|
||||
late List<String> platformCalls;
|
||||
late List<MethodCall> platformMethodCalls;
|
||||
late Map<String, String> assetContents;
|
||||
late bool cameraPermissionGranted;
|
||||
|
||||
setUp(() {
|
||||
platformCalls = <String>[];
|
||||
platformMethodCalls = <MethodCall>[];
|
||||
assetContents = <String, String>{};
|
||||
cameraPermissionGranted = true;
|
||||
fakeWebViewPlatform = _FakeWebViewPlatform();
|
||||
fakeImagePickerPlatform = _FakeImagePickerPlatform();
|
||||
|
|
@ -52,9 +58,24 @@ void main() {
|
|||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(_platformChannel, (call) async {
|
||||
platformCalls.add(call.method);
|
||||
platformMethodCalls.add(call);
|
||||
return null;
|
||||
});
|
||||
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMessageHandler('flutter/assets', (message) async {
|
||||
if (message == null) {
|
||||
return null;
|
||||
}
|
||||
final assetKey = utf8.decode(message.buffer.asUint8List());
|
||||
final content = assetContents[assetKey];
|
||||
if (content == null) {
|
||||
return null;
|
||||
}
|
||||
final bytes = Uint8List.fromList(utf8.encode(content));
|
||||
return ByteData.view(bytes.buffer);
|
||||
});
|
||||
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(_permissionChannel, (call) async {
|
||||
if (call.method != 'requestPermissions') {
|
||||
|
|
@ -75,6 +96,8 @@ void main() {
|
|||
.setMockMethodCallHandler(_platformChannel, null);
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(_permissionChannel, null);
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMessageHandler('flutter/assets', null);
|
||||
});
|
||||
|
||||
group('运行时与平台', () {
|
||||
|
|
@ -126,6 +149,71 @@ void main() {
|
|||
}
|
||||
});
|
||||
|
||||
testWidgets('runShellApp 会应用自定义首屏方向', (tester) async {
|
||||
try {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.linux;
|
||||
|
||||
await runShellApp(
|
||||
const ShellEnvironment(
|
||||
appName: '横屏应用',
|
||||
appKey: 'landscape_app',
|
||||
accentColor: Color(0xFF3ED37B),
|
||||
backgroundColor: Color(0xFFFFFFFF),
|
||||
textColor: Color(0xFF1F2937),
|
||||
mutedTextColor: Color(0xFF6B7280),
|
||||
initialUrl: 'https://example.com/start',
|
||||
preferredOrientations: <DeviceOrientation>[
|
||||
DeviceOrientation.landscapeLeft,
|
||||
],
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(
|
||||
shellCoreTestHooks.preferredOrientations,
|
||||
<DeviceOrientation>[DeviceOrientation.landscapeLeft],
|
||||
);
|
||||
final orientationCall = platformMethodCalls.lastWhere(
|
||||
(call) => call.method == 'SystemChrome.setPreferredOrientations',
|
||||
);
|
||||
expect(orientationCall.arguments.toString(), contains('landscapeLeft'));
|
||||
} finally {
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('runShellApp 会读取本地默认启动配置文件', (tester) async {
|
||||
try {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.linux;
|
||||
assetContents['assets/config/test_bootstrap.json'] =
|
||||
'{"initialUrl":"https://asset.example.com/start","preferredOrientations":["landscapeRight"]}';
|
||||
|
||||
await runShellApp(
|
||||
const ShellEnvironment(
|
||||
appName: '本地配置应用',
|
||||
appKey: 'asset_app',
|
||||
accentColor: Color(0xFF3ED37B),
|
||||
backgroundColor: Color(0xFFFFFFFF),
|
||||
textColor: Color(0xFF1F2937),
|
||||
mutedTextColor: Color(0xFF6B7280),
|
||||
bootstrapConfigAsset: 'assets/config/test_bootstrap.json',
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(
|
||||
shellCoreTestHooks.initialUrl,
|
||||
'https://asset.example.com/start',
|
||||
);
|
||||
expect(
|
||||
shellCoreTestHooks.preferredOrientations,
|
||||
<DeviceOrientation>[DeviceOrientation.landscapeRight],
|
||||
);
|
||||
} finally {
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('ShellApp 在非 Android 下展示兜底页', (tester) async {
|
||||
try {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.windows;
|
||||
|
|
@ -664,8 +752,9 @@ void main() {
|
|||
expect(indicator.value, 0.5);
|
||||
});
|
||||
|
||||
testWidgets('LaunchOverlay 始终显示不定态 CircularProgressIndicator',
|
||||
(tester) async {
|
||||
testWidgets('LaunchOverlay 始终显示不定态 CircularProgressIndicator', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: LaunchOverlay(progress: 0, hasMeasuredProgress: false),
|
||||
|
|
@ -1036,6 +1125,26 @@ void main() {
|
|||
expect(_testEnvironment.textColor, const Color(0xFF1F2937));
|
||||
expect(_testEnvironment.mutedTextColor, const Color(0xFF6B7280));
|
||||
expect(_testEnvironment.initialUrl, 'example.com/login');
|
||||
expect(_testEnvironment.bootstrapConfigAsset, isNull);
|
||||
expect(_testEnvironment.bootstrapConfigUrl, isNull);
|
||||
expect(_testEnvironment.upgradeConfigUrl, isNull);
|
||||
expect(_testEnvironment.preferredOrientations, isNull);
|
||||
});
|
||||
|
||||
test('启动配置能解析首页地址与方向', () {
|
||||
final config = shellCoreTestHooks.parseBootstrapConfigString(
|
||||
'{"data":{"initialUrl":"https://example.com/bootstrap","preferredOrientations":["portraitUp","landscapeLeft"]}}',
|
||||
);
|
||||
|
||||
expect(config, isNotNull);
|
||||
expect(config!.initialUrl, 'https://example.com/bootstrap');
|
||||
expect(
|
||||
config.preferredOrientations,
|
||||
<DeviceOrientation>[
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.landscapeLeft,
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('initialUrl 可选字段缺省时使用默认地址', () {
|
||||
|
|
@ -1533,6 +1642,52 @@ void main() {
|
|||
// 由 MaterialApp 自带一些 FadeTransition,检查无多余 Text
|
||||
expect(find.text(''), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('主帧资源错误后 pageFinished 不会清空错误浮层', (tester) async {
|
||||
try {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
|
||||
await tester.pumpWidget(const MaterialApp(home: WebShellPage()));
|
||||
await tester.pump();
|
||||
|
||||
final controller = fakeWebViewPlatform.createdControllers.last;
|
||||
final delegate =
|
||||
controller.delegate! as _FakePlatformNavigationDelegate;
|
||||
const failedUrl = 'https://example.com/login';
|
||||
|
||||
delegate.onPageStarted?.call(failedUrl);
|
||||
await tester.pump();
|
||||
|
||||
delegate.onWebResourceError?.call(
|
||||
const WebResourceError(
|
||||
errorCode: -2,
|
||||
description: 'net::ERR_INTERNET_DISCONNECTED',
|
||||
errorType: WebResourceErrorType.hostLookup,
|
||||
isForMainFrame: true,
|
||||
url: failedUrl,
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('网络连接失败'), findsOneWidget);
|
||||
expect(find.text('没有成功连接到服务器,请检查网络后重试。'), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const ValueKey<String>('fake-webview')),
|
||||
findsNothing,
|
||||
);
|
||||
|
||||
delegate.onPageFinished?.call(failedUrl);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('网络连接失败'), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const ValueKey<String>('fake-webview')),
|
||||
findsNothing,
|
||||
);
|
||||
} finally {
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1628,30 +1783,52 @@ class _FakePlatformWebViewCookieManager extends PlatformWebViewCookieManager {
|
|||
class _FakePlatformNavigationDelegate extends PlatformNavigationDelegate {
|
||||
_FakePlatformNavigationDelegate(super.params) : super.implementation();
|
||||
|
||||
NavigationRequestCallback? onNavigationRequest;
|
||||
PageEventCallback? onPageStarted;
|
||||
PageEventCallback? onPageFinished;
|
||||
HttpResponseErrorCallback? onHttpError;
|
||||
ProgressCallback? onProgress;
|
||||
WebResourceErrorCallback? onWebResourceError;
|
||||
UrlChangeCallback? onUrlChange;
|
||||
|
||||
@override
|
||||
Future<void> setOnNavigationRequest(
|
||||
NavigationRequestCallback onNavigationRequest,
|
||||
) async {}
|
||||
) async {
|
||||
this.onNavigationRequest = onNavigationRequest;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setOnPageStarted(PageEventCallback onPageStarted) async {}
|
||||
Future<void> setOnPageStarted(PageEventCallback onPageStarted) async {
|
||||
this.onPageStarted = onPageStarted;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setOnPageFinished(PageEventCallback onPageFinished) async {}
|
||||
Future<void> setOnPageFinished(PageEventCallback onPageFinished) async {
|
||||
this.onPageFinished = onPageFinished;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setOnHttpError(HttpResponseErrorCallback onHttpError) async {}
|
||||
Future<void> setOnHttpError(HttpResponseErrorCallback onHttpError) async {
|
||||
this.onHttpError = onHttpError;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setOnProgress(ProgressCallback onProgress) async {}
|
||||
Future<void> setOnProgress(ProgressCallback onProgress) async {
|
||||
this.onProgress = onProgress;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setOnWebResourceError(
|
||||
WebResourceErrorCallback onWebResourceError,
|
||||
) async {}
|
||||
) async {
|
||||
this.onWebResourceError = onWebResourceError;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setOnUrlChange(UrlChangeCallback onUrlChange) async {}
|
||||
Future<void> setOnUrlChange(UrlChangeCallback onUrlChange) async {
|
||||
this.onUrlChange = onUrlChange;
|
||||
}
|
||||
}
|
||||
|
||||
class _FakePlatformWebViewController extends PlatformWebViewController {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"initialUrl": "http://192.168.2.57:8080/test_bridge.html",
|
||||
"preferredOrientations": ["portraitUp", "portraitDown"]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"initialUrl": "http://192.168.2.57:8080/test_bridge.html",
|
||||
"versionName": "1.0.0",
|
||||
"version": 100,
|
||||
"isForce": 0,
|
||||
"remark": "1. 修复已知问题\n2. 测试升级弹窗功能是否正常加载\n3. JSON 动态覆盖 initialUrl",
|
||||
"filePath": "https://gitee.com/mr_koi/static_host/raw/master/app-release.apk",
|
||||
"fileSize": 25000
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"versionName": "1.0.0",
|
||||
"version": 100,
|
||||
"isForce": 0,
|
||||
"remark": "1. 修复已知问题\n2. 测试升级弹窗功能是否正常加载",
|
||||
"filePath": "https://gitee.com/mr_koi/static_host/raw/master/app-release.apk",
|
||||
"fileSize": 25000
|
||||
}
|
||||
}
|
||||
|
|
@ -1,86 +1,78 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
/// 品牌名允许的字符:小写字母、数字、下划线。
|
||||
final RegExp _validBrandName = RegExp(r'^[a-z][a-z0-9_]*$');
|
||||
const List<String> _defaultPreferredOrientations = <String>[
|
||||
'portraitUp',
|
||||
'portraitDown',
|
||||
];
|
||||
|
||||
Future<void> main(List<String> args) async {
|
||||
if (args.isEmpty) {
|
||||
print('\x1B[31m用法:dart run tool/generate_app.dart <品牌名>\x1B[0m');
|
||||
print('\x1B[33m示例:dart run tool/generate_app.dart quanxue\x1B[0m');
|
||||
print('\x1B[33m示例:dart run tool/generate_app.dart aixue\x1B[0m');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
final String brand = args.first;
|
||||
|
||||
final brand = args.first;
|
||||
if (!_validBrandName.hasMatch(brand)) {
|
||||
print(
|
||||
'\x1B[31m[错误] 品牌名 "$brand" 格式无效,'
|
||||
'仅允许小写字母、数字和下划线(且必须以字母开头)。\x1B[0m',
|
||||
);
|
||||
print('\x1B[31m[错误] 品牌名 "$brand" 格式无效,仅允许小写字母、数字和下划线。\x1B[0m');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
final File configFile = File('flavors/$brand.yaml');
|
||||
|
||||
final configFile = File('flavors/$brand.yaml');
|
||||
if (!configFile.existsSync()) {
|
||||
print('\x1B[31m[错误] 未找到配置文件:${configFile.path}\x1B[0m');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
print('\x1B[34m[信息] 正在为品牌生成应用:$brand...\x1B[0m');
|
||||
final yamlString = await configFile.readAsString();
|
||||
final config = loadYaml(yamlString) as YamlMap;
|
||||
|
||||
// 1. 解析 YAML 配置
|
||||
final String yamlString = await configFile.readAsString();
|
||||
final YamlMap config = loadYaml(yamlString) as YamlMap;
|
||||
|
||||
final String appName = config['app_name'] as String;
|
||||
final String applicationId = config['application_id'] as String;
|
||||
final String appKey = config['app_key'] as String;
|
||||
|
||||
final YamlMap theme = config['theme'] as YamlMap;
|
||||
final String accentColor = theme['accent_color'] as String;
|
||||
final String bgColor = theme['bg_color'] as String;
|
||||
final String textColor = theme['text_color'] as String;
|
||||
final String mutedTextColor = theme['muted_text_color'] as String;
|
||||
|
||||
final String appDir = 'apps/$brand';
|
||||
|
||||
print('\x1B[32m✔ 配置加载完成。\x1B[0m');
|
||||
|
||||
// 2. 创建 Flutter 应用
|
||||
await _createFlutterApp(brand, appDir, applicationId);
|
||||
|
||||
// 3. 添加核心依赖
|
||||
await _addCoreDependency(appDir);
|
||||
|
||||
// 4. 覆盖 MainActivity.java 以继承 CoreShellActivity
|
||||
await _overwriteMainActivity(appDir, applicationId);
|
||||
|
||||
// 5. 覆盖 AndroidManifest.xml 中的应用名称
|
||||
await _overwriteManifestLabel(appDir, appName);
|
||||
|
||||
final String? defaultUrl = config['default_url'] as String?;
|
||||
|
||||
// 6. 生成 lib/main.dart
|
||||
await _generateDartEntrypoint(
|
||||
appDir,
|
||||
appName,
|
||||
appKey,
|
||||
accentColor,
|
||||
bgColor,
|
||||
textColor,
|
||||
mutedTextColor,
|
||||
defaultUrl,
|
||||
final appName = config['app_name'] as String;
|
||||
final applicationId = config['application_id'] as String;
|
||||
final appKey = config['app_key'] as String;
|
||||
final defaultUrl = config['default_url'] as String?;
|
||||
final bootstrapConfigUrl = config['bootstrap_config_url'] as String?;
|
||||
final upgradeConfigUrl = config['upgrade_config_url'] as String?;
|
||||
final preferredOrientations = _readPreferredOrientations(
|
||||
config['preferred_orientations'],
|
||||
);
|
||||
|
||||
// 7. 生成图标与启动页配置
|
||||
final theme = config['theme'] as YamlMap;
|
||||
final accentColor = theme['accent_color'] as String;
|
||||
final bgColor = theme['bg_color'] as String;
|
||||
final textColor = theme['text_color'] as String;
|
||||
final mutedTextColor = theme['muted_text_color'] as String;
|
||||
|
||||
final appDir = 'apps/$brand';
|
||||
|
||||
await _createFlutterApp(brand, appDir, applicationId);
|
||||
await _addCoreDependency(appDir);
|
||||
await _rewriteAndroidPackageId(appDir, applicationId);
|
||||
await _overwriteMainActivity(appDir, applicationId);
|
||||
await _overwriteManifestLabel(appDir, appName);
|
||||
await _generateDartEntrypoint(
|
||||
appDir: appDir,
|
||||
appName: appName,
|
||||
appKey: appKey,
|
||||
accentColor: accentColor,
|
||||
backgroundColor: bgColor,
|
||||
textColor: textColor,
|
||||
mutedTextColor: mutedTextColor,
|
||||
bootstrapConfigUrl: bootstrapConfigUrl,
|
||||
upgradeConfigUrl: upgradeConfigUrl,
|
||||
);
|
||||
await _generateBootstrapConfig(
|
||||
appDir: appDir,
|
||||
defaultUrl: defaultUrl,
|
||||
preferredOrientations: preferredOrientations,
|
||||
);
|
||||
await _generateBrandingAssets(brand, appDir, config);
|
||||
|
||||
// 8. 在 pubspec.yaml 中注册 Flutter assets
|
||||
await _registerFlutterAssets(appDir);
|
||||
|
||||
// 9. 配置签名 (KeyStore)
|
||||
await _configureSigning(appDir, config);
|
||||
|
||||
print('\x1B[32m✔ 应用 $brand 已生成到 $appDir!\x1B[0m');
|
||||
|
|
@ -88,29 +80,55 @@ Future<void> main(List<String> args) async {
|
|||
print(' cd $appDir && flutter build apk');
|
||||
}
|
||||
|
||||
List<String> _readPreferredOrientations(Object? raw) {
|
||||
if (raw is! YamlList && raw is! List) {
|
||||
return _defaultPreferredOrientations;
|
||||
}
|
||||
|
||||
final values = <String>[];
|
||||
for (final item in (raw as List)) {
|
||||
final normalized = _normalizeOrientation(item?.toString());
|
||||
if (normalized != null && !values.contains(normalized)) {
|
||||
values.add(normalized);
|
||||
}
|
||||
}
|
||||
return values.isEmpty ? _defaultPreferredOrientations : values;
|
||||
}
|
||||
|
||||
String? _normalizeOrientation(String? raw) {
|
||||
final value = raw?.trim();
|
||||
if (value == null || value.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return switch (value) {
|
||||
'portraitUp' || 'DeviceOrientation.portraitUp' => 'portraitUp',
|
||||
'portraitDown' || 'DeviceOrientation.portraitDown' => 'portraitDown',
|
||||
'landscapeLeft' || 'DeviceOrientation.landscapeLeft' => 'landscapeLeft',
|
||||
'landscapeRight' || 'DeviceOrientation.landscapeRight' => 'landscapeRight',
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
Future<void> _createFlutterApp(
|
||||
String brand,
|
||||
String appDir,
|
||||
String applicationId,
|
||||
) async {
|
||||
print('\x1B[34m[信息] 正在执行 flutter create...\x1B[0m');
|
||||
final Directory dir = Directory(appDir);
|
||||
final dir = Directory(appDir);
|
||||
if (dir.existsSync()) {
|
||||
print('\x1B[33m[警告] 目录 $appDir 已存在,正在清理...\x1B[0m');
|
||||
dir.deleteSync(recursive: true);
|
||||
}
|
||||
|
||||
// 提取组织名
|
||||
// 例如:com.yuanxuan.quanxue -> org: com.yuanxuan
|
||||
final List<String> segments = applicationId.split('.');
|
||||
final String org = segments.sublist(0, segments.length - 1).join('.');
|
||||
|
||||
final ProcessResult result = await Process.run('flutter', [
|
||||
final segments = applicationId.split('.');
|
||||
final org = segments.sublist(0, segments.length - 1).join('.');
|
||||
final result = await Process.run('flutter', <String>[
|
||||
'create',
|
||||
'--org',
|
||||
org,
|
||||
'--project-name',
|
||||
brand.replaceAll('-', '_'),
|
||||
brand,
|
||||
'--platforms',
|
||||
'android',
|
||||
'--android-language',
|
||||
|
|
@ -126,7 +144,7 @@ Future<void> _createFlutterApp(
|
|||
|
||||
Future<void> _addCoreDependency(String appDir) async {
|
||||
print('\x1B[34m[信息] 正在添加 web_shell_core 依赖...\x1B[0m');
|
||||
final ProcessResult result = await Process.run('flutter', [
|
||||
final result = await Process.run('flutter', <String>[
|
||||
'pub',
|
||||
'add',
|
||||
'web_shell_core',
|
||||
|
|
@ -140,13 +158,40 @@ Future<void> _addCoreDependency(String appDir) async {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _rewriteAndroidPackageId(
|
||||
String appDir,
|
||||
String applicationId,
|
||||
) async {
|
||||
print('\x1B[34m[信息] 正在校正 Android applicationId / namespace...\x1B[0m');
|
||||
final gradleFile = File('$appDir/android/app/build.gradle.kts');
|
||||
if (!gradleFile.existsSync()) {
|
||||
print('\x1B[31m[错误] 未找到 ${gradleFile.path}\x1B[0m');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
var content = await gradleFile.readAsString();
|
||||
content = content.replaceFirst(
|
||||
RegExp(r'namespace\s*=\s*"[^"]+"'),
|
||||
'namespace = "$applicationId"',
|
||||
);
|
||||
content = content.replaceFirst(
|
||||
RegExp(r'applicationId\s*=\s*"[^"]+"'),
|
||||
'applicationId = "$applicationId"',
|
||||
);
|
||||
await gradleFile.writeAsString(content);
|
||||
|
||||
final javaDir = Directory('$appDir/android/app/src/main/java');
|
||||
if (javaDir.existsSync()) {
|
||||
javaDir.deleteSync(recursive: true);
|
||||
}
|
||||
await javaDir.create(recursive: true);
|
||||
}
|
||||
|
||||
Future<void> _overwriteMainActivity(String appDir, String applicationId) async {
|
||||
print('\x1B[34m[信息] 正在注入 CoreShellActivity 继承关系...\x1B[0m');
|
||||
final String mainActivityPath =
|
||||
"$appDir/android/app/src/main/java/${applicationId.replaceAll('.', '/')}/MainActivity.java";
|
||||
|
||||
// 准备 Java 文件内容
|
||||
final String javaContent =
|
||||
final mainActivityPath =
|
||||
'$appDir/android/app/src/main/java/${applicationId.replaceAll('.', '/')}/MainActivity.java';
|
||||
final javaContent =
|
||||
'''
|
||||
package $applicationId;
|
||||
|
||||
|
|
@ -156,42 +201,45 @@ public class MainActivity extends CoreShellActivity {
|
|||
}
|
||||
''';
|
||||
|
||||
await File(mainActivityPath).create(recursive: true);
|
||||
await File(mainActivityPath).writeAsString(javaContent);
|
||||
final mainActivityFile = File(mainActivityPath);
|
||||
await mainActivityFile.parent.create(recursive: true);
|
||||
await mainActivityFile.writeAsString(javaContent);
|
||||
}
|
||||
|
||||
Future<void> _overwriteManifestLabel(String appDir, String appName) async {
|
||||
print('\x1B[34m[信息] 正在更新 AndroidManifest.xml 应用名...\x1B[0m');
|
||||
final File manifestFile = File(
|
||||
'$appDir/android/app/src/main/AndroidManifest.xml',
|
||||
);
|
||||
String content = await manifestFile.readAsString();
|
||||
|
||||
// 使用简单正则替换 android:label="..."
|
||||
final manifestFile = File('$appDir/android/app/src/main/AndroidManifest.xml');
|
||||
var content = await manifestFile.readAsString();
|
||||
content = content.replaceAll(
|
||||
RegExp(r'android:label="[^"]*"'),
|
||||
'android:label="$appName"',
|
||||
);
|
||||
|
||||
await manifestFile.writeAsString(content);
|
||||
}
|
||||
|
||||
Future<void> _generateDartEntrypoint(
|
||||
String appDir,
|
||||
String appName,
|
||||
String appKey,
|
||||
String accentColor,
|
||||
String bgColor,
|
||||
String textColor,
|
||||
String mutedTextColor,
|
||||
String? defaultUrl,
|
||||
) async {
|
||||
Future<void> _generateDartEntrypoint({
|
||||
required String appDir,
|
||||
required String appName,
|
||||
required String appKey,
|
||||
required String accentColor,
|
||||
required String backgroundColor,
|
||||
required String textColor,
|
||||
required String mutedTextColor,
|
||||
String? bootstrapConfigUrl,
|
||||
String? upgradeConfigUrl,
|
||||
}) async {
|
||||
print('\x1B[34m[信息] 正在生成 lib/main.dart...\x1B[0m');
|
||||
final File mainFile = File('$appDir/lib/main.dart');
|
||||
final mainFile = File('$appDir/lib/main.dart');
|
||||
|
||||
final String urlParam = defaultUrl != null ? "initialUrl: '$defaultUrl'," : "";
|
||||
final extraLines = <String>[
|
||||
" bootstrapConfigAsset: 'assets/config/bootstrap.json',",
|
||||
if (bootstrapConfigUrl != null && bootstrapConfigUrl.trim().isNotEmpty)
|
||||
" bootstrapConfigUrl: '${bootstrapConfigUrl.trim()}',",
|
||||
if (upgradeConfigUrl != null && upgradeConfigUrl.trim().isNotEmpty)
|
||||
" upgradeConfigUrl: '${upgradeConfigUrl.trim()}',",
|
||||
].join('\n');
|
||||
|
||||
final String dartContent =
|
||||
final dartContent =
|
||||
'''
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:web_shell_core/web_shell_core.dart';
|
||||
|
|
@ -202,11 +250,11 @@ void main() {
|
|||
appName: '$appName',
|
||||
appKey: '$appKey',
|
||||
accentColor: const Color($accentColor),
|
||||
backgroundColor: const Color($bgColor),
|
||||
backgroundColor: const Color($backgroundColor),
|
||||
textColor: const Color($textColor),
|
||||
mutedTextColor: const Color($mutedTextColor),
|
||||
splashImage: const AssetImage('assets/branding/splash.png'),
|
||||
$urlParam
|
||||
$extraLines
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -214,13 +262,31 @@ void main() {
|
|||
|
||||
await mainFile.writeAsString(dartContent);
|
||||
|
||||
// 清理 flutter create 默认生成的测试文件(因为它依赖已删除的 MyApp 类)
|
||||
final File defaultTestFile = File('$appDir/test/widget_test.dart');
|
||||
final defaultTestFile = File('$appDir/test/widget_test.dart');
|
||||
if (defaultTestFile.existsSync()) {
|
||||
await defaultTestFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _generateBootstrapConfig({
|
||||
required String appDir,
|
||||
required String? defaultUrl,
|
||||
required List<String> preferredOrientations,
|
||||
}) async {
|
||||
print('\x1B[34m[信息] 正在生成默认启动配置 assets/config/bootstrap.json...\x1B[0m');
|
||||
final file = File('$appDir/assets/config/bootstrap.json');
|
||||
await file.parent.create(recursive: true);
|
||||
|
||||
final data = <String, Object?>{
|
||||
if (defaultUrl != null && defaultUrl.trim().isNotEmpty)
|
||||
'initialUrl': defaultUrl.trim(),
|
||||
'preferredOrientations': preferredOrientations,
|
||||
};
|
||||
|
||||
final content = const JsonEncoder.withIndent(' ').convert(data);
|
||||
await file.writeAsString('$content\n');
|
||||
}
|
||||
|
||||
Future<void> _generateBrandingAssets(
|
||||
String brand,
|
||||
String appDir,
|
||||
|
|
@ -231,62 +297,50 @@ Future<void> _generateBrandingAssets(
|
|||
print('\x1B[33m[警告] 配置中未找到 branding 段,跳过资源生成。\x1B[0m');
|
||||
return;
|
||||
}
|
||||
final YamlMap branding = config['branding'] as YamlMap;
|
||||
|
||||
// ── 1. 复制品牌资源到生成的应用目录 ──
|
||||
final Directory brandSourceDir = Directory('flavors/$brand');
|
||||
final Directory brandTargetDir = Directory('$appDir/assets/branding');
|
||||
final branding = config['branding'] as YamlMap;
|
||||
final brandSourceDir = Directory('flavors/$brand');
|
||||
final brandTargetDir = Directory('$appDir/assets/branding');
|
||||
if (!brandSourceDir.existsSync()) {
|
||||
print('\x1B[31m[错误] 品牌资源目录不存在:${brandSourceDir.path}\x1B[0m');
|
||||
print(
|
||||
'\x1B[33m请在 flavors/$brand/ 目录下放置 icon.png、'
|
||||
'icon_foreground.png、splash.png 等资源文件。\x1B[0m',
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
await brandTargetDir.create(recursive: true);
|
||||
for (final entity in brandSourceDir.listSync(recursive: true)) {
|
||||
if (entity is File) {
|
||||
final relativePath = entity.path.substring(
|
||||
brandSourceDir.path.length + 1,
|
||||
);
|
||||
final targetFile = File('${brandTargetDir.path}/$relativePath');
|
||||
await targetFile.parent.create(recursive: true);
|
||||
await entity.copy(targetFile.path);
|
||||
if (entity is! File) {
|
||||
continue;
|
||||
}
|
||||
final relativePath = entity.path.substring(brandSourceDir.path.length + 1);
|
||||
final targetFile = File('${brandTargetDir.path}/$relativePath');
|
||||
await targetFile.parent.create(recursive: true);
|
||||
await entity.copy(targetFile.path);
|
||||
}
|
||||
|
||||
print('\x1B[32m✔ 品牌资源已复制到 ${brandTargetDir.path}\x1B[0m');
|
||||
|
||||
// ── 2. 添加资源生成器的 dev 依赖 ──
|
||||
print('\x1B[34m[信息] 正在添加资源生成器依赖...\x1B[0m');
|
||||
final ProcessResult addDevDepsResult = await Process.run('flutter', [
|
||||
final addDevDepsResult = await Process.run('flutter', <String>[
|
||||
'pub',
|
||||
'add',
|
||||
'--dev',
|
||||
'flutter_launcher_icons',
|
||||
'flutter_native_splash',
|
||||
], workingDirectory: appDir);
|
||||
|
||||
if (addDevDepsResult.exitCode != 0) {
|
||||
print(
|
||||
'\x1B[31m[错误] 添加资源生成器依赖失败:'
|
||||
'\n${addDevDepsResult.stderr}\x1B[0m',
|
||||
);
|
||||
print('\x1B[31m[错误] 添加资源生成器依赖失败:\n${addDevDepsResult.stderr}\x1B[0m');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// ── 3. 确保依赖已解析 ──
|
||||
await Process.run('flutter', ['pub', 'get'], workingDirectory: appDir);
|
||||
await Process.run('flutter', <String>[
|
||||
'pub',
|
||||
'get',
|
||||
], workingDirectory: appDir);
|
||||
|
||||
// ── 4. 生成 flutter_launcher_icons 配置 ──
|
||||
// 资源路径指向复制后的位置(相对于 appDir)
|
||||
final String iconPath = 'assets/branding/${branding['icon']}';
|
||||
final String iconForeground =
|
||||
'assets/branding/${branding['icon_foreground']}';
|
||||
final String iconBackground = branding['icon_background'] as String;
|
||||
|
||||
final String iconsYaml =
|
||||
final iconPath = 'assets/branding/${branding['icon']}';
|
||||
final iconForeground = 'assets/branding/${branding['icon_foreground']}';
|
||||
final iconBackground = branding['icon_background'] as String;
|
||||
final iconsYaml =
|
||||
'''
|
||||
flutter_launcher_icons:
|
||||
android: true
|
||||
|
|
@ -296,11 +350,9 @@ flutter_launcher_icons:
|
|||
''';
|
||||
await File('$appDir/flutter_launcher_icons.yaml').writeAsString(iconsYaml);
|
||||
|
||||
// ── 4. 生成 flutter_native_splash 配置 ──
|
||||
final String splashPath = 'assets/branding/${branding['splash']}';
|
||||
final String splashColor = branding['splash_color'] as String;
|
||||
|
||||
final String splashYaml =
|
||||
final splashPath = 'assets/branding/${branding['splash']}';
|
||||
final splashColor = branding['splash_color'] as String;
|
||||
final splashYaml =
|
||||
'''
|
||||
flutter_native_splash:
|
||||
color: "$splashColor"
|
||||
|
|
@ -311,39 +363,33 @@ flutter_native_splash:
|
|||
''';
|
||||
await File('$appDir/flutter_native_splash.yaml').writeAsString(splashYaml);
|
||||
|
||||
// ── 5. 执行资源生成器 ──
|
||||
print('\x1B[34m[信息] 正在生成应用图标...\x1B[0m');
|
||||
final ProcessResult iconsResult = await Process.run('dart', [
|
||||
final iconsResult = await Process.run('dart', <String>[
|
||||
'run',
|
||||
'flutter_launcher_icons',
|
||||
'-f',
|
||||
'flutter_launcher_icons.yaml',
|
||||
], workingDirectory: appDir);
|
||||
|
||||
if (iconsResult.exitCode != 0) {
|
||||
print('\x1B[31m[错误] 图标生成失败:\n${iconsResult.stderr}\x1B[0m');
|
||||
print('\x1B[33mstdout:\n${iconsResult.stdout}\x1B[0m');
|
||||
exit(1);
|
||||
}
|
||||
print('\x1B[32m✔ 应用图标已生成。\x1B[0m');
|
||||
|
||||
print('\x1B[34m[信息] 正在生成启动页...\x1B[0m');
|
||||
final ProcessResult splashResult = await Process.run('dart', [
|
||||
final splashResult = await Process.run('dart', <String>[
|
||||
'run',
|
||||
'flutter_native_splash:create',
|
||||
'--path=flutter_native_splash.yaml',
|
||||
], workingDirectory: appDir);
|
||||
|
||||
if (splashResult.exitCode != 0) {
|
||||
print('\x1B[31m[错误] 启动页生成失败:\n${splashResult.stderr}\x1B[0m');
|
||||
print('\x1B[33mstdout:\n${splashResult.stdout}\x1B[0m');
|
||||
exit(1);
|
||||
}
|
||||
print('\x1B[32m✔ 启动页已生成。\x1B[0m');
|
||||
|
||||
// ── 6. 移除资源生成器依赖以避免干扰 release 打包 ──
|
||||
print('\x1B[34m[信息] 正在移除资源生成器依赖...\x1B[0m');
|
||||
await Process.run('flutter', [
|
||||
await Process.run('flutter', <String>[
|
||||
'pub',
|
||||
'remove',
|
||||
'flutter_launcher_icons',
|
||||
|
|
@ -353,38 +399,43 @@ flutter_native_splash:
|
|||
|
||||
Future<void> _registerFlutterAssets(String appDir) async {
|
||||
print('\x1B[34m[信息] 正在注册 Flutter assets...\x1B[0m');
|
||||
final File pubspecFile = File('$appDir/pubspec.yaml');
|
||||
String content = await pubspecFile.readAsString();
|
||||
final pubspecFile = File('$appDir/pubspec.yaml');
|
||||
var content = await pubspecFile.readAsString();
|
||||
|
||||
// 检查是否已有未注释的 assets: 声明
|
||||
final bool hasAssets = RegExp(r'^\s+assets:', multiLine: true).hasMatch(content);
|
||||
if (!hasAssets) {
|
||||
// 必须匹配行首的顶层 flutter: 键,忽略缩进的 flutter: (SDK 依赖)
|
||||
if (!RegExp(r'^\s+assets:\s*$', multiLine: true).hasMatch(content)) {
|
||||
content = content.replaceFirst(
|
||||
RegExp(r'^flutter:\s*\n', multiLine: true),
|
||||
'flutter:\n assets:\n - assets/branding/\n',
|
||||
'flutter:\n assets:\n - assets/branding/\n - assets/config/\n',
|
||||
);
|
||||
await pubspecFile.writeAsString(content);
|
||||
} else {
|
||||
if (!content.contains(' - assets/branding/')) {
|
||||
content = content.replaceFirst(
|
||||
RegExp(r'^\s+assets:\s*\n', multiLine: true),
|
||||
' assets:\n - assets/branding/\n',
|
||||
);
|
||||
}
|
||||
if (!content.contains(' - assets/config/')) {
|
||||
content = content.replaceFirst(
|
||||
' - assets/branding/\n',
|
||||
' - assets/branding/\n - assets/config/\n',
|
||||
);
|
||||
}
|
||||
}
|
||||
print('\x1B[32m✔ Flutter assets 已注册。\x1B[0m');
|
||||
|
||||
await pubspecFile.writeAsString(content);
|
||||
}
|
||||
|
||||
Future<void> _configureSigning(String appDir, YamlMap config) async {
|
||||
print('\x1B[34m[信息] 正在配置应用签名...\x1B[0m');
|
||||
final signing = config['signing'] as YamlMap?;
|
||||
final keyAlias = signing?['key_alias'] as String? ?? 'my-key-alias';
|
||||
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';
|
||||
|
||||
// 提取配置,默认为工具根目录下的 key.jks
|
||||
final YamlMap? signing = config['signing'] as YamlMap?;
|
||||
final String keyAlias = signing?['key_alias'] as String? ?? 'my-key-alias';
|
||||
final String keyPassword = signing?['key_password'] as String? ?? '123456';
|
||||
final String storePassword = signing?['store_password'] as String? ?? '123456';
|
||||
|
||||
// key.properties 会放在 apps/<brand>/android/ 目录下
|
||||
// 但它被 app/build.gradle.kts 读取和求值,所以在 build.gradle.kts 中它的相对基准是 android/app/
|
||||
// 指向 tool/key.jks 的相对路径是: ../../../../tool/key.jks
|
||||
final String storeFile = signing?['store_file'] as String? ?? '../../../../tool/key.jks';
|
||||
|
||||
// 1. 写入 android/key.properties
|
||||
final String keyPropsContent = '''
|
||||
final keyPropsContent =
|
||||
'''
|
||||
storePassword=$storePassword
|
||||
keyPassword=$keyPassword
|
||||
keyAlias=$keyAlias
|
||||
|
|
@ -392,16 +443,13 @@ storeFile=$storeFile
|
|||
''';
|
||||
await File('$appDir/android/key.properties').writeAsString(keyPropsContent);
|
||||
|
||||
// 2. 修改 build.gradle.kts
|
||||
final File gradleFile = File('$appDir/android/app/build.gradle.kts');
|
||||
final gradleFile = File('$appDir/android/app/build.gradle.kts');
|
||||
if (!gradleFile.existsSync()) {
|
||||
print('\x1B[33m[警告] 未找到 build.gradle.kts,跳过注入签名配置。\x1B[0m');
|
||||
return;
|
||||
}
|
||||
|
||||
String content = await gradleFile.readAsString();
|
||||
|
||||
// 注入 Properties 加载代码
|
||||
var content = await gradleFile.readAsString();
|
||||
if (!content.contains('val keystoreProperties = Properties()')) {
|
||||
content = content.replaceFirst('android {', '''
|
||||
import java.util.Properties
|
||||
|
|
@ -417,9 +465,8 @@ android {
|
|||
''');
|
||||
}
|
||||
|
||||
// 注入 signingConfigs 并修改 buildTypes.release
|
||||
if (!content.contains('signingConfigs {\\n create("release") {')) {
|
||||
final String newBuildTypes = '''
|
||||
if (!content.contains('signingConfigs {\n create("release") {')) {
|
||||
const newBuildTypes = '''
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
keyAlias = keystoreProperties["keyAlias"] as String?
|
||||
|
|
@ -435,12 +482,11 @@ android {
|
|||
}
|
||||
}
|
||||
''';
|
||||
|
||||
// 匹配原始的 buildTypes { release { ... } } 块
|
||||
final RegExp buildTypeRegex = RegExp(r'\s*buildTypes\s*\{[\s\S]*?signingConfigs\.getByName\("debug"\)\s*\}\s*\}');
|
||||
final buildTypeRegex = RegExp(
|
||||
r'\s*buildTypes\s*\{[\s\S]*?signingConfigs\.getByName\("debug"\)\s*\}\s*\}',
|
||||
);
|
||||
content = content.replaceFirst(buildTypeRegex, '\n$newBuildTypes');
|
||||
}
|
||||
|
||||
await gradleFile.writeAsString(content);
|
||||
print('\x1B[32m✔ 签名配置注入完成。\x1B[0m');
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Reference in New Issue