添加版本更新的头部信息 区域识别支持

This commit is contained in:
DESKTOP-I3JPKHK\wy 2026-04-27 19:31:27 +08:00
parent e6e7f8e951
commit 11a42dd186
10 changed files with 1017 additions and 133 deletions

View File

@ -132,12 +132,109 @@ AppUpgradeSimple.instance.configure(const UpgradeConfig(
- **代码块**`` `version 2.0` `` - **代码块**`` `version 2.0` ``
- **高亮**`[特别注意]` - **高亮**`[特别注意]`
支持嵌套组合使用,例如:
- `**[特别注意]**`
- `__**重要说明**__`
- `请使用 \`version 2.0\` 进行灰度验证`
```text ```text
1. 新增 **深色模式** 支持 1. 新增 **深色模式** 支持
2. 修复 `Login` 页面崩溃问题 2. 修复 `Login` 页面崩溃问题
3. [推荐] 性能大幅优化 3. [推荐] 性能大幅优化
``` ```
### 头部说明块 `/{ ... /}`
从当前版本开始,`updateContent` 支持一个可选的头部说明区域,用于展示“本次版本概述 / 重点提示 / 总结说明”等内容。
头部块语法:
- 起始标记:`/{`
- 结束标记:`/}`
- 头部块是可选的
- 头部块可以为空
- 头部块内继续支持现有富文本标记与嵌套组合
#### 多行写法
```text
/{
本次 **V2.2.0** 版本围绕[使用体验]与[学习管理]进行了多项优化。
__请优先关注__ 工作录入与成长中心改动。
/}
1. 新增草稿箱功能
2. 优化多账号登录机制
```
#### 单行写法
```text
/{ 本次版本重点优化登录体验与录入效率 /}
1. 新增草稿箱功能
2. 修复已知问题
```
#### 空头部块写法
```text
/{ /}
1. 第一条更新说明
2. 第二条更新说明
```
#### 头部块中的嵌套示例
```text
/{
**重要内容**
__[特别注意]__
`version 2.0`
普通说明与 **[高亮重点]** 混合展示
/}
```
### 推荐的 `updateContent` 写法
建议服务端直接返回结构清晰的字符串,头部概述和正文分开:
```text
/{
本次 **V2.2.0** 版本围绕[使用体验]、[账号安全]、[家校联动]、[学习管理]等方面进行多项优化与功能新增,具体更新内容如下:
/}
1. 工作录入新增**草稿箱功能**,录入内容自动暂存;
2. 优化**多账号登录机制**,同一手机号绑定多个账号时可手动选择;
3. 全新上线**家校互动板块**
4. **成长中心**全面调整优化。
```
### 解析规则说明
- 只有当 `updateContent` 的第一段内容以 `/{` 开始时,才会被识别为头部说明块
- 头部说明块中的内容会单独显示在正文上方
- 头部中的 `** / __ / \` / []` 标记会继续走现有富文本渲染逻辑
- 如果正文行本身已经包含 `1.`、`一、`、`-`、`•` 等前缀,插件不会再额外补一个圆点,避免双重列表样式
- 如果缺少闭合标记 `/}`,插件会回退为普通正文处理,不会抛出异常
### 调试与预解析
如果你希望在展示前先检查解析结果,可以使用:
```dart
final parsed = AppUpgradeSimple.parseUpdateContent(updateContent);
debugPrint('hasExplicitHeaderBlock: ${parsed.hasExplicitHeaderBlock}');
debugPrint('header: ${parsed.header}');
debugPrint('bodyItems: ${parsed.bodyItems.map((e) => e.text).toList()}');
```
返回结果说明:
- `parsed.header`:头部说明内容
- `parsed.hasExplicitHeaderBlock`:是否显式使用了 `/{ ... /}` 头部块
- `parsed.bodyItems`:正文条目列表
- `parsed.bodyItems[i].hasLeadingMarker`:该条正文是否已经自带列表前缀
### 对话框特性 ### 对话框特性
- **发现新版本**(强制/非强制) - **发现新版本**(强制/非强制)

View File

@ -9,7 +9,7 @@ buildscript {
} }
dependencies { dependencies {
classpath("com.android.tools.build:gradle:8.7.3") classpath("com.android.tools.build:gradle:8.9.1")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
} }
} }

3
example/.gitignore vendored
View File

@ -43,3 +43,6 @@ app.*.map.json
/android/app/debug /android/app/debug
/android/app/profile /android/app/profile
/android/app/release /android/app/release
# FVM Version Cache
.fvm/

3
example/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"dart.flutterSdkPath": ".fvm/versions/3.35.5"
}

View File

@ -18,7 +18,7 @@ pluginManagement {
plugins { plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.3" apply false id("com.android.application") version "8.9.1" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false id("org.jetbrains.kotlin.android") version "2.1.0" apply false
} }

View File

@ -67,13 +67,13 @@ class _HomePageState extends State<HomePage> {
context: context, context: context,
future: () async { future: () async {
final updateAppEvent = await _getUpdateAppEvent(); final updateAppEvent = await _getUpdateAppEvent();
print("获取最新版本: $updateAppEvent"); debugPrint("获取最新版本: $updateAppEvent");
if (updateAppEvent == null) return null; if (updateAppEvent == null) return null;
return _convertToAppUpgradeVersion(updateAppEvent); return _convertToAppUpgradeVersion(updateAppEvent);
}, },
onComplete: (bool val) { onComplete: () {
print("更新插件执行完成....: $val"); debugPrint("更新插件执行完成....");
}, },
config: UpgradeConfig.development, config: UpgradeConfig.development,
// showNoUpdateToast: false, // showNoUpdateToast: false,
@ -99,10 +99,14 @@ class _HomePageState extends State<HomePage> {
"id": 708608950206533, "id": 708608950206533,
"version": 307, "version": 307,
"versionName": "1.0.9", "versionName": "1.0.9",
"remark": """ "remark": """班级活动 / 文创更新,支持**多选班级**
****``\n
****__级可集中查看下级``__\n ****
****__在[]__\n""",
1 1
/ """,
"imageBase": null, "imageBase": null,
"updatetype": 1, "updatetype": 1,
"isActive": 1, "isActive": 1,
@ -158,7 +162,7 @@ class _HomePageState extends State<HomePage> {
final upgradeInfo = await AppUpgradeSimple.instance.silentCheckUpdate( final upgradeInfo = await AppUpgradeSimple.instance.silentCheckUpdate(
future: () async { future: () async {
final updateAppEvent = await _getUpdateAppEvent(); final updateAppEvent = await _getUpdateAppEvent();
print("获取最新版本: $updateAppEvent"); debugPrint("获取最新版本: $updateAppEvent");
if (updateAppEvent == null) return null; if (updateAppEvent == null) return null;
return _convertToAppUpgradeVersion(updateAppEvent); return _convertToAppUpgradeVersion(updateAppEvent);
@ -167,7 +171,9 @@ class _HomePageState extends State<HomePage> {
// 2. // 2.
if (upgradeInfo != null && upgradeInfo.hasUpdate) { if (upgradeInfo != null && upgradeInfo.hasUpdate) {
// //
print('发现新版本: ${upgradeInfo.versionName}'); debugPrint('发现新版本: ${upgradeInfo.versionName}');
if (!mounted) return;
// //
AppUpgradeSimple.instance.showPreparedUpgrade( AppUpgradeSimple.instance.showPreparedUpgrade(

View File

@ -5,10 +5,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: async name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.13.0" version: "2.13.1"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@ -45,26 +45,26 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: cupertino_icons name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.0.8" version: "1.0.9"
dio: dio:
dependency: transitive dependency: transitive
description: description:
name: dio name: dio
sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "5.9.0" version: "5.9.2"
dio_web_adapter: dio_web_adapter:
dependency: transitive dependency: transitive
description: description:
name: dio_web_adapter name: dio_web_adapter
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.1" version: "2.1.2"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -77,10 +77,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: ffi name: ffi
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.4" version: "2.2.0"
file: file:
dependency: transitive dependency: transitive
description: description:
@ -224,10 +224,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: package_info_plus name: package_info_plus
sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "9.0.0" version: "9.0.1"
package_info_plus_platform_interface: package_info_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -256,10 +256,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path_provider_android name: path_provider_android
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e sha256: "149441ca6e4f38193b2e004c0ca6376a3d11f51fa5a77552d8bd4d2b0c0912ba"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.2.22" version: "2.2.23"
path_provider_foundation: path_provider_foundation:
dependency: transitive dependency: transitive
description: description:
@ -373,10 +373,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: source_span name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.10.1" version: "1.10.2"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -445,10 +445,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_android name: url_launcher_android
sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "6.3.28" version: "6.3.29"
url_launcher_ios: url_launcher_ios:
dependency: transitive dependency: transitive
description: description:
@ -509,10 +509,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" sha256: "046d3928e16fa4dc46e8350415661755ab759d9fc97fc21b5ab295f71e4f0499"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "15.0.2" version: "15.1.0"
web: web:
dependency: transitive dependency: transitive
description: description:
@ -551,7 +551,7 @@ packages:
path: ".." path: ".."
relative: true relative: true
source: path source: path
version: "1.0.0" version: "1.0.5"
sdks: sdks:
dart: ">=3.9.0 <4.0.0" dart: ">=3.9.0 <4.0.0"
flutter: ">=3.35.0" flutter: ">=3.35.0"

View File

@ -23,8 +23,10 @@ class _SimpleAppUpgradePlugin {
return AppUpgradePluginPlatform.instance.checkUpdate(url, params: params); return AppUpgradePluginPlatform.instance.checkUpdate(url, params: params);
} }
Future<String?> downloadApk(String url, {Function(DownloadProgress)? onProgress}) { Future<String?> downloadApk(String url,
return AppUpgradePluginPlatform.instance.downloadApk(url, onProgress: onProgress); {Function(DownloadProgress)? onProgress}) {
return AppUpgradePluginPlatform.instance
.downloadApk(url, onProgress: onProgress);
} }
Future<bool> installApk(String filePath) { Future<bool> installApk(String filePath) {
@ -32,7 +34,8 @@ class _SimpleAppUpgradePlugin {
} }
Future<bool> goToAppStore(String url, {required BuildContext context}) { Future<bool> goToAppStore(String url, {required BuildContext context}) {
return AppUpgradePluginPlatform.instance.goToAppStore(url, context: context); return AppUpgradePluginPlatform.instance
.goToAppStore(url, context: context);
} }
Future<Map<String, String>> getAppInfo() { Future<Map<String, String>> getAppInfo() {
@ -110,6 +113,32 @@ class UpgradeConfig {
); );
} }
class ParsedUpgradeContent {
const ParsedUpgradeContent({
this.header,
this.hasExplicitHeaderBlock = false,
this.bodyItems = const <ParsedUpgradeContentItem>[],
});
final String? header;
final bool hasExplicitHeaderBlock;
final List<ParsedUpgradeContentItem> bodyItems;
bool get hasHeader => header?.isNotEmpty ?? false;
bool get hasBodyItems => bodyItems.isNotEmpty;
}
class ParsedUpgradeContentItem {
const ParsedUpgradeContentItem({
required this.text,
this.hasLeadingMarker = false,
});
final String text;
final bool hasLeadingMarker;
}
/// App升级管理器 /// App升级管理器
/// APIApp升级功能 /// APIApp升级功能
class AppUpgradeSimple { class AppUpgradeSimple {
@ -127,7 +156,8 @@ class AppUpgradeSimple {
} }
@visibleForTesting @visibleForTesting
AppUpgradeSimple.private({_SimpleAppUpgradePlugin? plugin}) : _plugin = plugin ?? _SimpleAppUpgradePlugin.instance; AppUpgradeSimple.private({_SimpleAppUpgradePlugin? plugin})
: _plugin = plugin ?? _SimpleAppUpgradePlugin.instance;
AppUpgradeSimple._() : _plugin = _SimpleAppUpgradePlugin.instance; AppUpgradeSimple._() : _plugin = _SimpleAppUpgradePlugin.instance;
@ -157,7 +187,10 @@ class AppUpgradeSimple {
if (downloadPath != null) { if (downloadPath != null) {
final dir = Directory(downloadPath); final dir = Directory(downloadPath);
if (await dir.exists()) { if (await dir.exists()) {
final files = await dir.list().where((file) => file.path.endsWith('.apk')).toList(); final files = await dir
.list()
.where((file) => file.path.endsWith('.apk'))
.toList();
for (final file in files) { for (final file in files) {
try { try {
await file.delete(); await file.delete();
@ -232,12 +265,14 @@ class AppUpgradeSimple {
}) async { }) async {
// 使 // 使
final effectiveConfig = config ?? _config; final effectiveConfig = config ?? _config;
final finalShowNoUpdateToast = showNoUpdateToast ?? effectiveConfig.showNoUpdateToast; final finalShowNoUpdateToast =
showNoUpdateToast ?? effectiveConfig.showNoUpdateToast;
final finalAutoInstall = autoInstall ?? effectiveConfig.autoInstall; final finalAutoInstall = autoInstall ?? effectiveConfig.autoInstall;
try { try {
assert(_canShowMaterialDialog(context), '请在 MaterialApp 环境内调用'); assert(_canShowMaterialDialog(context), '请在 MaterialApp 环境内调用');
final info = await _prepareUpgradeInfo(future: future, config: effectiveConfig); final info =
await _prepareUpgradeInfo(future: future, config: effectiveConfig);
if (info == null) { if (info == null) {
return; return;
} }
@ -281,7 +316,8 @@ class AppUpgradeSimple {
}) async { }) async {
final effectiveConfig = config ?? _config; final effectiveConfig = config ?? _config;
try { try {
final info = await _prepareUpgradeInfo(future: future, config: effectiveConfig); final info =
await _prepareUpgradeInfo(future: future, config: effectiveConfig);
if (effectiveConfig.enableDebugLog) { if (effectiveConfig.enableDebugLog) {
if (info == null) { if (info == null) {
debugPrint('🔕 静默检查结果: 未返回版本信息'); debugPrint('🔕 静默检查结果: 未返回版本信息');
@ -377,13 +413,15 @@ class AppUpgradeSimple {
if (versionBuildNumber > currentBuildNumber) { if (versionBuildNumber > currentBuildNumber) {
hasUpdate = true; hasUpdate = true;
} else { } else {
if (versionName != null && compareVersionStrings(versionName, currentVersionName) > 0) { if (versionName != null &&
compareVersionStrings(versionName, currentVersionName) > 0) {
hasUpdate = true; hasUpdate = true;
} }
} }
} else { } else {
// //
if (versionName != null && compareVersionStrings(versionName, currentVersionName) > 0) { if (versionName != null &&
compareVersionStrings(versionName, currentVersionName) > 0) {
hasUpdate = true; hasUpdate = true;
} }
} }
@ -412,7 +450,8 @@ class AppUpgradeSimple {
// UpgradeInfo // UpgradeInfo
// serverInfo // serverInfo
final safeVersionName = serverInfo.versionName ?? currentVersionName; final safeVersionName = serverInfo.versionName ?? currentVersionName;
final safeVersionBuildNumber = serverInfo.versionBuildNumber ?? currentBuildNumber; final safeVersionBuildNumber =
serverInfo.versionBuildNumber ?? currentBuildNumber;
return UpgradeInfo( return UpgradeInfo(
hasUpdate: hasUpdate, hasUpdate: hasUpdate,
@ -428,7 +467,11 @@ class AppUpgradeSimple {
apkMd5: serverInfo.apkMd5, apkMd5: serverInfo.apkMd5,
appMarkets: serverInfo.appMarkets, appMarkets: serverInfo.appMarkets,
supportedMethods: serverInfo.supportedMethods ?? supportedMethods: serverInfo.supportedMethods ??
const [AppUpgradeMethod.market, AppUpgradeMethod.browser, AppUpgradeMethod.inApp], const [
AppUpgradeMethod.market,
AppUpgradeMethod.browser,
AppUpgradeMethod.inApp
],
); );
} }
@ -479,12 +522,43 @@ class AppUpgradeSimple {
return await _plugin.getAppInfo(); return await _plugin.getAppInfo();
} }
static ParsedUpgradeContent parseUpdateContent(String content) {
final normalizedContent =
content.replaceAll('\r\n', '\n').replaceAll('\r', '\n').trim();
if (normalizedContent.isEmpty) {
return const ParsedUpgradeContent();
}
final extraction = _extractUpgradeContentHeader(
normalizedContent.split('\n'),
);
final bodyItems = extraction.bodyLines
.map((line) => line.trim())
.where((line) => line.isNotEmpty)
.map(
(line) => ParsedUpgradeContentItem(
text: line,
hasLeadingMarker: _contentLineMarkerRegExp.hasMatch(line),
),
)
.toList(growable: false);
return ParsedUpgradeContent(
header: extraction.header,
hasExplicitHeaderBlock: extraction.hasExplicitHeaderBlock,
bodyItems: bodyItems,
);
}
static int compareVersionStrings(String v1, String v2) { static int compareVersionStrings(String v1, String v2) {
try { try {
final v1Parts = v1.split('.').map((e) => int.tryParse(e) ?? 0).toList(); final v1Parts = v1.split('.').map((e) => int.tryParse(e) ?? 0).toList();
final v2Parts = v2.split('.').map((e) => int.tryParse(e) ?? 0).toList(); final v2Parts = v2.split('.').map((e) => int.tryParse(e) ?? 0).toList();
final maxLength = v1Parts.length > v2Parts.length ? v1Parts.length : v2Parts.length; final maxLength =
v1Parts.length > v2Parts.length ? v1Parts.length : v2Parts.length;
for (int i = 0; i < maxLength; i++) { for (int i = 0; i < maxLength; i++) {
final part1 = i < v1Parts.length ? v1Parts[i] : 0; final part1 = i < v1Parts.length ? v1Parts[i] : 0;
@ -529,7 +603,8 @@ class AppUpgradeSimple {
autoInstall: autoInstall, autoInstall: autoInstall,
onUpdateLater: onUpdateLater, onUpdateLater: onUpdateLater,
config: effectiveConfig, config: effectiveConfig,
showToast: (message) => _showToast(message, context, effectiveConfig), showToast: (message) =>
_showToast(message, context, effectiveConfig),
); );
} }
}, },
@ -537,7 +612,8 @@ class AppUpgradeSimple {
} }
/// 使 Overlay Toast Scaffold /// 使 Overlay Toast Scaffold
void _showOverlayToast(BuildContext context, String message, UpgradeConfig config) { void _showOverlayToast(
BuildContext context, String message, UpgradeConfig config) {
if (!context.mounted) { if (!context.mounted) {
debugPrint('Toast消息context已卸载: $message'); debugPrint('Toast消息context已卸载: $message');
return; return;
@ -572,7 +648,8 @@ class AppUpgradeSimple {
} }
/// 使 ScaffoldMessenger SnackBar /// 使 ScaffoldMessenger SnackBar
void _tryShowSnackBar(BuildContext context, String message, UpgradeConfig config) { void _tryShowSnackBar(
BuildContext context, String message, UpgradeConfig config) {
if (!context.mounted) { if (!context.mounted) {
_showOverlayToast(context, message, config); _showOverlayToast(context, message, config);
return; return;
@ -598,8 +675,9 @@ class AppUpgradeSimple {
} }
// 使 context ScaffoldMessenger // 使 context ScaffoldMessenger
final messenger = final messenger = rootContext != null && rootContext.mounted
rootContext != null && rootContext.mounted ? ScaffoldMessenger.maybeOf(rootContext) : scaffoldMessenger; ? ScaffoldMessenger.maybeOf(rootContext)
: scaffoldMessenger;
if (messenger == null) { if (messenger == null) {
_showOverlayToast(context, message, config); _showOverlayToast(context, message, config);
@ -624,7 +702,8 @@ class AppUpgradeSimple {
} }
/// Toast提示 /// Toast提示
void _showToast(String message, BuildContext context, [UpgradeConfig? config]) { void _showToast(String message, BuildContext context,
[UpgradeConfig? config]) {
final effectiveConfig = config ?? _config; final effectiveConfig = config ?? _config;
if (effectiveConfig.customToast != null) { if (effectiveConfig.customToast != null) {
effectiveConfig.customToast!(message); effectiveConfig.customToast!(message);
@ -637,7 +716,9 @@ class AppUpgradeSimple {
bool _canShowMaterialDialog(BuildContext context) { bool _canShowMaterialDialog(BuildContext context) {
if (!context.mounted) return false; if (!context.mounted) return false;
try { try {
return Localizations.of<MaterialLocalizations>(context, MaterialLocalizations) != null; return Localizations.of<MaterialLocalizations>(
context, MaterialLocalizations) !=
null;
} catch (_) { } catch (_) {
return false; return false;
} }
@ -669,7 +750,8 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
} }
void onAppLifecycleStateChanged(AppLifecycleState state) { void onAppLifecycleStateChanged(AppLifecycleState state) {
debugPrint('🔄 应用生命周期状态变化: $state, _isWaitingForInstallation=$_isWaitingForInstallation'); debugPrint(
'🔄 应用生命周期状态变化: $state, _isWaitingForInstallation=$_isWaitingForInstallation');
if (_isWaitingForInstallation && state == AppLifecycleState.resumed) { if (_isWaitingForInstallation && state == AppLifecycleState.resumed) {
debugPrint('⚡ 应用回到前台,检查安装状态'); debugPrint('⚡ 应用回到前台,检查安装状态');
@ -726,12 +808,14 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
if (!Platform.isAndroid || info.downloadUrl == null) return; if (!Platform.isAndroid || info.downloadUrl == null) return;
if (!mounted) return; if (!mounted) return;
final hasStorage = await PermissionHelper.checkAndRequestStoragePermission(context: context); final hasStorage = await PermissionHelper.checkAndRequestStoragePermission(
context: context);
if (!hasStorage) { if (!hasStorage) {
showToast('缺少存储权限,无法下载'); showToast('缺少存储权限,无法下载');
return; return;
} }
await PermissionHelper.checkAndRequestNotificationPermission(context: context); await PermissionHelper.checkAndRequestNotificationPermission(
context: context);
setState(() { setState(() {
_isDownloading = true; _isDownloading = true;
@ -794,7 +878,9 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
if (config.requireInstallPermission) { if (config.requireInstallPermission) {
debugPrint('🔐 检查安装权限(配置要求)'); debugPrint('🔐 检查安装权限(配置要求)');
final hasPermission = await PermissionHelper.checkAndRequestInstallPermission(context: context); final hasPermission =
await PermissionHelper.checkAndRequestInstallPermission(
context: context);
if (!hasPermission) { if (!hasPermission) {
if (mounted) { if (mounted) {
setState(() { setState(() {
@ -862,7 +948,8 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
Future<void> _checkInstallationResult() async { Future<void> _checkInstallationResult() async {
if (!mounted || !_isWaitingForInstallation) { if (!mounted || !_isWaitingForInstallation) {
debugPrint('跳过安装结果检查: mounted=$mounted, _isWaitingForInstallation=$_isWaitingForInstallation'); debugPrint(
'跳过安装结果检查: mounted=$mounted, _isWaitingForInstallation=$_isWaitingForInstallation');
return; return;
} }
@ -871,23 +958,30 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
try { try {
final appInfo = await _plugin.getAppInfo(); final appInfo = await _plugin.getAppInfo();
final currentVersion = appInfo['version'] ?? ''; final currentVersion = appInfo['version'] ?? '';
final currentBuildNumber = int.tryParse(appInfo['buildNumber'] ?? '0') ?? 0; final currentBuildNumber =
int.tryParse(appInfo['buildNumber'] ?? '0') ?? 0;
debugPrint('📱 当前版本: $currentVersion, 构建号: $currentBuildNumber'); debugPrint('📱 当前版本: $currentVersion, 构建号: $currentBuildNumber');
debugPrint('🎯 目标版本: ${info.versionName}, 构建号: ${info.versionBuildNumber}'); debugPrint(
'🎯 目标版本: ${info.versionName}, 构建号: ${info.versionBuildNumber}');
bool isUpdated = false; bool isUpdated = false;
if (info.versionBuildNumber > 0) { if (info.versionBuildNumber > 0) {
if (currentBuildNumber > info.versionBuildNumber) { if (currentBuildNumber > info.versionBuildNumber) {
isUpdated = true; isUpdated = true;
} else { } else {
isUpdated = AppUpgradeSimple.compareVersionStrings(currentVersion, info.versionName) > 0; isUpdated = AppUpgradeSimple.compareVersionStrings(
currentVersion, info.versionName) >
0;
} }
debugPrint( debugPrint(
'📊 构建号比较: $currentBuildNumber vs ${info.versionBuildNumber}, 版本比较(如需): ${info.versionName} -> $isUpdated'); '📊 构建号比较: $currentBuildNumber vs ${info.versionBuildNumber}, 版本比较(如需): ${info.versionName} -> $isUpdated');
} else { } else {
isUpdated = AppUpgradeSimple.compareVersionStrings(currentVersion, info.versionName) > 0; isUpdated = AppUpgradeSimple.compareVersionStrings(
debugPrint('📊 版本号比较: $currentVersion vs ${info.versionName} = $isUpdated'); currentVersion, info.versionName) >
0;
debugPrint(
'📊 版本号比较: $currentVersion vs ${info.versionName} = $isUpdated');
} }
if (isUpdated) { if (isUpdated) {
@ -941,7 +1035,9 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
} }
if (_statusText == '权限被拒绝' && config.requireInstallPermission) { if (_statusText == '权限被拒绝' && config.requireInstallPermission) {
final hasPermission = await PermissionHelper.checkAndRequestInstallPermission(context: context); final hasPermission =
await PermissionHelper.checkAndRequestInstallPermission(
context: context);
if (!hasPermission) { if (!hasPermission) {
showToast('仍未获得安装权限,请在设置中手动开启'); showToast('仍未获得安装权限,请在设置中手动开启');
return; return;
@ -1010,7 +1106,8 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
if (info.isForceUpdate) ...[ if (info.isForceUpdate) ...[
const SizedBox(width: 8), const SizedBox(width: 8),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), padding:
const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorScheme.error, color: colorScheme.error,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
@ -1052,7 +1149,8 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
icon: Icons.update, icon: Icons.update,
label: '已安装版本', label: '已安装版本',
// //
value: '${info.currentVersionName} +${info.currentBuildNumber}', value:
'${info.currentVersionName} +${info.currentBuildNumber}',
colorScheme: colorScheme, colorScheme: colorScheme,
), ),
), ),
@ -1116,8 +1214,8 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
Widget _buildUpdateContent(BuildContext context) { Widget _buildUpdateContent(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
final changeItems = final parsedContent =
info.updateContent.split(RegExp(r'\r?\n')).map((e) => e.trim()).where((e) => e.isNotEmpty).toList(); AppUpgradeSimple.parseUpdateContent(info.updateContent);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -1154,7 +1252,7 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
), ),
), ),
child: SingleChildScrollView( child: SingleChildScrollView(
child: changeItems.isEmpty child: !parsedContent.hasBodyItems && !parsedContent.hasHeader
? _buildRichText( ? _buildRichText(
info.updateContent, info.updateContent,
colorScheme, colorScheme,
@ -1162,17 +1260,29 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
: Column( : Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: changeItems.asMap().entries.map((entry) { children: [
if (parsedContent.hasHeader) ...[
_buildRichText(
parsedContent.header!,
colorScheme,
),
if (parsedContent.hasBodyItems)
const SizedBox(height: 10),
],
...parsedContent.bodyItems.asMap().entries.map((entry) {
final index = entry.key; final index = entry.key;
final line = entry.value; final item = entry.value;
return Container( return Container(
width: double.infinity, width: double.infinity,
margin: EdgeInsets.only( margin: EdgeInsets.only(
bottom: index < changeItems.length - 1 ? 8 : 0, bottom: index < parsedContent.bodyItems.length - 1
? 8
: 0,
), ),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (!item.hasLeadingMarker) ...[
Container( Container(
margin: const EdgeInsets.only(top: 6), margin: const EdgeInsets.only(top: 6),
width: 4, width: 4,
@ -1183,11 +1293,15 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded(child: _buildRichText(line, colorScheme)), ],
Expanded(
child: _buildRichText(item.text, colorScheme),
),
], ],
), ),
); );
}).toList(), }),
],
), ),
), ),
), ),
@ -1271,10 +1385,12 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
// [] // []
if (currentChar == '[') { if (currentChar == '[') {
final innerResult = _parseRichTextInternal(text, styles, index + 1, ']'); final innerResult =
_parseRichTextInternal(text, styles, index + 1, ']');
if (innerResult.closed) { if (innerResult.closed) {
flushBuffer(); flushBuffer();
final innerText = text.substring(index + 1, innerResult.nextIndex - 1); final innerText =
text.substring(index + 1, innerResult.nextIndex - 1);
spans.addAll(_applyStyleToSpans( spans.addAll(_applyStyleToSpans(
innerResult.spans, innerResult.spans,
styles.highlightStyle, styles.highlightStyle,
@ -1310,10 +1426,12 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
} }
// **** // ****
else if (text.startsWith('**', index)) { else if (text.startsWith('**', index)) {
final innerResult = _parseRichTextInternal(text, styles, index + 2, '**'); final innerResult =
_parseRichTextInternal(text, styles, index + 2, '**');
if (innerResult.closed) { if (innerResult.closed) {
flushBuffer(); flushBuffer();
final innerText = text.substring(index + 2, innerResult.nextIndex - 2); final innerText =
text.substring(index + 2, innerResult.nextIndex - 2);
spans.addAll(_applyStyleToSpans( spans.addAll(_applyStyleToSpans(
innerResult.spans, innerResult.spans,
styles.boldStyle, styles.boldStyle,
@ -1330,10 +1448,12 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
} }
// __斜体__ // __斜体__
else if (text.startsWith('__', index)) { else if (text.startsWith('__', index)) {
final innerResult = _parseRichTextInternal(text, styles, index + 2, '__'); final innerResult =
_parseRichTextInternal(text, styles, index + 2, '__');
if (innerResult.closed) { if (innerResult.closed) {
flushBuffer(); flushBuffer();
final innerText = text.substring(index + 2, innerResult.nextIndex - 2); final innerText =
text.substring(index + 2, innerResult.nextIndex - 2);
spans.addAll(_applyStyleToSpans( spans.addAll(_applyStyleToSpans(
innerResult.spans, innerResult.spans,
styles.italicStyle, styles.italicStyle,
@ -1358,7 +1478,8 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
return _RichTextParseResult(spans, index, false); return _RichTextParseResult(spans, index, false);
} }
List<TextSpan> _applyStyleToSpans(List<TextSpan> spans, TextStyle style, String fallbackText) { List<TextSpan> _applyStyleToSpans(
List<TextSpan> spans, TextStyle style, String fallbackText) {
if (spans.isEmpty) { if (spans.isEmpty) {
return [ return [
TextSpan( TextSpan(
@ -1372,8 +1493,10 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
} }
TextSpan _mergeTextSpanStyle(TextSpan span, TextStyle style) { TextSpan _mergeTextSpanStyle(TextSpan span, TextStyle style) {
final mergedChildren = final mergedChildren = span.children
span.children?.map((child) => child is TextSpan ? _mergeTextSpanStyle(child, style) : child).toList(); ?.map((child) =>
child is TextSpan ? _mergeTextSpanStyle(child, style) : child)
.toList();
final mergedStyle = span.style != null ? style.merge(span.style) : style; final mergedStyle = span.style != null ? style.merge(span.style) : style;
@ -1384,7 +1507,8 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
); );
} }
Widget _buildEnhancedDownloadProgress(BuildContext context, ColorScheme colorScheme) { Widget _buildEnhancedDownloadProgress(
BuildContext context, ColorScheme colorScheme) {
final bool showRetryButton = _downloadedFilePath != null && final bool showRetryButton = _downloadedFilePath != null &&
!_isDownloading && !_isDownloading &&
!_isInstalling && !_isInstalling &&
@ -1488,7 +1612,8 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
child: Container( child: Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorScheme.primaryContainer.withOpacity(0.2), color: colorScheme.primaryContainer
.withOpacity(0.2),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
border: Border.all( border: Border.all(
color: colorScheme.primary.withOpacity(0.3), color: colorScheme.primary.withOpacity(0.3),
@ -1517,7 +1642,8 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
'系统将自动检测安装结果', '系统将自动检测安装结果',
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
color: colorScheme.onSurface.withOpacity(0.7), color: colorScheme.onSurface
.withOpacity(0.7),
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@ -1534,9 +1660,11 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: _retryInstall, onPressed: _retryInstall,
icon: Icon(_getRetryButtonIcon(), size: 16), icon: Icon(_getRetryButtonIcon(), size: 16),
label: Text(_getRetryButtonText(), style: const TextStyle(fontSize: 12)), label: Text(_getRetryButtonText(),
style: const TextStyle(fontSize: 12)),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 8),
minimumSize: Size.zero, minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap, tapTargetSize: MaterialTapTargetSize.shrinkWrap,
backgroundColor: _getRetryButtonColor(colorScheme), backgroundColor: _getRetryButtonColor(colorScheme),
@ -1545,7 +1673,8 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
), ),
], ],
), ),
if (_isDownloading || (_downloadProgress > 0 && _downloadProgress < 1.0)) ...[ if (_isDownloading ||
(_downloadProgress > 0 && _downloadProgress < 1.0)) ...[
const SizedBox(height: 16), const SizedBox(height: 16),
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
@ -1620,7 +1749,9 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
return Icons.cancel_outlined; return Icons.cancel_outlined;
} else if (_statusText == '安装超时' || _statusText == '检测失败') { } else if (_statusText == '安装超时' || _statusText == '检测失败') {
return Icons.schedule; return Icons.schedule;
} else if (_statusText == '安装失败' || _statusText == '安装异常' || _statusText == '权限被拒绝') { } else if (_statusText == '安装失败' ||
_statusText == '安装异常' ||
_statusText == '权限被拒绝') {
return Icons.error_outline; return Icons.error_outline;
} else if (_downloadProgress >= 1.0) { } else if (_downloadProgress >= 1.0) {
return Icons.check_circle_outline; return Icons.check_circle_outline;
@ -1646,7 +1777,9 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
return colorScheme.secondary.withOpacity(0.8); return colorScheme.secondary.withOpacity(0.8);
} else if (_statusText == '安装超时' || _statusText == '检测失败') { } else if (_statusText == '安装超时' || _statusText == '检测失败') {
return colorScheme.secondary.withOpacity(0.7); return colorScheme.secondary.withOpacity(0.7);
} else if (_statusText == '安装失败' || _statusText == '安装异常' || _statusText == '权限被拒绝') { } else if (_statusText == '安装失败' ||
_statusText == '安装异常' ||
_statusText == '权限被拒绝') {
return colorScheme.error; return colorScheme.error;
} else if (_downloadProgress >= 1.0) { } else if (_downloadProgress >= 1.0) {
return colorScheme.tertiary; return colorScheme.tertiary;
@ -1698,7 +1831,10 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
return colorScheme.secondary; return colorScheme.secondary;
} else if (_statusText == '安装失败' || _statusText == '安装异常') { } else if (_statusText == '安装失败' || _statusText == '安装异常') {
return colorScheme.error; return colorScheme.error;
} else if (_statusText == '安装超时' || _statusText == '安装被取消' || _statusText == '检测失败' || _statusText == '等待安装中') { } else if (_statusText == '安装超时' ||
_statusText == '安装被取消' ||
_statusText == '检测失败' ||
_statusText == '等待安装中') {
return colorScheme.secondary.withOpacity(0.8); return colorScheme.secondary.withOpacity(0.8);
} else if (_statusText == '请完成安装') { } else if (_statusText == '请完成安装') {
return colorScheme.secondary; return colorScheme.secondary;
@ -1758,7 +1894,8 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
Future<void> _handleIosAction(BuildContext context) async { Future<void> _handleIosAction(BuildContext context) async {
if (info.appStoreUrl != null) { if (info.appStoreUrl != null) {
final success = await _plugin.goToAppStore(info.appStoreUrl!, context: context); final success =
await _plugin.goToAppStore(info.appStoreUrl!, context: context);
if (!success) { if (!success) {
showToast('无法打开App Store请稍后重试'); showToast('无法打开App Store请稍后重试');
} }
@ -1822,7 +1959,9 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
debugPrint('配置的应用市场白名单: ${info.appMarkets}'); debugPrint('配置的应用市场白名单: ${info.appMarkets}');
// //
final availableMarkets = info.appMarkets!.where((market) => installedMarkets.contains(market.name)).toList(); final availableMarkets = info.appMarkets!
.where((market) => installedMarkets.contains(market.name))
.toList();
debugPrint('可用的应用市场: $availableMarkets'); debugPrint('可用的应用市场: $availableMarkets');
@ -1843,7 +1982,8 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
final appInfo = await _plugin.getAppInfo(); final appInfo = await _plugin.getAppInfo();
final pkg = appInfo['packageName'] ?? ''; final pkg = appInfo['packageName'] ?? '';
if (pkg.isNotEmpty) { if (pkg.isNotEmpty) {
final success = await _plugin.goToAppStore('market://details?id=$pkg', context: context); final success = await _plugin.goToAppStore('market://details?id=$pkg',
context: context);
if (!success) { if (!success) {
showToast('当前APP没有上架当前设备对应的应用市场请选择其他方式更新'); showToast('当前APP没有上架当前设备对应的应用市场请选择其他方式更新');
} }
@ -1858,7 +1998,8 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
// //
} }
Future<void> _showDownloadChoiceSheet(List<AppUpgradeMethod> availableMethods) async { Future<void> _showDownloadChoiceSheet(
List<AppUpgradeMethod> availableMethods) async {
if (!mounted) return; if (!mounted) return;
final choice = await showModalBottomSheet<AppUpgradeMethod>( final choice = await showModalBottomSheet<AppUpgradeMethod>(
@ -1897,7 +2038,9 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
Expanded( Expanded(
child: Text('选择更新方式', child: Text('选择更新方式',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold))), style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold))),
], ],
), ),
Positioned( Positioned(
@ -1913,23 +2056,30 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
if (availableMethods.contains(AppUpgradeMethod.market)) if (availableMethods.contains(AppUpgradeMethod.market))
ListTile( ListTile(
leading: const Icon(Icons.storefront_outlined), leading: const Icon(Icons.storefront_outlined),
title: const Text('前往应用市场更新', style: TextStyle(fontSize: 16)), title:
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), const Text('前往应用市场更新', style: TextStyle(fontSize: 16)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
onTap: () => Navigator.of(ctx).pop(AppUpgradeMethod.market), onTap: () => Navigator.of(ctx).pop(AppUpgradeMethod.market),
), ),
if (availableMethods.contains(AppUpgradeMethod.inApp)) if (availableMethods.contains(AppUpgradeMethod.inApp))
ListTile( ListTile(
leading: const Icon(Icons.system_update), leading: const Icon(Icons.system_update),
title: const Text('APP内更新', style: TextStyle(fontSize: 16)), title: const Text('APP内更新', style: TextStyle(fontSize: 16)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
onTap: () => Navigator.of(ctx).pop(AppUpgradeMethod.inApp), onTap: () => Navigator.of(ctx).pop(AppUpgradeMethod.inApp),
), ),
if (availableMethods.contains(AppUpgradeMethod.browser)) if (availableMethods.contains(AppUpgradeMethod.browser))
ListTile( ListTile(
leading: const Icon(Icons.download_for_offline_outlined), leading: const Icon(Icons.download_for_offline_outlined),
title: const Text('前往浏览器下载安装包', textAlign: TextAlign.left, style: TextStyle(fontSize: 16)), title: const Text('前往浏览器下载安装包',
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), textAlign: TextAlign.left,
onTap: () => Navigator.of(ctx).pop(AppUpgradeMethod.browser), style: TextStyle(fontSize: 16)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
onTap: () =>
Navigator.of(ctx).pop(AppUpgradeMethod.browser),
), ),
const Divider(height: 24), const Divider(height: 24),
SizedBox( SizedBox(
@ -1938,7 +2088,8 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
backgroundColor: Colors.white, backgroundColor: Colors.white,
foregroundColor: Theme.of(context).textTheme.bodyLarge?.color, foregroundColor:
Theme.of(context).textTheme.bodyLarge?.color,
elevation: 2, elevation: 2,
shadowColor: Colors.grey.withOpacity(0.5), shadowColor: Colors.grey.withOpacity(0.5),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
@ -1947,7 +2098,9 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
), ),
), ),
onPressed: () => Navigator.of(ctx).pop(), onPressed: () => Navigator.of(ctx).pop(),
child: const Text('取消', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), child: const Text('取消',
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w500)),
), ),
), ),
], ],
@ -1993,7 +2146,8 @@ class _SimpleUpgradeDialog extends StatefulWidget {
State<_SimpleUpgradeDialog> createState() => _SimpleUpgradeDialogState(); State<_SimpleUpgradeDialog> createState() => _SimpleUpgradeDialogState();
} }
class _SimpleUpgradeDialogState extends State<_SimpleUpgradeDialog> with _UpgradeDialogLogic, WidgetsBindingObserver { class _SimpleUpgradeDialogState extends State<_SimpleUpgradeDialog>
with _UpgradeDialogLogic, WidgetsBindingObserver {
@override @override
UpgradeInfo get info => widget.info; UpgradeInfo get info => widget.info;
@override @override
@ -2068,11 +2222,13 @@ class _SimpleUpgradeDialogState extends State<_SimpleUpgradeDialog> with _Upgrad
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox(height: 10), const SizedBox(height: 10),
_buildVersionInfoCard(context, Theme.of(context).colorScheme), _buildVersionInfoCard(
context, Theme.of(context).colorScheme),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildUpdateContent(context), _buildUpdateContent(context),
if (_isDownloading || _downloadedFilePath != null) if (_isDownloading || _downloadedFilePath != null)
_buildEnhancedDownloadProgress(context, Theme.of(context).colorScheme), _buildEnhancedDownloadProgress(
context, Theme.of(context).colorScheme),
], ],
), ),
), ),
@ -2120,12 +2276,13 @@ class _ForceUpgradeDialog extends StatefulWidget {
State<_ForceUpgradeDialog> createState() => _ForceUpgradeDialogState(); State<_ForceUpgradeDialog> createState() => _ForceUpgradeDialogState();
} }
class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog> with _UpgradeDialogLogic, WidgetsBindingObserver { class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog>
with _UpgradeDialogLogic, WidgetsBindingObserver {
@override @override
UpgradeInfo get info => widget.info; UpgradeInfo get info => widget.info;
@override @override
void Function(String) get showToast => void Function(String) get showToast => (message) =>
(message) => AppUpgradeSimple.instance._showToast(message, context, widget.config); AppUpgradeSimple.instance._showToast(message, context, widget.config);
@override @override
bool get autoInstall => widget.autoInstall; bool get autoInstall => widget.autoInstall;
@override @override
@ -2175,7 +2332,8 @@ class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog> with _UpgradeD
const Expanded( const Expanded(
child: Text( child: Text(
'发现新版本', '发现新版本',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), style: TextStyle(
fontSize: 18, fontWeight: FontWeight.w600),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ),
@ -2190,11 +2348,13 @@ class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog> with _UpgradeD
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox(height: 10), const SizedBox(height: 10),
_buildVersionInfoCard(context, Theme.of(context).colorScheme), _buildVersionInfoCard(
context, Theme.of(context).colorScheme),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildUpdateContent(context), _buildUpdateContent(context),
if (_isDownloading || _downloadedFilePath != null) if (_isDownloading || _downloadedFilePath != null)
_buildEnhancedDownloadProgress(context, Theme.of(context).colorScheme), _buildEnhancedDownloadProgress(
context, Theme.of(context).colorScheme),
], ],
), ),
), ),
@ -2247,7 +2407,8 @@ class _ToastWidget extends StatelessWidget {
); );
}, },
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black87, color: Colors.black87,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
@ -2324,3 +2485,105 @@ class _RichTextParseResult {
const _RichTextParseResult(this.spans, this.nextIndex, this.closed); const _RichTextParseResult(this.spans, this.nextIndex, this.closed);
} }
final RegExp _contentLineMarkerRegExp = RegExp(
r'^(?:(?:\d+|[一二三四五六七八九十]+)[\.\、\\)](?=\s|[^\d]|$)\s*|(?:[-*+•●○▪▫■□◆◇])(?=\s|$)\s*)',
);
class _UpgradeContentHeaderExtraction {
const _UpgradeContentHeaderExtraction({
required this.bodyLines,
this.header,
this.hasExplicitHeaderBlock = false,
});
final List<String> bodyLines;
final String? header;
final bool hasExplicitHeaderBlock;
}
_UpgradeContentHeaderExtraction _extractUpgradeContentHeader(
List<String> rawLines,
) {
const startMarker = '/{';
const endMarker = '/}';
if (rawLines.isEmpty) {
return const _UpgradeContentHeaderExtraction(bodyLines: <String>[]);
}
final firstLine = rawLines.first.trim();
if (!firstLine.startsWith(startMarker)) {
return _UpgradeContentHeaderExtraction(
bodyLines: List<String>.from(rawLines),
);
}
final headerLines = <String>[];
final bodyLines = <String>[];
var currentLine = firstLine.substring(startMarker.length).trimLeft();
var endFound = false;
var endLineIndex = 0;
var pendingBlankLine = false;
void addHeaderLine(String value) {
final normalizedLine = value.trim();
if (normalizedLine.isEmpty) {
pendingBlankLine = headerLines.isNotEmpty;
return;
}
if (pendingBlankLine &&
headerLines.isNotEmpty &&
headerLines.last.isNotEmpty) {
headerLines.add('');
}
headerLines.add(normalizedLine);
pendingBlankLine = false;
}
bool closeHeader(String value) {
final endIndex = value.indexOf(endMarker);
if (endIndex < 0) {
return false;
}
addHeaderLine(value.substring(0, endIndex));
final trailingBody = value.substring(endIndex + endMarker.length).trim();
if (trailingBody.isNotEmpty) {
bodyLines.add(trailingBody);
}
endFound = true;
return true;
}
if (!closeHeader(currentLine) && currentLine.isNotEmpty) {
addHeaderLine(currentLine);
}
for (var index = 1; index < rawLines.length && !endFound; index++) {
currentLine = rawLines[index].trim();
endLineIndex = index;
if (closeHeader(currentLine)) {
break;
}
addHeaderLine(currentLine);
}
if (!endFound) {
return _UpgradeContentHeaderExtraction(
bodyLines: List<String>.from(rawLines),
);
}
if (endLineIndex + 1 < rawLines.length) {
bodyLines.addAll(rawLines.sublist(endLineIndex + 1));
}
return _UpgradeContentHeaderExtraction(
bodyLines: bodyLines,
header: headerLines.isEmpty ? null : headerLines.join('\n'),
hasExplicitHeaderBlock: true,
);
}

View File

@ -0,0 +1,228 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:yx_app_upgrade_flutter/app_upgrade_simple.dart';
import 'package:yx_app_upgrade_flutter/models/upgrade_info.dart';
void main() {
group('showPreparedUpgrade', () {
testWidgets('renders header block and body content without crashing',
(WidgetTester tester) async {
var updateLaterCalled = false;
await tester.pumpWidget(
_DialogTestHost(
info: _buildUpgradeInfo(
updateContent: '''
/{
**V2.2.0** [使]
__支持嵌套__ **[]** `version 2.0`
/}
1. ****
''',
),
onUpdateLater: () {
updateLaterCalled = true;
},
),
);
await tester.tap(find.text('open-upgrade-dialog'));
await tester.pumpAndSettle();
expect(find.text('发现新版本'), findsOneWidget);
expect(find.text('更新内容'), findsOneWidget);
expect(_richTextPlainTexts(tester),
contains(contains('本次 V2.2.0 版本聚焦使用体验优化。')));
expect(_richTextPlainTexts(tester),
contains(contains('支持嵌套,例如 特别注意 与 version 2.0')));
expect(_richTextPlainTexts(tester), contains(contains('1. 第一条说明')));
expect(_richTextPlainTexts(tester), contains(contains('普通补充说明')));
await tester.tap(find.text('稍后更新'));
await tester.pumpAndSettle();
expect(updateLaterCalled, isTrue);
expect(find.text('发现新版本'), findsNothing);
});
testWidgets('skips dialog when hasUpdate is false',
(WidgetTester tester) async {
await tester.pumpWidget(
_DialogTestHost(
info: _buildUpgradeInfo(
hasUpdate: false,
updateContent: '当前已经是最新版本',
),
),
);
await tester.tap(find.text('open-upgrade-dialog'));
await tester.pumpAndSettle();
expect(find.text('发现新版本'), findsNothing);
expect(find.text('更新内容'), findsNothing);
});
testWidgets('handles explicit but empty header block in dialog',
(WidgetTester tester) async {
await tester.pumpWidget(
_DialogTestHost(
info: _buildUpgradeInfo(
updateContent: '''
/{ /}
-
''',
),
),
);
await tester.tap(find.text('open-upgrade-dialog'));
await tester.pumpAndSettle();
expect(find.text('发现新版本'), findsOneWidget);
expect(_richTextPlainTexts(tester), contains(contains('- 第一条说明')));
expect(_richTextPlainTexts(tester), contains(contains('第二条说明')));
});
testWidgets('renders the final growth-center line as update content',
(WidgetTester tester) async {
await tester.pumpWidget(
_DialogTestHost(
info: _buildUpgradeInfo(
updateContent: '''
/{ **V2.2.0** [使][][][]/}
**稿**退使
****使使
线****线访
****
''',
),
),
);
await tester.tap(find.text('open-upgrade-dialog'));
await tester.pumpAndSettle();
expect(find.text('发现新版本'), findsOneWidget);
expect(
_richTextPlainTexts(tester),
contains(
contains(
'成长中心全面调整优化:成长中心入口迁移至工作台页面,升级全局搜索能力,章节内容改为结构化排版展示;优化视频播放逻辑与播放限制规则,同时新增学习过程监管功能,强化学习管控。',
),
),
);
});
testWidgets('renders supported rich text markers correctly on first lines',
(WidgetTester tester) async {
await tester.pumpWidget(
_DialogTestHost(
info: _buildUpgradeInfo(
updateContent: '''
/{**** /}
****
__斜体第二行__
``
[]
**[]**
''',
),
),
);
await tester.tap(find.text('open-upgrade-dialog'));
await tester.pumpAndSettle();
final plainTexts = _richTextPlainTexts(tester).toList();
expect(find.text('发现新版本'), findsOneWidget);
expect(
plainTexts,
contains(contains('头部粗体')),
);
expect(
plainTexts,
contains(contains('粗体首行')),
);
expect(
plainTexts,
contains(contains('斜体第二行')),
);
expect(
plainTexts,
contains(contains('代码第三行')),
);
expect(
plainTexts,
contains(contains('高亮第四行')),
);
expect(
plainTexts,
contains(contains('组合重点')),
);
});
});
}
class _DialogTestHost extends StatelessWidget {
const _DialogTestHost({
required this.info,
this.onUpdateLater,
});
final UpgradeInfo info;
final VoidCallback? onUpdateLater;
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Builder(
builder: (context) {
return Center(
child: ElevatedButton(
onPressed: () {
AppUpgradeSimple.instance.showPreparedUpgrade(
context: context,
info: info,
onUpdateLater: onUpdateLater,
config: const UpgradeConfig(
showNoUpdateToast: false,
autoInstall: false,
enableDebugLog: false,
),
);
},
child: const Text('open-upgrade-dialog'),
),
);
},
),
),
);
}
}
UpgradeInfo _buildUpgradeInfo({
bool hasUpdate = true,
String updateContent = '默认更新说明',
}) {
return UpgradeInfo(
hasUpdate: hasUpdate,
isForceUpdate: false,
versionBuildNumber: 200,
versionName: '2.0.0',
updateContent: updateContent,
currentBuildNumber: 100,
currentVersionName: '1.0.0',
);
}
Iterable<String> _richTextPlainTexts(WidgetTester tester) {
return tester
.widgetList<RichText>(find.byType(RichText))
.map((widget) => widget.text.toPlainText());
}

View File

@ -0,0 +1,284 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yx_app_upgrade_flutter/app_upgrade_simple.dart';
void main() {
group('parseUpdateContent', () {
test('extracts header block and preserves nested markers', () {
const content = '''
/{
**V2.2.0** [使]
__支持嵌套__ **[]** `version 2.0`
/}
1.
2.
''';
final parsed = AppUpgradeSimple.parseUpdateContent(content);
expect(parsed.hasExplicitHeaderBlock, isTrue);
expect(
parsed.header,
'本次 **V2.2.0** 版本聚焦[使用体验]优化。\n__支持嵌套__例如 **[特别注意]** 与 `version 2.0`',
);
expect(parsed.bodyItems, hasLength(2));
expect(parsed.bodyItems.first.text, '1. 第一条说明');
expect(parsed.bodyItems.first.hasLeadingMarker, isTrue);
expect(parsed.bodyItems.last.text, '2. 第二条说明');
});
test('supports inline header block', () {
const content = '''
/{ /}
-
''';
final parsed = AppUpgradeSimple.parseUpdateContent(content);
expect(parsed.hasExplicitHeaderBlock, isTrue);
expect(parsed.header, '更新内容如下:');
expect(parsed.bodyItems, hasLength(2));
expect(parsed.bodyItems.first.hasLeadingMarker, isTrue);
expect(parsed.bodyItems.last.hasLeadingMarker, isFalse);
});
test('supports explicit but empty header block', () {
const content = '''
/{ /}
''';
final parsed = AppUpgradeSimple.parseUpdateContent(content);
expect(parsed.hasExplicitHeaderBlock, isTrue);
expect(parsed.header, isNull);
expect(parsed.bodyItems, hasLength(1));
expect(parsed.bodyItems.first.text, '正文说明');
});
test('keeps original content when header block is missing', () {
const content = '''
****
''';
final parsed = AppUpgradeSimple.parseUpdateContent(content);
expect(parsed.hasExplicitHeaderBlock, isFalse);
expect(parsed.header, isNull);
expect(parsed.bodyItems, hasLength(2));
expect(parsed.bodyItems.first.text, '**重要内容**');
expect(parsed.bodyItems.last.text, '第二行说明');
});
test('preserves blank lines inside header block', () {
const content = '''
/{
/}
''';
final parsed = AppUpgradeSimple.parseUpdateContent(content);
expect(parsed.hasExplicitHeaderBlock, isTrue);
expect(parsed.header, '第一段说明\n\n第二段说明');
expect(parsed.bodyItems, hasLength(1));
expect(parsed.bodyItems.first.text, '正文说明');
});
test('falls back to original content when header block is not closed', () {
const content = '''
/{
''';
final parsed = AppUpgradeSimple.parseUpdateContent(content);
expect(parsed.hasExplicitHeaderBlock, isFalse);
expect(parsed.header, isNull);
expect(parsed.bodyItems, hasLength(3));
expect(parsed.bodyItems[0].text, '/{');
expect(parsed.bodyItems[1].text, '第一段说明');
expect(parsed.bodyItems[2].text, '正文说明');
});
test('supports trailing body text on the same line as header end marker',
() {
const content = '''
/{ /} 1.
''';
final parsed = AppUpgradeSimple.parseUpdateContent(content);
expect(parsed.hasExplicitHeaderBlock, isTrue);
expect(parsed.header, '头部说明');
expect(parsed.bodyItems, hasLength(1));
expect(parsed.bodyItems.first.text, '1. 第一条说明');
expect(parsed.bodyItems.first.hasLeadingMarker, isTrue);
});
test('normalizes windows line endings', () {
const content = '/{\r\n头部说明\r\n/}\r\n- 第一条\r\n普通说明\r\n';
final parsed = AppUpgradeSimple.parseUpdateContent(content);
expect(parsed.hasExplicitHeaderBlock, isTrue);
expect(parsed.header, '头部说明');
expect(parsed.bodyItems, hasLength(2));
expect(parsed.bodyItems.first.text, '- 第一条');
expect(parsed.bodyItems.last.text, '普通说明');
});
test('detects multiple supported leading marker styles', () {
const content = '''
1.
-
''';
final parsed = AppUpgradeSimple.parseUpdateContent(content);
expect(parsed.hasExplicitHeaderBlock, isFalse);
expect(parsed.bodyItems, hasLength(5));
expect(parsed.bodyItems[0].hasLeadingMarker, isTrue);
expect(parsed.bodyItems[1].hasLeadingMarker, isTrue);
expect(parsed.bodyItems[2].hasLeadingMarker, isTrue);
expect(parsed.bodyItems[3].hasLeadingMarker, isTrue);
expect(parsed.bodyItems[4].hasLeadingMarker, isFalse);
});
test('does not treat later header markers inside body as header block', () {
const content = '''
/{ /}
''';
final parsed = AppUpgradeSimple.parseUpdateContent(content);
expect(parsed.hasExplicitHeaderBlock, isFalse);
expect(parsed.header, isNull);
expect(parsed.bodyItems, hasLength(3));
expect(parsed.bodyItems[1].text, '/{ 这不是头部 /}');
});
test('returns empty result for blank content', () {
final parsed = AppUpgradeSimple.parseUpdateContent(' \n \r\n ');
expect(parsed.hasExplicitHeaderBlock, isFalse);
expect(parsed.header, isNull);
expect(parsed.bodyItems, isEmpty);
});
test('keeps the final growth-center line as a separate body item', () {
const content = '''
/{ **V2.2.0** [使][][][]/}
**稿**退使
****使使
线****线访
****
''';
final parsed = AppUpgradeSimple.parseUpdateContent(content);
expect(parsed.hasExplicitHeaderBlock, isTrue);
expect(
parsed.header,
'本次 **V2.2.0** 版本围绕[使用体验]、[账号安全]、[家校联动]、[学习管理]等方面进行多项优化与功能新增,具体更新内容如下:',
);
expect(parsed.bodyItems, hasLength(4));
expect(parsed.bodyItems[0].text, startsWith('工作录入新增**草稿箱功能**'));
expect(parsed.bodyItems[1].text, startsWith('优化**多账号登录机制**'));
expect(parsed.bodyItems[2].text, startsWith('全新上线**家校互动板块**'));
expect(
parsed.bodyItems[3].text,
'**成长中心**全面调整优化:成长中心入口迁移至工作台页面,升级全局搜索能力,章节内容改为结构化排版展示;优化视频播放逻辑与播放限制规则,同时新增学习过程监管功能,强化学习管控。',
);
});
test('does not treat bold marker at line start as list marker', () {
const content = '''
****
*
''';
final parsed = AppUpgradeSimple.parseUpdateContent(content);
expect(parsed.bodyItems, hasLength(2));
expect(parsed.bodyItems.first.text,
'**成长中心**全面调整优化:成长中心入口迁移至工作台页面,升级全局搜索能力。');
expect(parsed.bodyItems.first.hasLeadingMarker, isFalse);
expect(parsed.bodyItems.last.text, '* 合法无序列表项');
expect(parsed.bodyItems.last.hasLeadingMarker, isTrue);
});
test(
'supports all rich text markers when they appear on the first body line',
() {
const content = '''
****
__斜体次行__
``
[]
**[]**
''';
final parsed = AppUpgradeSimple.parseUpdateContent(content);
expect(parsed.hasExplicitHeaderBlock, isFalse);
expect(parsed.bodyItems, hasLength(5));
expect(parsed.bodyItems[0].text, '**粗体首行**');
expect(parsed.bodyItems[1].text, '__斜体次行__');
expect(parsed.bodyItems[2].text, '`代码第三行`');
expect(parsed.bodyItems[3].text, '[高亮第四行]');
expect(parsed.bodyItems[4].text, '**[组合重点]**');
expect(
parsed.bodyItems.every((item) => item.hasLeadingMarker == false),
isTrue,
);
});
test('supports rich text markers on the first line inside header block',
() {
const content = '''
/{
****
__[]__
``
/}
''';
final parsed = AppUpgradeSimple.parseUpdateContent(content);
expect(parsed.hasExplicitHeaderBlock, isTrue);
expect(
parsed.header,
'**头部粗体**\n__[头部高亮斜体]__\n`头部代码`',
);
expect(parsed.bodyItems, hasLength(1));
expect(parsed.bodyItems.first.text, '正文说明');
});
test('does not treat version-like prefix as ordered marker', () {
const content = '''
1.0
2.
''';
final parsed = AppUpgradeSimple.parseUpdateContent(content);
expect(parsed.bodyItems, hasLength(2));
expect(parsed.bodyItems.first.text, '1.0版本修复说明');
expect(parsed.bodyItems.first.hasLeadingMarker, isFalse);
expect(parsed.bodyItems.last.text, '2. 第一条真正的序号内容');
expect(parsed.bodyItems.last.hasLeadingMarker, isTrue);
});
});
}