添加修复大包更新安装覆盖的-待测试
添加修复了更新接口的令狐适配 添加了新的服务端地址-
This commit is contained in:
6
dist/manifest.json
vendored
Normal file
6
dist/manifest.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"version": "1.1.4",
|
||||
"md5": "eed99d4cdba060904454826051a04f0a",
|
||||
"targetVersionCode": 2,
|
||||
"abi": "arm64-v8a"
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
class HttpApiConsts {
|
||||
static const String baseUrl = "http://1.95.137.212:8081";
|
||||
// static const String baseUrl = "http://8.159.134.0:8012"; // 旧地址
|
||||
static const String baseUrl = "http://1.95.137.212:59015";
|
||||
|
||||
/// 账号相关
|
||||
// 登录
|
||||
|
||||
120
lib/core/update/update_cubit.dart
Normal file
120
lib/core/update/update_cubit.dart
Normal file
@@ -0,0 +1,120 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:open_file/open_file.dart';
|
||||
import 'version_check_service.dart';
|
||||
import 'update_state.dart';
|
||||
|
||||
/// 更新 Cubit
|
||||
class UpdateCubit extends Cubit<UpdateState> {
|
||||
final VersionCheckService _versionService;
|
||||
final Logger _logger;
|
||||
|
||||
UpdateCubit(this._versionService)
|
||||
: _logger = Logger(),
|
||||
super(UpdateInitial());
|
||||
|
||||
/// 检查版本更新
|
||||
Future<void> checkUpdate() async {
|
||||
emit(UpdateChecking());
|
||||
|
||||
try {
|
||||
// 从 pubspec.yaml 读取版本信息
|
||||
final String versionString = await rootBundle.loadString('pubspec.yaml');
|
||||
final versionMatch = RegExp(r'version:\s*(\d+\.\d+\.\d+)\+(\d+)').firstMatch(versionString);
|
||||
|
||||
final currentVersion = versionMatch?.group(1) ?? '1.0.0';
|
||||
final currentVersionCode = int.tryParse(versionMatch?.group(2) ?? '1') ?? 1;
|
||||
|
||||
_logger.i('📱 当前应用版本: $currentVersion ($currentVersionCode)');
|
||||
|
||||
// 检查更新
|
||||
final versionInfo = await _versionService.checkUpdate(
|
||||
currentVersion: currentVersion,
|
||||
currentVersionCode: currentVersionCode,
|
||||
);
|
||||
|
||||
if (versionInfo == null) {
|
||||
_logger.i('✅ 已是最新版本');
|
||||
emit(UpdateUpToDate());
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.i('🔄 发现新版本: ${versionInfo.version}');
|
||||
_logger.i('📦 更新类型: ${versionInfo.updateType}');
|
||||
_logger.i('⚠️ 强制更新: ${versionInfo.forceUpdate}');
|
||||
|
||||
emit(UpdateAvailable(versionInfo));
|
||||
} catch (e) {
|
||||
_logger.e('❌ 检查更新失败: $e');
|
||||
emit(UpdateFailure('检查更新失败: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// 应用差量补丁
|
||||
Future<void> applyPatch(String patchUrl, String version, int targetVersionCode, {String? md5}) async {
|
||||
emit(const UpdateDownloading(0.0, isPatch: true));
|
||||
|
||||
try {
|
||||
_logger.i('⬇️ 开始下载并应用补丁');
|
||||
|
||||
final success = await _versionService.applyPatch(
|
||||
patchUrl: patchUrl,
|
||||
version: version,
|
||||
targetVersionCode: targetVersionCode,
|
||||
md5: md5,
|
||||
onProgress: (progress) {
|
||||
// 🔥 实时更新下载进度
|
||||
emit(UpdateDownloading(progress, isPatch: true));
|
||||
},
|
||||
);
|
||||
|
||||
if (success) {
|
||||
emit(const UpdateInstalling(isPatch: true));
|
||||
emit(UpdateSuccess());
|
||||
} else {
|
||||
emit(const UpdateFailure('补丁应用失败'));
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.e('❌ 应用补丁异常: $e');
|
||||
emit(UpdateFailure('应用补丁失败: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// 下载并安装完整 APK
|
||||
Future<void> downloadAndInstallApk(String apkUrl) async {
|
||||
emit(const UpdateDownloading(0.0, isPatch: false));
|
||||
|
||||
try {
|
||||
_logger.i('⬇️ 开始下载完整 APK');
|
||||
|
||||
final apkPath = await _versionService.downloadApk(apkUrl, (progress) {
|
||||
emit(UpdateDownloading(progress, isPatch: false));
|
||||
});
|
||||
|
||||
if (apkPath != null) {
|
||||
emit(const UpdateInstalling(isPatch: false));
|
||||
|
||||
// 🔥 整包更新后,清除补丁版本记录
|
||||
await _versionService.clearPatchVersionInfo();
|
||||
|
||||
// 打开 APK 文件,触发系统安装界面
|
||||
final result = await OpenFile.open(apkPath);
|
||||
_logger.i('📦 安装结果: ${result.message}');
|
||||
|
||||
emit(UpdateSuccess());
|
||||
} else {
|
||||
emit(const UpdateFailure('APK 下载失败'));
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.e('❌ 下载安装 APK 异常: $e');
|
||||
emit(UpdateFailure('下载安装 APK 失败: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// 取消更新
|
||||
void cancelUpdate() {
|
||||
_logger.i('❌ 用户取消更新');
|
||||
emit(UpdateInitial());
|
||||
}
|
||||
}
|
||||
141
lib/core/update/update_dialog.dart
Normal file
141
lib/core/update/update_dialog.dart
Normal file
@@ -0,0 +1,141 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'update_cubit.dart';
|
||||
import 'update_state.dart';
|
||||
import 'version_check_service.dart';
|
||||
|
||||
/// 更新对话框
|
||||
class UpdateDialog extends StatelessWidget {
|
||||
final AppVersionInfo versionInfo;
|
||||
|
||||
const UpdateDialog({super.key, required this.versionInfo});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<UpdateCubit, UpdateState>(
|
||||
listener: (context, state) {
|
||||
if (state is UpdateSuccess) {
|
||||
// 更新成功,关闭对话框
|
||||
Navigator.of(context).pop();
|
||||
|
||||
if (versionInfo.updateType == 'patch') {
|
||||
// 差量更新需要重启
|
||||
_showRestartDialog(context);
|
||||
}
|
||||
// 整包更新会自动打开安装界面,不需要额外操作
|
||||
} else if (state is UpdateFailure) {
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('更新失败: ${state.error}')),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: AlertDialog(
|
||||
title: const Text('发现新版本'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('版本号: ${versionInfo.version}'),
|
||||
const SizedBox(height: 8),
|
||||
Text('更新类型: ${versionInfo.updateType == "patch" ? "差量更新" : "整包更新"}'),
|
||||
const SizedBox(height: 8),
|
||||
if (versionInfo.forceUpdate)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Text(
|
||||
'⚠️ 强制更新',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text('更新内容:', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
Text(versionInfo.updateDesc.isEmpty ? '优化用户体验,修复已知问题' : versionInfo.updateDesc),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
if (!versionInfo.forceUpdate)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.read<UpdateCubit>().cancelUpdate();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('稍后'),
|
||||
),
|
||||
BlocBuilder<UpdateCubit, UpdateState>(
|
||||
builder: (context, state) {
|
||||
if (state is UpdateDownloading) {
|
||||
return SizedBox(
|
||||
width: 60,
|
||||
height: 60,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
value: state.progress,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
Text('${(state.progress * 100).toInt()}%'),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else if (state is UpdateInstalling) {
|
||||
return const Text('安装中...');
|
||||
} else {
|
||||
return ElevatedButton(
|
||||
onPressed: () {
|
||||
if (versionInfo.updateType == 'patch' && versionInfo.patchUrl != null) {
|
||||
// 差量更新
|
||||
context.read<UpdateCubit>().applyPatch(
|
||||
versionInfo.patchUrl!,
|
||||
versionInfo.version,
|
||||
versionInfo.versionCode,
|
||||
md5: versionInfo.patchMd5,
|
||||
);
|
||||
} else if (versionInfo.apkUrl != null) {
|
||||
// 整包更新
|
||||
context.read<UpdateCubit>().downloadAndInstallApk(versionInfo.apkUrl!);
|
||||
}
|
||||
},
|
||||
child: const Text('立即更新'),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showRestartDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('更新完成'),
|
||||
content: const Text('差量更新已应用,需要重启应用才能生效。\n是否立即重启?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: const Text('稍后'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop();
|
||||
// TODO: 实现重启逻辑
|
||||
// 可以使用 restart_app 包或退出应用让用户手动打开
|
||||
},
|
||||
child: const Text('立即重启'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
55
lib/core/update/update_state.dart
Normal file
55
lib/core/update/update_state.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
import 'version_check_service.dart';
|
||||
|
||||
/// 更新状态基类
|
||||
abstract class UpdateState {
|
||||
const UpdateState();
|
||||
}
|
||||
|
||||
/// 初始状态
|
||||
class UpdateInitial extends UpdateState {
|
||||
const UpdateInitial();
|
||||
}
|
||||
|
||||
/// 检查中
|
||||
class UpdateChecking extends UpdateState {
|
||||
const UpdateChecking();
|
||||
}
|
||||
|
||||
/// 发现新版本
|
||||
class UpdateAvailable extends UpdateState {
|
||||
final AppVersionInfo versionInfo;
|
||||
|
||||
const UpdateAvailable(this.versionInfo);
|
||||
}
|
||||
|
||||
/// 下载中
|
||||
class UpdateDownloading extends UpdateState {
|
||||
final double progress;
|
||||
final bool isPatch; // true=差量补丁, false=完整APK
|
||||
|
||||
const UpdateDownloading(this.progress, {required this.isPatch});
|
||||
}
|
||||
|
||||
/// 安装中
|
||||
class UpdateInstalling extends UpdateState {
|
||||
final bool isPatch;
|
||||
|
||||
const UpdateInstalling({required this.isPatch});
|
||||
}
|
||||
|
||||
/// 更新成功
|
||||
class UpdateSuccess extends UpdateState {
|
||||
const UpdateSuccess();
|
||||
}
|
||||
|
||||
/// 已是最新版本
|
||||
class UpdateUpToDate extends UpdateState {
|
||||
const UpdateUpToDate();
|
||||
}
|
||||
|
||||
/// 更新失败
|
||||
class UpdateFailure extends UpdateState {
|
||||
final String error;
|
||||
|
||||
const UpdateFailure(this.error);
|
||||
}
|
||||
268
lib/core/update/version_check_service.dart
Normal file
268
lib/core/update/version_check_service.dart
Normal file
@@ -0,0 +1,268 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_patcher/flutter_patcher.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'dart:io';
|
||||
|
||||
/// 应用版本信息模型
|
||||
class AppVersionInfo {
|
||||
final String version;
|
||||
final int versionCode;
|
||||
final String updateType; // "patch" 或 "full"
|
||||
final bool forceUpdate;
|
||||
final String updateDesc;
|
||||
final String? patchUrl;
|
||||
final String? patchMd5;
|
||||
final String? apkUrl;
|
||||
final String? apkMd5;
|
||||
|
||||
AppVersionInfo({
|
||||
required this.version,
|
||||
required this.versionCode,
|
||||
required this.updateType,
|
||||
required this.forceUpdate,
|
||||
required this.updateDesc,
|
||||
this.patchUrl,
|
||||
this.patchMd5,
|
||||
this.apkUrl,
|
||||
this.apkMd5,
|
||||
});
|
||||
|
||||
factory AppVersionInfo.fromJson(Map<String, dynamic> json) {
|
||||
// 兼容嵌套结构:如果存在 patch 对象,则从中提取 url 和 md5
|
||||
final patchData = json['patch'] as Map<String, dynamic>?;
|
||||
final fullApkData = json['fullApk'] as Map<String, dynamic>?;
|
||||
|
||||
return AppVersionInfo(
|
||||
version: json['version'] ?? '',
|
||||
versionCode: json['versionCode'] ?? 0, // 处理 null 情况
|
||||
updateType: json['updateType'] ?? 'full',
|
||||
forceUpdate: json['forceUpdate'] ?? false,
|
||||
updateDesc: json['updateDesc'] ?? '',
|
||||
patchUrl: patchData?['patchUrl'] as String?,
|
||||
patchMd5: patchData?['md5'] as String?,
|
||||
apkUrl: fullApkData?['apkUrl'] as String?,
|
||||
apkMd5: fullApkData?['md5'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 版本检查服务
|
||||
class VersionCheckService {
|
||||
static final VersionCheckService _instance = VersionCheckService._internal();
|
||||
factory VersionCheckService() => _instance;
|
||||
VersionCheckService._internal();
|
||||
|
||||
final Logger _logger = Logger();
|
||||
final Dio _dio = Dio(BaseOptions(
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 10),
|
||||
));
|
||||
|
||||
// 后端接口地址
|
||||
static const String apiUrl = 'http://8.159.134.0:8012/api/update/check';
|
||||
|
||||
// 🔥 补丁版本号文件路径(持久化存储,不会被清理缓存删除)
|
||||
static const String _patchVersionFileName = '.patch_version';
|
||||
|
||||
/// 检查版本更新
|
||||
Future<AppVersionInfo?> checkUpdate({
|
||||
required String currentVersion,
|
||||
required int currentVersionCode,
|
||||
}) async {
|
||||
try {
|
||||
_logger.i('🔍 检查版本更新');
|
||||
_logger.i('📱 APK 自带版本: $currentVersion ($currentVersionCode)');
|
||||
|
||||
// 🔥 从文件读取已应用的补丁版本
|
||||
final patchInfo = await _loadPatchVersionInfo();
|
||||
final appliedPatchVersion = patchInfo['version'] as String?;
|
||||
final appliedPatchVersionCode = patchInfo['versionCode'] as int?;
|
||||
|
||||
// 如果打过补丁,使用补丁的版本信息;否则使用 APK 自带的版本
|
||||
final requestVersion = appliedPatchVersion ?? currentVersion;
|
||||
final requestVersionCode = appliedPatchVersionCode ?? currentVersionCode;
|
||||
|
||||
_logger.i('🚀 请求后端版本: $requestVersion ($requestVersionCode)');
|
||||
|
||||
// 调用后端接口
|
||||
final response = await _dio.get(
|
||||
apiUrl,
|
||||
queryParameters: {
|
||||
'version': requestVersion,
|
||||
'versionCode': requestVersionCode,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data['data'];
|
||||
|
||||
if (data['hasUpdate'] == false) {
|
||||
_logger.i('✅ 已是最新版本');
|
||||
return null;
|
||||
}
|
||||
|
||||
final versionInfo = AppVersionInfo.fromJson(data);
|
||||
|
||||
// 如果已应用此版本补丁,跳过更新
|
||||
if (appliedPatchVersion == versionInfo.version && versionInfo.updateType == 'patch') {
|
||||
_logger.i('✅ 已应用最新版本补丁: ${versionInfo.version}');
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.i('✅ 发现新版本: ${versionInfo.version}');
|
||||
_logger.i('🔄 更新类型: ${versionInfo.updateType}');
|
||||
_logger.i('⚠️ 强制更新: ${versionInfo.forceUpdate}');
|
||||
|
||||
return versionInfo;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e, stack) {
|
||||
_logger.e('❌ 版本检查失败: $e');
|
||||
_logger.e('❌ 堆栈信息: $stack');
|
||||
throw Exception('网络请求失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 下载并应用差量补丁
|
||||
Future<bool> applyPatch({
|
||||
required String patchUrl,
|
||||
required String version,
|
||||
required int targetVersionCode,
|
||||
String? md5,
|
||||
Function(double)? onProgress,
|
||||
}) async {
|
||||
try {
|
||||
_logger.i('⬇️ 开始下载补丁: $patchUrl');
|
||||
|
||||
// 1. 手动下载补丁文件
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final patchPath = '${tempDir.path}/update_patch.so';
|
||||
|
||||
await _dio.download(
|
||||
patchUrl,
|
||||
patchPath,
|
||||
onReceiveProgress: (received, total) {
|
||||
if (total != -1 && onProgress != null) {
|
||||
onProgress(received / total);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
_logger.i('✅ 补丁下载完成,开始应用...');
|
||||
|
||||
// 2. 应用本地补丁文件
|
||||
final result = await FlutterPatcher.applyPatch(
|
||||
PatchInfo(
|
||||
version: version,
|
||||
patchUrl: 'file://$patchPath',
|
||||
targetVersionCode: targetVersionCode,
|
||||
md5: md5 ?? '',
|
||||
),
|
||||
);
|
||||
|
||||
if (result.ok) {
|
||||
_logger.i('✅ 补丁应用成功,需要冷启动生效');
|
||||
// 🔥 保存补丁版本号到文件(持久化存储)
|
||||
await _savePatchVersionInfo(version, targetVersionCode);
|
||||
_logger.i('💾 已保存补丁版本: $version ($targetVersionCode)');
|
||||
} else {
|
||||
_logger.e('❌ 补丁应用失败: ${result.error}');
|
||||
}
|
||||
|
||||
return result.ok;
|
||||
} catch (e) {
|
||||
_logger.e('❌ 应用补丁异常: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 下载完整 APK
|
||||
Future<String?> downloadApk(String apkUrl, Function(double) onProgress) async {
|
||||
try {
|
||||
_logger.i('⬇️ 开始下载 APK: $apkUrl');
|
||||
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final apkPath = '${tempDir.path}/app-update.apk';
|
||||
|
||||
await _dio.download(
|
||||
apkUrl,
|
||||
apkPath,
|
||||
onReceiveProgress: (received, total) {
|
||||
if (total != -1) {
|
||||
onProgress(received / total);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
_logger.i('✅ APK 下载完成: $apkPath');
|
||||
return apkPath;
|
||||
} catch (e) {
|
||||
_logger.e('❌ 下载 APK 失败: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 回滚到内置版本
|
||||
Future<void> rollback() async {
|
||||
try {
|
||||
_logger.i('🔄 执行回滚操作');
|
||||
await FlutterPatcher.rollback();
|
||||
_logger.i('✅ 回滚成功,下次冷启动生效');
|
||||
} catch (e) {
|
||||
_logger.e('❌ 回滚失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 🔥 清除补丁版本信息(整包更新后调用)
|
||||
Future<void> clearPatchVersionInfo() async {
|
||||
try {
|
||||
final directory = await getApplicationSupportDirectory();
|
||||
final file = File('${directory.path}/$_patchVersionFileName');
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
_logger.i('🗑️ 已清除补丁版本记录');
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.e('❌ 清除补丁版本信息失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 🔥 保存补丁版本信息到文件(持久化,不会被清理缓存删除)
|
||||
Future<void> _savePatchVersionInfo(String version, int versionCode) async {
|
||||
try {
|
||||
final directory = await getApplicationSupportDirectory();
|
||||
final file = File('${directory.path}/$_patchVersionFileName');
|
||||
await file.writeAsString('$version|$versionCode');
|
||||
_logger.i('💾 补丁版本已保存到: ${file.path}');
|
||||
} catch (e) {
|
||||
_logger.e('❌ 保存补丁版本信息失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 🔥 从文件加载补丁版本信息
|
||||
Future<Map<String, dynamic>> _loadPatchVersionInfo() async {
|
||||
try {
|
||||
final directory = await getApplicationSupportDirectory();
|
||||
final file = File('${directory.path}/$_patchVersionFileName');
|
||||
if (await file.exists()) {
|
||||
final content = await file.readAsString();
|
||||
final parts = content.split('|');
|
||||
if (parts.length == 2) {
|
||||
final version = parts[0];
|
||||
final versionCode = int.tryParse(parts[1]);
|
||||
if (versionCode != null) {
|
||||
_logger.i('📂 从文件读取补丁版本: $version ($versionCode)');
|
||||
return {'version': version, 'versionCode': versionCode};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {'version': null, 'versionCode': null};
|
||||
} catch (e) {
|
||||
_logger.e('❌ 加载补丁版本信息失败: $e');
|
||||
return {'version': null, 'versionCode': null};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,8 +66,8 @@ class _LoginPageState extends State<LoginPage> {
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/huawei.png',
|
||||
width: 60,
|
||||
height: 60,
|
||||
width: 80,
|
||||
height: 80,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
@@ -89,8 +89,8 @@ class _LoginPageState extends State<LoginPage> {
|
||||
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"账号登录",
|
||||
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.black),
|
||||
"账号登录v1.1.4",
|
||||
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.brown),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text("请填写以下信息以验证身份", style: TextStyle(fontSize: 15, color: Colors.grey)),
|
||||
|
||||
311
lib/main.dart
311
lib/main.dart
@@ -2,13 +2,19 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_patcher/flutter_patcher.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/core/infrastructure/logging/app_bloc_observer.dart';
|
||||
import 'package:maibu_satabot_v2/core/logging/i_logger_service.dart';
|
||||
import 'package:maibu_satabot_v2/core/theme/AppTheme.dart';
|
||||
import 'package:maibu_satabot_v2/core/update/update_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/core/update/version_check_service.dart';
|
||||
import 'package:maibu_satabot_v2/core/update/update_dialog.dart';
|
||||
import 'package:maibu_satabot_v2/core/update/update_state.dart';
|
||||
import 'package:maibu_satabot_v2/features/auth/presentation/bloc/auth_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/main_container/presentation/cubit/tab_config_cubit.dart';
|
||||
@@ -22,26 +28,22 @@ import 'features/auth/presentation/bloc/login_cubit.dart';
|
||||
import 'features/devices/presentation/bloc/device_status_bloc.dart';
|
||||
import 'features/home/presentation/bloc/permission_request_bloc.dart';
|
||||
|
||||
// 🔥 定义全局 Navigator Key
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
void main() async {
|
||||
// 1. 使用 runZonedGuarded 捕获所有未处理的异步错误
|
||||
runZonedGuarded(
|
||||
() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// 2. 初始化依赖注入 (这里面应包含 ILoggerService 的注册)
|
||||
await FlutterPatcher.init();
|
||||
await init();
|
||||
|
||||
// 3. 从 sl 中获取 logger 实例并初始化 Sentry
|
||||
final logger = sl<ILoggerService>();
|
||||
await logger.init();
|
||||
|
||||
// 4. 设置全局 Bloc 观察者,自动上报 Bloc 错误
|
||||
Bloc.observer = AppBlocObserver(logger);
|
||||
|
||||
runApp(const MyApp());
|
||||
},
|
||||
(error, stack) {
|
||||
// 5. 捕获顶级错误并上报
|
||||
sl<ILoggerService>().captureException(error, stackTrace: stack);
|
||||
},
|
||||
);
|
||||
@@ -52,37 +54,28 @@ class MyApp extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 在 runApp 之前调用 appStarted,确保 GoRouter 初始化时能获取到正确的初始状态
|
||||
sl<AuthCubit>().appStarted();
|
||||
final deviceStatusBloc = sl<DeviceStatusBloc>();
|
||||
final localeCubit = sl<LocaleCubit>();
|
||||
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
// 核心修正:在这里提供 AuthCubit
|
||||
BlocProvider<AuthCubit>(create: (_) => sl<AuthCubit>()),
|
||||
|
||||
BlocProvider<AppUserCubit>(create: (_) => sl<AppUserCubit>()),
|
||||
BlocProvider<LoginCubit>(create: (_) => sl<LoginCubit>()),
|
||||
BlocProvider<DevicesCubit>(
|
||||
create: (_) {
|
||||
// 增加一个健壮性检查
|
||||
final user = sl<AppUserCubit>().state.user;
|
||||
final devicesCubit = sl<DevicesCubit>();
|
||||
if (user != null) {
|
||||
devicesCubit.fetchAllDevices(user.username);
|
||||
}
|
||||
if (user != null) devicesCubit.fetchAllDevices(user.username);
|
||||
return devicesCubit;
|
||||
},
|
||||
),
|
||||
BlocProvider<DeviceStatusBloc>.value(
|
||||
value: deviceStatusBloc,
|
||||
),
|
||||
// 🔥 语言管理 Cubit
|
||||
BlocProvider<DeviceStatusBloc>.value(value: deviceStatusBloc),
|
||||
BlocProvider<LocaleCubit>.value(value: localeCubit),
|
||||
BlocProvider<TabConfigCubit>.value(value: sl<TabConfigCubit>()),
|
||||
BlocProvider<PermissionRequestBloc>(create: (_) => sl<PermissionRequestBloc>()),
|
||||
// 其他 Cubit...
|
||||
BlocProvider<UpdateCubit>(create: (_) => UpdateCubit(VersionCheckService())),
|
||||
],
|
||||
child: BlocBuilder<LocaleCubit, Locale>(
|
||||
bloc: localeCubit,
|
||||
@@ -90,10 +83,7 @@ class MyApp extends StatelessWidget {
|
||||
return MaterialApp.router(
|
||||
title: 'Maibu Satabot',
|
||||
locale: locale,
|
||||
supportedLocales: const [
|
||||
Locale('zh', 'CN'),
|
||||
Locale('en', 'US'),
|
||||
],
|
||||
supportedLocales: const [Locale('zh', 'CN'), Locale('en', 'US')],
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
@@ -103,7 +93,7 @@ class MyApp extends StatelessWidget {
|
||||
theme: AppTheme.lightTheme,
|
||||
routerConfig: sl<GoRouter>(),
|
||||
builder: (context, child) {
|
||||
return _LifecycleListener(child: child);
|
||||
return _LifecycleListener(child: _UpdateChecker(child: child!));
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -111,12 +101,10 @@ class MyApp extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
// 🔥 新增:应用生命周期监听器
|
||||
|
||||
class _LifecycleListener extends StatefulWidget {
|
||||
final Widget? child;
|
||||
|
||||
const _LifecycleListener({required this.child});
|
||||
|
||||
@override
|
||||
State<_LifecycleListener> createState() => _LifecycleListenerState();
|
||||
}
|
||||
@@ -136,67 +124,238 @@ class _LifecycleListenerState extends State<_LifecycleListener> with WidgetsBind
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
|
||||
switch (state) {
|
||||
case AppLifecycleState.resumed:
|
||||
// 🔥 从息屏或后台恢复到前台
|
||||
debugPrint('📱 [生命周期] 应用恢复到前台 (resumed),检查 TCP 连接...');
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
debugPrint('📱 [生命周期] 应用恢复到前台');
|
||||
_handleResume();
|
||||
break;
|
||||
|
||||
case AppLifecycleState.inactive:
|
||||
// 应用处于非活动状态(iOS 来电、Android 分屏等)
|
||||
debugPrint('💤 [生命周期] 应用进入非活动状态 (inactive)');
|
||||
break;
|
||||
|
||||
case AppLifecycleState.paused:
|
||||
// 应用进入后台(息屏、按 Home 键等)
|
||||
debugPrint('🌙 [生命周期] 应用进入后台 (paused)');
|
||||
break;
|
||||
|
||||
case AppLifecycleState.detached:
|
||||
// 应用仍然托管但不会显示给用户(例如销毁中的 Activity)
|
||||
debugPrint('🚫 [生命周期] 应用已分离 (detached)');
|
||||
break;
|
||||
|
||||
case AppLifecycleState.hidden:
|
||||
// 应用不可见(Android 14+ 或未来版本)
|
||||
debugPrint('👻 [生命周期] 应用已隐藏 (hidden)');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 🔥 处理恢复到前台的逻辑
|
||||
Future<void> _handleResume() async {
|
||||
try {
|
||||
final authCubit = context.read<AuthCubit>();
|
||||
|
||||
// 检查当前是否已登录
|
||||
if (authCubit.state is! AuthAuthenticated) {
|
||||
debugPrint('⚠️ [生命周期] 用户未登录,跳过 TCP 重连');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查 TCP 连接状态
|
||||
// 注意:这里需要通过 authCubit 访问 tcp 实例
|
||||
// 由于 tcp 是 private 字段,我们需要在 AuthCubit 中暴露一个方法
|
||||
debugPrint('✅ [生命周期] 准备执行 TCP 重连检查...');
|
||||
|
||||
// 延迟一点执行,确保 UI 已经加载完成
|
||||
if (authCubit.state is! AuthAuthenticated) return;
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
// 调用 AuthCubit 的重连方法
|
||||
await authCubit.reconnectAfterResume();
|
||||
|
||||
debugPrint('✅ [生命周期] TCP 重连检查完成');
|
||||
} catch (e, stack) {
|
||||
debugPrint('❌ [生命周期] 重连过程发生错误:$e\n$stack');
|
||||
} catch (e) {
|
||||
debugPrint('❌ [生命周期] 重连错误:$e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.child!;
|
||||
}
|
||||
|
||||
class _UpdateChecker extends StatefulWidget {
|
||||
final Widget child;
|
||||
const _UpdateChecker({required this.child});
|
||||
@override
|
||||
State<_UpdateChecker> createState() => _UpdateCheckerState();
|
||||
}
|
||||
|
||||
class _UpdateCheckerState extends State<_UpdateChecker> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
if (mounted) context.read<UpdateCubit>().checkUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.child!;
|
||||
return BlocConsumer<UpdateCubit, UpdateState>(
|
||||
listener: (context, state) {
|
||||
if (state is UpdateFailure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('更新失败: ${state.error}'), backgroundColor: Colors.red),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return Stack(
|
||||
children: [
|
||||
widget.child,
|
||||
// 🔥 核心修正:直接在 UI 树中渲染更新提示,完全不依赖 Navigator
|
||||
if (state is UpdateAvailable)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.6), // 半透明遮罩
|
||||
child: Center(
|
||||
child: Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 30),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('🎉 发现新版本',
|
||||
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 12),
|
||||
Text('版本号: ${state.versionInfo.version}',
|
||||
style: const TextStyle(fontSize: 16)),
|
||||
Text('更新类型: ${state.versionInfo.updateType == "patch" ? "差量更新" : "整包更新"}',
|
||||
style: const TextStyle(fontSize: 16, color: Colors.grey)),
|
||||
if (state.versionInfo.updateDesc.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
const Text('更新内容:', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text(state.versionInfo.updateDesc),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (!state.versionInfo.forceUpdate)
|
||||
TextButton(
|
||||
onPressed: () => context.read<UpdateCubit>().cancelUpdate(),
|
||||
child: const Text('稍后'),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
final info = state.versionInfo;
|
||||
if (info.patchUrl != null) {
|
||||
// 🔥 执行差量更新
|
||||
context.read<UpdateCubit>().applyPatch(
|
||||
info.patchUrl!,
|
||||
info.version,
|
||||
info.versionCode,
|
||||
md5: info.patchMd5,
|
||||
);
|
||||
} else if (info.apkUrl != null) {
|
||||
// 🔥 执行整包更新
|
||||
context.read<UpdateCubit>().downloadAndInstallApk(info.apkUrl!);
|
||||
}
|
||||
},
|
||||
child: const Text('立即更新'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 🔥 下载/安装进度弹窗
|
||||
if (state is UpdateDownloading || state is UpdateInstalling)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
child: Center(
|
||||
child: Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (state is UpdateDownloading)
|
||||
const Text('⬇️ 正在下载更新...',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
if (state is UpdateInstalling)
|
||||
const Text('🔧 正在应用更新...',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 16),
|
||||
if (state is UpdateDownloading)
|
||||
LinearProgressIndicator(
|
||||
value: state.progress,
|
||||
minHeight: 8,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
if (state is UpdateDownloading)
|
||||
const SizedBox(height: 8),
|
||||
if (state is UpdateDownloading)
|
||||
Text('${(state.progress * 100).toInt()}%',
|
||||
style: const TextStyle(fontSize: 14, color: Colors.grey)),
|
||||
if (state is UpdateInstalling)
|
||||
const CircularProgressIndicator(),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
state is UpdateDownloading ? '请稍候,正在下载补丁文件' : '正在应用补丁,请勿关闭应用',
|
||||
style: const TextStyle(fontSize: 14, color: Colors.grey),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 🔥 更新成功提示
|
||||
if (state is UpdateSuccess)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
child: Center(
|
||||
child: Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.check_circle, color: Colors.green, size: 60),
|
||||
const SizedBox(height: 16),
|
||||
const Text('✅ 更新已完成',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'补丁已成功应用!\n\n请完全关闭 App 后重新打开,\n即可体验新版本功能。',
|
||||
style: TextStyle(fontSize: 15),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
// 提示用户手动重启
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('请从后台划掉 App,然后重新打开'),
|
||||
duration: Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
context.read<UpdateCubit>().cancelUpdate();
|
||||
},
|
||||
child: const Text('我知道了'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 调试面板
|
||||
Positioned(
|
||||
top: 40,
|
||||
right: 10,
|
||||
child: Material(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('状态: ${state.runtimeType.toString().replaceAll('Update', '')}',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 12)),
|
||||
// 🔥 暂时关闭刷新按钮和点击阴影
|
||||
// IconButton(
|
||||
// icon: const Icon(Icons.refresh, color: Colors.white),
|
||||
// onPressed: () => context.read<UpdateCubit>().checkUpdate(),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
76
pubspec.lock
76
pubspec.lock
@@ -434,6 +434,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.7.7+1"
|
||||
flutter_patcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_patcher
|
||||
sha256: e76de4c6469b46730d0b1d7dc7a9852fc09ff175c019739d5ad4e431a961d11a
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.1.2"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -916,6 +924,70 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "9.3.0"
|
||||
open_file:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: open_file
|
||||
sha256: b22decdae85b459eac24aeece48f33845c6f16d278a9c63d75c5355345ca236b
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.5.11"
|
||||
open_file_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: open_file_android
|
||||
sha256: "58141fcaece2f453a9684509a7275f231ac0e3d6ceb9a5e6de310a7dff9084aa"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.6"
|
||||
open_file_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: open_file_ios
|
||||
sha256: a5acd07ba1f304f807a97acbcc489457e1ad0aadff43c467987dd9eef814098f
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
open_file_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: open_file_linux
|
||||
sha256: d189f799eecbb139c97f8bc7d303f9e720954fa4e0fa1b0b7294767e5f2d7550
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.0.5"
|
||||
open_file_mac:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: open_file_mac
|
||||
sha256: cd293f6750de6438ab2390513c99128ade8c974825d4d8128886d1cda8c64d01
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
open_file_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: open_file_platform_interface
|
||||
sha256: "101b424ca359632699a7e1213e83d025722ab668b9fd1412338221bf9b0e5757"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
open_file_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: open_file_web
|
||||
sha256: e3dbc9584856283dcb30aef5720558b90f88036360bd078e494ab80a80130c4f
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.0.4"
|
||||
open_file_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: open_file_windows
|
||||
sha256: d26c31ddf935a94a1a3aa43a23f4fff8a5ff4eea395fe7a8cb819cf55431c875
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.0.3"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -928,10 +1000,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20"
|
||||
sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "9.0.1"
|
||||
version: "8.3.1"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.0.0+1
|
||||
version: 1.1.2+2
|
||||
|
||||
flutter_icons:
|
||||
android: "launcher_icon"
|
||||
@@ -131,6 +131,8 @@ dependencies:
|
||||
|
||||
#qr_code_scanner: ^1.0.1 # 用于扫描二维码
|
||||
vibration: ^3.1.8
|
||||
flutter_patcher: ^0.1.2 # Add flutter_patcher here
|
||||
open_file: ^3.3.2 # 打开文件(安装 APK)
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@@ -174,6 +176,7 @@ flutter:
|
||||
- assets/www/webrtc/
|
||||
- assets/svgs/
|
||||
- assets/languages/
|
||||
- pubspec.yaml
|
||||
#- assets/tiles/ # 声明所有大疆地图资源(通配子目录)
|
||||
#- assets/tiles/metadata.json # 可选:场站配置文件
|
||||
|
||||
|
||||
Reference in New Issue
Block a user