添加修复大包更新安装覆盖的-待测试

添加修复了更新接口的令狐适配
添加了新的服务端地址-
This commit is contained in:
2026-05-27 16:05:21 +08:00
parent afef5c4ef1
commit 938aac2db9
10 changed files with 913 additions and 88 deletions

6
dist/manifest.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"version": "1.1.4",
"md5": "eed99d4cdba060904454826051a04f0a",
"targetVersionCode": 2,
"abi": "arm64-v8a"
}

View File

@@ -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";
/// 账号相关
// 登录

View 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());
}
}

View 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('立即重启'),
),
],
),
);
}
}

View 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);
}

View 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};
}
}
}

View File

@@ -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)),

View File

@@ -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(),
// ),
],
),
),
),
),
],
);
},
);
}
}

View File

@@ -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:

View File

@@ -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 # 可选:场站配置文件