Compare commits
6 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
11a42dd186 | |
|
|
e6e7f8e951 | |
|
|
93462a04ed | |
|
|
704a300896 | |
|
|
bcfed6f7c1 | |
|
|
7978f7c9f7 |
97
README.md
97
README.md
|
|
@ -132,12 +132,109 @@ AppUpgradeSimple.instance.configure(const UpgradeConfig(
|
|||
- **代码块**:`` `version 2.0` ``
|
||||
- **高亮**:`[特别注意]`
|
||||
|
||||
支持嵌套组合使用,例如:
|
||||
|
||||
- `**[特别注意]**`
|
||||
- `__**重要说明**__`
|
||||
- `请使用 \`version 2.0\` 进行灰度验证`
|
||||
|
||||
```text
|
||||
1. 新增 **深色模式** 支持
|
||||
2. 修复 `Login` 页面崩溃问题
|
||||
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`:该条正文是否已经自带列表前缀
|
||||
|
||||
### 对话框特性
|
||||
|
||||
- **发现新版本**(强制/非强制)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ buildscript {
|
|||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,3 +43,6 @@ app.*.map.json
|
|||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
# FVM Version Cache
|
||||
.fvm/
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"dart.flutterSdkPath": ".fvm/versions/3.35.5"
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ pluginManagement {
|
|||
|
||||
plugins {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -67,13 +67,13 @@ class _HomePageState extends State<HomePage> {
|
|||
context: context,
|
||||
future: () async {
|
||||
final updateAppEvent = await _getUpdateAppEvent();
|
||||
print("获取最新版本: $updateAppEvent");
|
||||
debugPrint("获取最新版本: $updateAppEvent");
|
||||
if (updateAppEvent == null) return null;
|
||||
|
||||
return _convertToAppUpgradeVersion(updateAppEvent);
|
||||
},
|
||||
onComplete: (bool val) {
|
||||
print("更新插件执行完成....: $val");
|
||||
onComplete: () {
|
||||
debugPrint("更新插件执行完成....");
|
||||
},
|
||||
config: UpgradeConfig.development,
|
||||
// showNoUpdateToast: false,
|
||||
|
|
@ -99,10 +99,14 @@ class _HomePageState extends State<HomePage> {
|
|||
"id": 708608950206533,
|
||||
"version": 307,
|
||||
"versionName": "1.0.9",
|
||||
"remark": """
|
||||
**布置工作:**上级可以向分属团队`布置任务`,设定类型和时间。\n
|
||||
**工作管理上:**__级可集中查看下级`所有工作`的详情与进度。__\n
|
||||
**学生详情:**__在[学生管理]中添加详情页,支持记录家长信息、学生备注等,完善学生档案。__\n""",
|
||||
"remark": """班级活动 / 文创更新,支持**多选班级**;
|
||||
新增成长中心;
|
||||
学科辅助可**全选学生**;
|
||||
学习习惯抽查打分方式优化;
|
||||
1 对 1 谈话选学生时,可直观查看本月已谈话次数;
|
||||
上传图片支持多选,操作优化;
|
||||
解决方案搜索移除年级限制;
|
||||
总部长 / 部长可一键清除已读状态,布置任务可撤回重发。""",
|
||||
"imageBase": null,
|
||||
"updatetype": 1,
|
||||
"isActive": 1,
|
||||
|
|
@ -158,7 +162,7 @@ class _HomePageState extends State<HomePage> {
|
|||
final upgradeInfo = await AppUpgradeSimple.instance.silentCheckUpdate(
|
||||
future: () async {
|
||||
final updateAppEvent = await _getUpdateAppEvent();
|
||||
print("获取最新版本: $updateAppEvent");
|
||||
debugPrint("获取最新版本: $updateAppEvent");
|
||||
if (updateAppEvent == null) return null;
|
||||
|
||||
return _convertToAppUpgradeVersion(updateAppEvent);
|
||||
|
|
@ -167,7 +171,9 @@ class _HomePageState extends State<HomePage> {
|
|||
// 2. 根据结果处理
|
||||
if (upgradeInfo != null && upgradeInfo.hasUpdate) {
|
||||
// 有新版本,可以显示红点或在用户点击时调用弹窗
|
||||
print('发现新版本: ${upgradeInfo.versionName}');
|
||||
debugPrint('发现新版本: ${upgradeInfo.versionName}');
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// 在需要展示弹窗的时机(如用户点击按钮):
|
||||
AppUpgradeSimple.instance.showPreparedUpgrade(
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.13.0"
|
||||
version: "2.13.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -45,26 +45,26 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
|
||||
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
version: "1.0.9"
|
||||
dio:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio
|
||||
sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
|
||||
sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.9.0"
|
||||
version: "5.9.2"
|
||||
dio_web_adapter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio_web_adapter
|
||||
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
|
||||
sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -77,10 +77,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
|
||||
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.2.0"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -224,10 +224,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d
|
||||
sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "9.0.0"
|
||||
version: "9.0.1"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -256,10 +256,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
|
||||
sha256: "149441ca6e4f38193b2e004c0ca6376a3d11f51fa5a77552d8bd4d2b0c0912ba"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.22"
|
||||
version: "2.2.23"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -373,10 +373,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.10.1"
|
||||
version: "1.10.2"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -445,10 +445,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611"
|
||||
sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.3.28"
|
||||
version: "6.3.29"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -509,10 +509,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
|
||||
sha256: "046d3928e16fa4dc46e8350415661755ab759d9fc97fc21b5ab295f71e4f0499"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "15.0.2"
|
||||
version: "15.1.0"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -551,7 +551,7 @@ packages:
|
|||
path: ".."
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.0.0"
|
||||
version: "1.0.5"
|
||||
sdks:
|
||||
dart: ">=3.9.0 <4.0.0"
|
||||
flutter: ">=3.35.0"
|
||||
|
|
|
|||
|
|
@ -23,8 +23,10 @@ class _SimpleAppUpgradePlugin {
|
|||
return AppUpgradePluginPlatform.instance.checkUpdate(url, params: params);
|
||||
}
|
||||
|
||||
Future<String?> downloadApk(String url, {Function(DownloadProgress)? onProgress}) {
|
||||
return AppUpgradePluginPlatform.instance.downloadApk(url, onProgress: onProgress);
|
||||
Future<String?> downloadApk(String url,
|
||||
{Function(DownloadProgress)? onProgress}) {
|
||||
return AppUpgradePluginPlatform.instance
|
||||
.downloadApk(url, onProgress: onProgress);
|
||||
}
|
||||
|
||||
Future<bool> installApk(String filePath) {
|
||||
|
|
@ -32,7 +34,8 @@ class _SimpleAppUpgradePlugin {
|
|||
}
|
||||
|
||||
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() {
|
||||
|
|
@ -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升级管理器
|
||||
/// 提供最简单的API,一行代码即可实现App升级功能
|
||||
class AppUpgradeSimple {
|
||||
|
|
@ -127,7 +156,8 @@ class AppUpgradeSimple {
|
|||
}
|
||||
|
||||
@visibleForTesting
|
||||
AppUpgradeSimple.private({_SimpleAppUpgradePlugin? plugin}) : _plugin = plugin ?? _SimpleAppUpgradePlugin.instance;
|
||||
AppUpgradeSimple.private({_SimpleAppUpgradePlugin? plugin})
|
||||
: _plugin = plugin ?? _SimpleAppUpgradePlugin.instance;
|
||||
|
||||
AppUpgradeSimple._() : _plugin = _SimpleAppUpgradePlugin.instance;
|
||||
|
||||
|
|
@ -157,7 +187,10 @@ class AppUpgradeSimple {
|
|||
if (downloadPath != null) {
|
||||
final dir = Directory(downloadPath);
|
||||
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) {
|
||||
try {
|
||||
await file.delete();
|
||||
|
|
@ -232,12 +265,14 @@ class AppUpgradeSimple {
|
|||
}) async {
|
||||
// 使用传入的配置或默认配置
|
||||
final effectiveConfig = config ?? _config;
|
||||
final finalShowNoUpdateToast = showNoUpdateToast ?? effectiveConfig.showNoUpdateToast;
|
||||
final finalShowNoUpdateToast =
|
||||
showNoUpdateToast ?? effectiveConfig.showNoUpdateToast;
|
||||
final finalAutoInstall = autoInstall ?? effectiveConfig.autoInstall;
|
||||
try {
|
||||
assert(_canShowMaterialDialog(context), '请在 MaterialApp 环境内调用');
|
||||
|
||||
final info = await _prepareUpgradeInfo(future: future, config: effectiveConfig);
|
||||
final info =
|
||||
await _prepareUpgradeInfo(future: future, config: effectiveConfig);
|
||||
if (info == null) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -281,7 +316,8 @@ class AppUpgradeSimple {
|
|||
}) async {
|
||||
final effectiveConfig = config ?? _config;
|
||||
try {
|
||||
final info = await _prepareUpgradeInfo(future: future, config: effectiveConfig);
|
||||
final info =
|
||||
await _prepareUpgradeInfo(future: future, config: effectiveConfig);
|
||||
if (effectiveConfig.enableDebugLog) {
|
||||
if (info == null) {
|
||||
debugPrint('🔕 静默检查结果: 未返回版本信息');
|
||||
|
|
@ -377,13 +413,15 @@ class AppUpgradeSimple {
|
|||
if (versionBuildNumber > currentBuildNumber) {
|
||||
hasUpdate = true;
|
||||
} else {
|
||||
if (versionName != null && compareVersionStrings(versionName, currentVersionName) > 0) {
|
||||
if (versionName != null &&
|
||||
compareVersionStrings(versionName, currentVersionName) > 0) {
|
||||
hasUpdate = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 只比较版本名
|
||||
if (versionName != null && compareVersionStrings(versionName, currentVersionName) > 0) {
|
||||
if (versionName != null &&
|
||||
compareVersionStrings(versionName, currentVersionName) > 0) {
|
||||
hasUpdate = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -412,7 +450,8 @@ class AppUpgradeSimple {
|
|||
// 构建 UpgradeInfo
|
||||
// 兜底处理,避免 serverInfo 里的可空字段传入非空参数导致崩溃
|
||||
final safeVersionName = serverInfo.versionName ?? currentVersionName;
|
||||
final safeVersionBuildNumber = serverInfo.versionBuildNumber ?? currentBuildNumber;
|
||||
final safeVersionBuildNumber =
|
||||
serverInfo.versionBuildNumber ?? currentBuildNumber;
|
||||
|
||||
return UpgradeInfo(
|
||||
hasUpdate: hasUpdate,
|
||||
|
|
@ -428,7 +467,11 @@ class AppUpgradeSimple {
|
|||
apkMd5: serverInfo.apkMd5,
|
||||
appMarkets: serverInfo.appMarkets,
|
||||
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();
|
||||
}
|
||||
|
||||
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) {
|
||||
try {
|
||||
final v1Parts = v1.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++) {
|
||||
final part1 = i < v1Parts.length ? v1Parts[i] : 0;
|
||||
|
|
@ -529,7 +603,8 @@ class AppUpgradeSimple {
|
|||
autoInstall: autoInstall,
|
||||
onUpdateLater: onUpdateLater,
|
||||
config: effectiveConfig,
|
||||
showToast: (message) => _showToast(message, context, effectiveConfig),
|
||||
showToast: (message) =>
|
||||
_showToast(message, context, effectiveConfig),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
@ -537,7 +612,8 @@ class AppUpgradeSimple {
|
|||
}
|
||||
|
||||
/// 使用 Overlay 显示 Toast(不依赖 Scaffold)
|
||||
void _showOverlayToast(BuildContext context, String message, UpgradeConfig config) {
|
||||
void _showOverlayToast(
|
||||
BuildContext context, String message, UpgradeConfig config) {
|
||||
if (!context.mounted) {
|
||||
debugPrint('Toast消息(context已卸载): $message');
|
||||
return;
|
||||
|
|
@ -572,7 +648,8 @@ class AppUpgradeSimple {
|
|||
}
|
||||
|
||||
/// 尝试使用 ScaffoldMessenger 显示 SnackBar(如果可用)
|
||||
void _tryShowSnackBar(BuildContext context, String message, UpgradeConfig config) {
|
||||
void _tryShowSnackBar(
|
||||
BuildContext context, String message, UpgradeConfig config) {
|
||||
if (!context.mounted) {
|
||||
_showOverlayToast(context, message, config);
|
||||
return;
|
||||
|
|
@ -598,8 +675,9 @@ class AppUpgradeSimple {
|
|||
}
|
||||
|
||||
// 优先使用根 context 的 ScaffoldMessenger
|
||||
final messenger =
|
||||
rootContext != null && rootContext.mounted ? ScaffoldMessenger.maybeOf(rootContext) : scaffoldMessenger;
|
||||
final messenger = rootContext != null && rootContext.mounted
|
||||
? ScaffoldMessenger.maybeOf(rootContext)
|
||||
: scaffoldMessenger;
|
||||
|
||||
if (messenger == null) {
|
||||
_showOverlayToast(context, message, config);
|
||||
|
|
@ -624,7 +702,8 @@ class AppUpgradeSimple {
|
|||
}
|
||||
|
||||
/// 显示Toast提示
|
||||
void _showToast(String message, BuildContext context, [UpgradeConfig? config]) {
|
||||
void _showToast(String message, BuildContext context,
|
||||
[UpgradeConfig? config]) {
|
||||
final effectiveConfig = config ?? _config;
|
||||
if (effectiveConfig.customToast != null) {
|
||||
effectiveConfig.customToast!(message);
|
||||
|
|
@ -637,7 +716,9 @@ class AppUpgradeSimple {
|
|||
bool _canShowMaterialDialog(BuildContext context) {
|
||||
if (!context.mounted) return false;
|
||||
try {
|
||||
return Localizations.of<MaterialLocalizations>(context, MaterialLocalizations) != null;
|
||||
return Localizations.of<MaterialLocalizations>(
|
||||
context, MaterialLocalizations) !=
|
||||
null;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -669,7 +750,8 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
}
|
||||
|
||||
void onAppLifecycleStateChanged(AppLifecycleState state) {
|
||||
debugPrint('🔄 应用生命周期状态变化: $state, _isWaitingForInstallation=$_isWaitingForInstallation');
|
||||
debugPrint(
|
||||
'🔄 应用生命周期状态变化: $state, _isWaitingForInstallation=$_isWaitingForInstallation');
|
||||
|
||||
if (_isWaitingForInstallation && state == AppLifecycleState.resumed) {
|
||||
debugPrint('⚡ 应用回到前台,检查安装状态');
|
||||
|
|
@ -726,12 +808,14 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
if (!Platform.isAndroid || info.downloadUrl == null) return;
|
||||
if (!mounted) return;
|
||||
|
||||
final hasStorage = await PermissionHelper.checkAndRequestStoragePermission(context: context);
|
||||
final hasStorage = await PermissionHelper.checkAndRequestStoragePermission(
|
||||
context: context);
|
||||
if (!hasStorage) {
|
||||
showToast('缺少存储权限,无法下载');
|
||||
return;
|
||||
}
|
||||
await PermissionHelper.checkAndRequestNotificationPermission(context: context);
|
||||
await PermissionHelper.checkAndRequestNotificationPermission(
|
||||
context: context);
|
||||
|
||||
setState(() {
|
||||
_isDownloading = true;
|
||||
|
|
@ -794,7 +878,9 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
|
||||
if (config.requireInstallPermission) {
|
||||
debugPrint('🔐 检查安装权限(配置要求)');
|
||||
final hasPermission = await PermissionHelper.checkAndRequestInstallPermission(context: context);
|
||||
final hasPermission =
|
||||
await PermissionHelper.checkAndRequestInstallPermission(
|
||||
context: context);
|
||||
if (!hasPermission) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
|
|
@ -862,7 +948,8 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
|
||||
Future<void> _checkInstallationResult() async {
|
||||
if (!mounted || !_isWaitingForInstallation) {
|
||||
debugPrint('跳过安装结果检查: mounted=$mounted, _isWaitingForInstallation=$_isWaitingForInstallation');
|
||||
debugPrint(
|
||||
'跳过安装结果检查: mounted=$mounted, _isWaitingForInstallation=$_isWaitingForInstallation');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -871,23 +958,30 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
try {
|
||||
final appInfo = await _plugin.getAppInfo();
|
||||
final currentVersion = appInfo['version'] ?? '';
|
||||
final currentBuildNumber = int.tryParse(appInfo['buildNumber'] ?? '0') ?? 0;
|
||||
final currentBuildNumber =
|
||||
int.tryParse(appInfo['buildNumber'] ?? '0') ?? 0;
|
||||
|
||||
debugPrint('📱 当前版本: $currentVersion, 构建号: $currentBuildNumber');
|
||||
debugPrint('🎯 目标版本: ${info.versionName}, 构建号: ${info.versionBuildNumber}');
|
||||
debugPrint(
|
||||
'🎯 目标版本: ${info.versionName}, 构建号: ${info.versionBuildNumber}');
|
||||
|
||||
bool isUpdated = false;
|
||||
if (info.versionBuildNumber > 0) {
|
||||
if (currentBuildNumber > info.versionBuildNumber) {
|
||||
isUpdated = true;
|
||||
} else {
|
||||
isUpdated = AppUpgradeSimple.compareVersionStrings(currentVersion, info.versionName) > 0;
|
||||
isUpdated = AppUpgradeSimple.compareVersionStrings(
|
||||
currentVersion, info.versionName) >
|
||||
0;
|
||||
}
|
||||
debugPrint(
|
||||
'📊 构建号比较: $currentBuildNumber vs ${info.versionBuildNumber}, 版本比较(如需): ${info.versionName} -> $isUpdated');
|
||||
} else {
|
||||
isUpdated = AppUpgradeSimple.compareVersionStrings(currentVersion, info.versionName) > 0;
|
||||
debugPrint('📊 版本号比较: $currentVersion vs ${info.versionName} = $isUpdated');
|
||||
isUpdated = AppUpgradeSimple.compareVersionStrings(
|
||||
currentVersion, info.versionName) >
|
||||
0;
|
||||
debugPrint(
|
||||
'📊 版本号比较: $currentVersion vs ${info.versionName} = $isUpdated');
|
||||
}
|
||||
|
||||
if (isUpdated) {
|
||||
|
|
@ -941,7 +1035,9 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
}
|
||||
|
||||
if (_statusText == '权限被拒绝' && config.requireInstallPermission) {
|
||||
final hasPermission = await PermissionHelper.checkAndRequestInstallPermission(context: context);
|
||||
final hasPermission =
|
||||
await PermissionHelper.checkAndRequestInstallPermission(
|
||||
context: context);
|
||||
if (!hasPermission) {
|
||||
showToast('仍未获得安装权限,请在设置中手动开启');
|
||||
return;
|
||||
|
|
@ -1010,7 +1106,8 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
if (info.isForceUpdate) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.error,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
|
|
@ -1050,9 +1147,10 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
child: _buildInfoChip(
|
||||
context,
|
||||
icon: Icons.update,
|
||||
label: '安装版本',
|
||||
label: '已安装版本',
|
||||
// 显示版本号
|
||||
value: '${info.currentVersionName} +${info.currentBuildNumber}',
|
||||
value:
|
||||
'${info.currentVersionName} +${info.currentBuildNumber}',
|
||||
colorScheme: colorScheme,
|
||||
),
|
||||
),
|
||||
|
|
@ -1116,8 +1214,8 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
|
||||
Widget _buildUpdateContent(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final changeItems =
|
||||
info.updateContent.split(RegExp(r'\r?\n')).map((e) => e.trim()).where((e) => e.isNotEmpty).toList();
|
||||
final parsedContent =
|
||||
AppUpgradeSimple.parseUpdateContent(info.updateContent);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
|
@ -1154,7 +1252,7 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: changeItems.isEmpty
|
||||
child: !parsedContent.hasBodyItems && !parsedContent.hasHeader
|
||||
? _buildRichText(
|
||||
info.updateContent,
|
||||
colorScheme,
|
||||
|
|
@ -1162,32 +1260,48 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: changeItems.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final line = entry.value;
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: EdgeInsets.only(
|
||||
bottom: index < changeItems.length - 1 ? 8 : 0,
|
||||
children: [
|
||||
if (parsedContent.hasHeader) ...[
|
||||
_buildRichText(
|
||||
parsedContent.header!,
|
||||
colorScheme,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 6),
|
||||
width: 4,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primary,
|
||||
shape: BoxShape.circle,
|
||||
if (parsedContent.hasBodyItems)
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
...parsedContent.bodyItems.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final item = entry.value;
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: EdgeInsets.only(
|
||||
bottom: index < parsedContent.bodyItems.length - 1
|
||||
? 8
|
||||
: 0,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!item.hasLeadingMarker) ...[
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 6),
|
||||
width: 4,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
Expanded(
|
||||
child: _buildRichText(item.text, colorScheme),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: _buildRichText(line, colorScheme)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -1271,10 +1385,12 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
|
||||
// [高亮]
|
||||
if (currentChar == '[') {
|
||||
final innerResult = _parseRichTextInternal(text, styles, index + 1, ']');
|
||||
final innerResult =
|
||||
_parseRichTextInternal(text, styles, index + 1, ']');
|
||||
if (innerResult.closed) {
|
||||
flushBuffer();
|
||||
final innerText = text.substring(index + 1, innerResult.nextIndex - 1);
|
||||
final innerText =
|
||||
text.substring(index + 1, innerResult.nextIndex - 1);
|
||||
spans.addAll(_applyStyleToSpans(
|
||||
innerResult.spans,
|
||||
styles.highlightStyle,
|
||||
|
|
@ -1310,10 +1426,12 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
}
|
||||
// **粗体**
|
||||
else if (text.startsWith('**', index)) {
|
||||
final innerResult = _parseRichTextInternal(text, styles, index + 2, '**');
|
||||
final innerResult =
|
||||
_parseRichTextInternal(text, styles, index + 2, '**');
|
||||
if (innerResult.closed) {
|
||||
flushBuffer();
|
||||
final innerText = text.substring(index + 2, innerResult.nextIndex - 2);
|
||||
final innerText =
|
||||
text.substring(index + 2, innerResult.nextIndex - 2);
|
||||
spans.addAll(_applyStyleToSpans(
|
||||
innerResult.spans,
|
||||
styles.boldStyle,
|
||||
|
|
@ -1330,10 +1448,12 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
}
|
||||
// __斜体__
|
||||
else if (text.startsWith('__', index)) {
|
||||
final innerResult = _parseRichTextInternal(text, styles, index + 2, '__');
|
||||
final innerResult =
|
||||
_parseRichTextInternal(text, styles, index + 2, '__');
|
||||
if (innerResult.closed) {
|
||||
flushBuffer();
|
||||
final innerText = text.substring(index + 2, innerResult.nextIndex - 2);
|
||||
final innerText =
|
||||
text.substring(index + 2, innerResult.nextIndex - 2);
|
||||
spans.addAll(_applyStyleToSpans(
|
||||
innerResult.spans,
|
||||
styles.italicStyle,
|
||||
|
|
@ -1358,7 +1478,8 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
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) {
|
||||
return [
|
||||
TextSpan(
|
||||
|
|
@ -1372,8 +1493,10 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
}
|
||||
|
||||
TextSpan _mergeTextSpanStyle(TextSpan span, TextStyle style) {
|
||||
final mergedChildren =
|
||||
span.children?.map((child) => child is TextSpan ? _mergeTextSpanStyle(child, style) : child).toList();
|
||||
final mergedChildren = span.children
|
||||
?.map((child) =>
|
||||
child is TextSpan ? _mergeTextSpanStyle(child, style) : child)
|
||||
.toList();
|
||||
|
||||
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 &&
|
||||
!_isDownloading &&
|
||||
!_isInstalling &&
|
||||
|
|
@ -1488,7 +1612,8 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primaryContainer.withOpacity(0.2),
|
||||
color: colorScheme.primaryContainer
|
||||
.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: colorScheme.primary.withOpacity(0.3),
|
||||
|
|
@ -1517,7 +1642,8 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
'系统将自动检测安装结果',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: colorScheme.onSurface.withOpacity(0.7),
|
||||
color: colorScheme.onSurface
|
||||
.withOpacity(0.7),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
|
@ -1534,9 +1660,11 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
child: ElevatedButton.icon(
|
||||
onPressed: _retryInstall,
|
||||
icon: Icon(_getRetryButtonIcon(), size: 16),
|
||||
label: Text(_getRetryButtonText(), style: const TextStyle(fontSize: 12)),
|
||||
label: Text(_getRetryButtonText(),
|
||||
style: const TextStyle(fontSize: 12)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
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),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
|
|
@ -1620,7 +1749,9 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
return Icons.cancel_outlined;
|
||||
} else if (_statusText == '安装超时' || _statusText == '检测失败') {
|
||||
return Icons.schedule;
|
||||
} else if (_statusText == '安装失败' || _statusText == '安装异常' || _statusText == '权限被拒绝') {
|
||||
} else if (_statusText == '安装失败' ||
|
||||
_statusText == '安装异常' ||
|
||||
_statusText == '权限被拒绝') {
|
||||
return Icons.error_outline;
|
||||
} else if (_downloadProgress >= 1.0) {
|
||||
return Icons.check_circle_outline;
|
||||
|
|
@ -1646,7 +1777,9 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
return colorScheme.secondary.withOpacity(0.8);
|
||||
} else if (_statusText == '安装超时' || _statusText == '检测失败') {
|
||||
return colorScheme.secondary.withOpacity(0.7);
|
||||
} else if (_statusText == '安装失败' || _statusText == '安装异常' || _statusText == '权限被拒绝') {
|
||||
} else if (_statusText == '安装失败' ||
|
||||
_statusText == '安装异常' ||
|
||||
_statusText == '权限被拒绝') {
|
||||
return colorScheme.error;
|
||||
} else if (_downloadProgress >= 1.0) {
|
||||
return colorScheme.tertiary;
|
||||
|
|
@ -1698,7 +1831,10 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
return colorScheme.secondary;
|
||||
} else if (_statusText == '安装失败' || _statusText == '安装异常') {
|
||||
return colorScheme.error;
|
||||
} else if (_statusText == '安装超时' || _statusText == '安装被取消' || _statusText == '检测失败' || _statusText == '等待安装中') {
|
||||
} else if (_statusText == '安装超时' ||
|
||||
_statusText == '安装被取消' ||
|
||||
_statusText == '检测失败' ||
|
||||
_statusText == '等待安装中') {
|
||||
return colorScheme.secondary.withOpacity(0.8);
|
||||
} else if (_statusText == '请完成安装') {
|
||||
return colorScheme.secondary;
|
||||
|
|
@ -1758,7 +1894,8 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
|
||||
Future<void> _handleIosAction(BuildContext context) async {
|
||||
if (info.appStoreUrl != null) {
|
||||
final success = await _plugin.goToAppStore(info.appStoreUrl!, context: context);
|
||||
final success =
|
||||
await _plugin.goToAppStore(info.appStoreUrl!, context: context);
|
||||
if (!success) {
|
||||
showToast('无法打开App Store,请稍后重试');
|
||||
}
|
||||
|
|
@ -1822,7 +1959,9 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
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');
|
||||
|
||||
|
|
@ -1843,7 +1982,8 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
final appInfo = await _plugin.getAppInfo();
|
||||
final pkg = appInfo['packageName'] ?? '';
|
||||
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) {
|
||||
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;
|
||||
|
||||
final choice = await showModalBottomSheet<AppUpgradeMethod>(
|
||||
|
|
@ -1897,7 +2038,9 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
Expanded(
|
||||
child: Text('选择更新方式',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold))),
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold))),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
|
|
@ -1913,23 +2056,30 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
if (availableMethods.contains(AppUpgradeMethod.market))
|
||||
ListTile(
|
||||
leading: const Icon(Icons.storefront_outlined),
|
||||
title: const Text('前往应用市场更新', style: TextStyle(fontSize: 16)),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title:
|
||||
const Text('前往应用市场更新', style: TextStyle(fontSize: 16)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
onTap: () => Navigator.of(ctx).pop(AppUpgradeMethod.market),
|
||||
),
|
||||
if (availableMethods.contains(AppUpgradeMethod.inApp))
|
||||
ListTile(
|
||||
leading: const Icon(Icons.system_update),
|
||||
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),
|
||||
),
|
||||
if (availableMethods.contains(AppUpgradeMethod.browser))
|
||||
ListTile(
|
||||
leading: const Icon(Icons.download_for_offline_outlined),
|
||||
title: const Text('前往浏览器下载安装包', textAlign: TextAlign.left, style: TextStyle(fontSize: 16)),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
onTap: () => Navigator.of(ctx).pop(AppUpgradeMethod.browser),
|
||||
title: const Text('前往浏览器下载安装包',
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(fontSize: 16)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
onTap: () =>
|
||||
Navigator.of(ctx).pop(AppUpgradeMethod.browser),
|
||||
),
|
||||
const Divider(height: 24),
|
||||
SizedBox(
|
||||
|
|
@ -1938,7 +2088,8 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
foregroundColor:
|
||||
Theme.of(context).textTheme.bodyLarge?.color,
|
||||
elevation: 2,
|
||||
shadowColor: Colors.grey.withOpacity(0.5),
|
||||
shape: RoundedRectangleBorder(
|
||||
|
|
@ -1947,7 +2098,9 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
),
|
||||
),
|
||||
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();
|
||||
}
|
||||
|
||||
class _SimpleUpgradeDialogState extends State<_SimpleUpgradeDialog> with _UpgradeDialogLogic, WidgetsBindingObserver {
|
||||
class _SimpleUpgradeDialogState extends State<_SimpleUpgradeDialog>
|
||||
with _UpgradeDialogLogic, WidgetsBindingObserver {
|
||||
@override
|
||||
UpgradeInfo get info => widget.info;
|
||||
@override
|
||||
|
|
@ -2068,11 +2222,13 @@ class _SimpleUpgradeDialogState extends State<_SimpleUpgradeDialog> with _Upgrad
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
_buildVersionInfoCard(context, Theme.of(context).colorScheme),
|
||||
_buildVersionInfoCard(
|
||||
context, Theme.of(context).colorScheme),
|
||||
const SizedBox(height: 16),
|
||||
_buildUpdateContent(context),
|
||||
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();
|
||||
}
|
||||
|
||||
class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog> with _UpgradeDialogLogic, WidgetsBindingObserver {
|
||||
class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog>
|
||||
with _UpgradeDialogLogic, WidgetsBindingObserver {
|
||||
@override
|
||||
UpgradeInfo get info => widget.info;
|
||||
@override
|
||||
void Function(String) get showToast =>
|
||||
(message) => AppUpgradeSimple.instance._showToast(message, context, widget.config);
|
||||
void Function(String) get showToast => (message) =>
|
||||
AppUpgradeSimple.instance._showToast(message, context, widget.config);
|
||||
@override
|
||||
bool get autoInstall => widget.autoInstall;
|
||||
@override
|
||||
|
|
@ -2175,7 +2332,8 @@ class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog> with _UpgradeD
|
|||
const Expanded(
|
||||
child: Text(
|
||||
'发现新版本',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
|
||||
style: TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.w600),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
|
|
@ -2190,11 +2348,13 @@ class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog> with _UpgradeD
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
_buildVersionInfoCard(context, Theme.of(context).colorScheme),
|
||||
_buildVersionInfoCard(
|
||||
context, Theme.of(context).colorScheme),
|
||||
const SizedBox(height: 16),
|
||||
_buildUpdateContent(context),
|
||||
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(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black87,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
|
|
@ -2324,3 +2485,105 @@ class _RichTextParseResult {
|
|||
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
|
@ -29,16 +30,22 @@ class UpgradeAuxiliaryUtils {
|
|||
}
|
||||
}
|
||||
|
||||
Timer? _updateLaterTimer;
|
||||
|
||||
static final UpgradeAuxiliaryUtils _instance = UpgradeAuxiliaryUtils._();
|
||||
|
||||
/// 获取 UpgradeService 单例
|
||||
static UpgradeAuxiliaryUtils get instance => _instance;
|
||||
|
||||
/// 启动版本检查
|
||||
/// reUpdateLater 是否恢复重新更新
|
||||
/// resetTimeMinute 重置时间 默认30分钟
|
||||
Future<void> initiateVersionCheck(
|
||||
BuildContext context, {
|
||||
required Future<AppUpgradeVersion?> Function(int upType) future,
|
||||
UpgradeConfig? config,
|
||||
bool reUpdateLater = true,
|
||||
int resetTimeMinute = 30,
|
||||
bool? showNoUpdateToast,
|
||||
bool? autoInstall,
|
||||
VoidCallback? onComplete,
|
||||
|
|
@ -72,6 +79,14 @@ class UpgradeAuxiliaryUtils {
|
|||
},
|
||||
onUpdateLater: () {
|
||||
_updateLater = true;
|
||||
|
||||
/// 30分钟 后重置,可以重新弹出
|
||||
if (reUpdateLater) {
|
||||
_updateLaterTimer?.cancel();
|
||||
_updateLaterTimer = Timer(Duration(seconds: 60 * resetTimeMinute), () {
|
||||
_updateLater = false;
|
||||
});
|
||||
}
|
||||
onUpdateLater?.call();
|
||||
},
|
||||
config: config ?? (kDebugMode ? UpgradeConfig.development : UpgradeConfig.production),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
name: yx_app_upgrade_flutter
|
||||
description: "A universal, robust, and high-performance Flutter app upgrade plugin with smart update features."
|
||||
version: 1.0.3
|
||||
version: 1.0.5
|
||||
homepage: https://github.com/yourusername/yx_app_upgrade_flutter
|
||||
|
||||
environment:
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue