diff --git a/dist/manifest.json b/dist/manifest.json new file mode 100644 index 00000000..f1c5bc91 --- /dev/null +++ b/dist/manifest.json @@ -0,0 +1,6 @@ +{ + "version": "1.1.4", + "md5": "eed99d4cdba060904454826051a04f0a", + "targetVersionCode": 2, + "abi": "arm64-v8a" +} diff --git a/lib/core/consts/http_api_consts.dart b/lib/core/consts/http_api_consts.dart index 54c3010b..819bab41 100644 --- a/lib/core/consts/http_api_consts.dart +++ b/lib/core/consts/http_api_consts.dart @@ -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"; /// 账号相关 // 登录 diff --git a/lib/core/update/update_cubit.dart b/lib/core/update/update_cubit.dart new file mode 100644 index 00000000..39aee886 --- /dev/null +++ b/lib/core/update/update_cubit.dart @@ -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 { + final VersionCheckService _versionService; + final Logger _logger; + + UpdateCubit(this._versionService) + : _logger = Logger(), + super(UpdateInitial()); + + /// 检查版本更新 + Future 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 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 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()); + } +} diff --git a/lib/core/update/update_dialog.dart b/lib/core/update/update_dialog.dart new file mode 100644 index 00000000..cc12771a --- /dev/null +++ b/lib/core/update/update_dialog.dart @@ -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( + 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().cancelUpdate(); + Navigator.of(context).pop(); + }, + child: const Text('稍后'), + ), + BlocBuilder( + 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().applyPatch( + versionInfo.patchUrl!, + versionInfo.version, + versionInfo.versionCode, + md5: versionInfo.patchMd5, + ); + } else if (versionInfo.apkUrl != null) { + // 整包更新 + context.read().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('立即重启'), + ), + ], + ), + ); + } +} diff --git a/lib/core/update/update_state.dart b/lib/core/update/update_state.dart new file mode 100644 index 00000000..72ffaa2c --- /dev/null +++ b/lib/core/update/update_state.dart @@ -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); +} diff --git a/lib/core/update/version_check_service.dart b/lib/core/update/version_check_service.dart new file mode 100644 index 00000000..a5c724ce --- /dev/null +++ b/lib/core/update/version_check_service.dart @@ -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 json) { + // 兼容嵌套结构:如果存在 patch 对象,则从中提取 url 和 md5 + final patchData = json['patch'] as Map?; + final fullApkData = json['fullApk'] as Map?; + + 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 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 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 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 rollback() async { + try { + _logger.i('🔄 执行回滚操作'); + await FlutterPatcher.rollback(); + _logger.i('✅ 回滚成功,下次冷启动生效'); + } catch (e) { + _logger.e('❌ 回滚失败: $e'); + } + } + + // 🔥 清除补丁版本信息(整包更新后调用) + Future 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 _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> _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}; + } + } +} diff --git a/lib/features/auth/presentation/pages/login_page.dart b/lib/features/auth/presentation/pages/login_page.dart index 5ec1f686..2f053160 100644 --- a/lib/features/auth/presentation/pages/login_page.dart +++ b/lib/features/auth/presentation/pages/login_page.dart @@ -66,8 +66,8 @@ class _LoginPageState extends State { 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 { 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)), diff --git a/lib/main.dart b/lib/main.dart index b09d40d0..bcda0e71 100644 --- a/lib/main.dart +++ b/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 navigatorKey = GlobalKey(); + void main() async { - // 1. 使用 runZonedGuarded 捕获所有未处理的异步错误 runZonedGuarded( () async { WidgetsFlutterBinding.ensureInitialized(); - - // 2. 初始化依赖注入 (这里面应包含 ILoggerService 的注册) + + await FlutterPatcher.init(); await init(); - - // 3. 从 sl 中获取 logger 实例并初始化 Sentry final logger = sl(); await logger.init(); - - // 4. 设置全局 Bloc 观察者,自动上报 Bloc 错误 Bloc.observer = AppBlocObserver(logger); - runApp(const MyApp()); }, (error, stack) { - // 5. 捕获顶级错误并上报 sl().captureException(error, stackTrace: stack); }, ); @@ -52,37 +54,28 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - // 在 runApp 之前调用 appStarted,确保 GoRouter 初始化时能获取到正确的初始状态 sl().appStarted(); final deviceStatusBloc = sl(); final localeCubit = sl(); return MultiBlocProvider( providers: [ - // 核心修正:在这里提供 AuthCubit BlocProvider(create: (_) => sl()), - BlocProvider(create: (_) => sl()), BlocProvider(create: (_) => sl()), BlocProvider( create: (_) { - // 增加一个健壮性检查 final user = sl().state.user; final devicesCubit = sl(); - if (user != null) { - devicesCubit.fetchAllDevices(user.username); - } + if (user != null) devicesCubit.fetchAllDevices(user.username); return devicesCubit; }, ), - BlocProvider.value( - value: deviceStatusBloc, - ), - // 🔥 语言管理 Cubit + BlocProvider.value(value: deviceStatusBloc), BlocProvider.value(value: localeCubit), BlocProvider.value(value: sl()), BlocProvider(create: (_) => sl()), - // 其他 Cubit... + BlocProvider(create: (_) => UpdateCubit(VersionCheckService())), ], child: BlocBuilder( 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(), 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 连接...'); - _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; + if (state == AppLifecycleState.resumed) { + debugPrint('📱 [生命周期] 应用恢复到前台'); + _handleResume(); } } - // 🔥 处理恢复到前台的逻辑 Future _handleResume() async { try { final authCubit = context.read(); - - // 检查当前是否已登录 - 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) { - return widget.child!; + 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().checkUpdate(); + }); } -} \ No newline at end of file + + @override + Widget build(BuildContext context) { + return BlocConsumer( + 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().cancelUpdate(), + child: const Text('稍后'), + ), + const SizedBox(width: 10), + ElevatedButton( + onPressed: () { + final info = state.versionInfo; + if (info.patchUrl != null) { + // 🔥 执行差量更新 + context.read().applyPatch( + info.patchUrl!, + info.version, + info.versionCode, + md5: info.patchMd5, + ); + } else if (info.apkUrl != null) { + // 🔥 执行整包更新 + context.read().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().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().checkUpdate(), + // ), + ], + ), + ), + ), + ), + ], + ); + }, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index b6bc79ee..d4d5ee41 100644 --- a/pubspec.lock +++ b/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: diff --git a/pubspec.yaml b/pubspec.yaml index 9b3f1ea6..27670875 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 # 可选:场站配置文件