25 changed files with 774 additions and 278 deletions
@ -0,0 +1,9 @@ |
|||
import 'package:get/get.dart'; |
|||
import '../controllers/ota_upgrade_controller.dart'; |
|||
|
|||
class OtaUpgradeBinding extends Bindings { |
|||
@override |
|||
void dependencies() { |
|||
Get.lazyPut<OtaUpgradeController>(() => OtaUpgradeController()); |
|||
} |
|||
} |
|||
@ -0,0 +1,213 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'dart:async'; |
|||
import 'dart:convert'; |
|||
import 'package:get_storage/get_storage.dart'; |
|||
import '../../../data/services/network/api.dart'; |
|||
import '../ota.dart'; |
|||
import 'dart:io'; |
|||
import 'package:dio/dio.dart'; |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:package_info_plus/package_info_plus.dart'; |
|||
import 'package:path_provider/path_provider.dart'; |
|||
import 'package:json_annotation/json_annotation.dart'; |
|||
|
|||
class OtaUpgradeController extends GetxController { |
|||
final Dio _dio = Dio(); |
|||
final RxString latestVersion = ''.obs; |
|||
final RxString currentVersion = ''.obs; |
|||
String updatepackageaddress = ''; |
|||
final RxDouble downloadProgress = 0.0.obs; |
|||
final RxBool isUpdating = false.obs; |
|||
final isUpdateAvailable = true.obs; |
|||
|
|||
final RxDouble progress = 0.0.obs; |
|||
|
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
_getCurrentVersion(); |
|||
} |
|||
|
|||
// 获取当前版本信息 |
|||
Future<void> _getCurrentVersion() async { |
|||
//发送ble获取固件版本 |
|||
var firmwareVersion = "1.1.0"; |
|||
currentVersion.value = firmwareVersion; |
|||
//发送ble获取产品id |
|||
var id = "a2"; |
|||
//发送api请求 |
|||
final response = await Api.getequipmentinfo({ |
|||
"id": id, |
|||
}); |
|||
if (response['info'] != '') { |
|||
print('getLoginToken:进来了'); |
|||
// 根据API设计调整 |
|||
final resp = OtaConfigResp.fromJson(response); |
|||
|
|||
print('成功解析: ${resp.id}, ${resp.factory}'); |
|||
latestVersion.value = resp.systemversion!; |
|||
updatepackageaddress = resp.updatepackageaddress!; |
|||
if (_isNewVersionAvailable(resp.systemversion!)) { |
|||
//需要升级 |
|||
print('需要更新'); |
|||
isUpdateAvailable.value = false; |
|||
} else { |
|||
isUpdateAvailable.value = true; |
|||
//已经是最新版本更新ui,按钮隐藏 |
|||
} |
|||
} else { |
|||
throw const FormatException('API 响应缺少 "info" 字段'); |
|||
} |
|||
} |
|||
|
|||
// 版本比较方法优化 |
|||
bool _isNewVersionAvailable(String latest) { |
|||
try { |
|||
List<int> currentParts = currentVersion.value |
|||
.split('.') |
|||
.map((e) => int.parse(e.trim())) |
|||
.toList(); |
|||
List<int> latestParts = |
|||
latest.split('.').map((e) => int.parse(e.trim())).toList(); |
|||
|
|||
for (int i = 0; i < latestParts.length; i++) { |
|||
int current = (i < currentParts.length) ? currentParts[i] : 0; |
|||
if (latestParts[i] > current) return true; |
|||
if (latestParts[i] < current) return false; |
|||
} |
|||
return false; |
|||
} catch (e) { |
|||
print('版本比较错误: $e'); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
// 版本更新 |
|||
Future<void> checkUpdate() async { |
|||
try { |
|||
if (latestVersion.value != "") |
|||
_showUpdateDialog(latestVersion.value); |
|||
else { |
|||
Get.snackbar('错误', '获取最新版本信息失败'); |
|||
} |
|||
} catch (e) { |
|||
Get.snackbar('错误', '获取版本信息失败'); |
|||
} |
|||
} |
|||
|
|||
// 显示更新对话框 |
|||
void _showUpdateDialog(String latest) { |
|||
Get.dialog( |
|||
AlertDialog( |
|||
title: Text('发现新版本 $latest'), |
|||
content: Text('是否立即更新?'), |
|||
actions: [ |
|||
TextButton( |
|||
onPressed: () => Get.back(), |
|||
child: Text('取消'), |
|||
), |
|||
TextButton( |
|||
onPressed: () { |
|||
Get.back(); |
|||
startDownload(); |
|||
}, |
|||
child: Text('更新'), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
|
|||
// 开始下载 |
|||
Future<void> startDownload() async { |
|||
isUpdating.value = true; |
|||
// 查找最后一个 '/' 的位置 |
|||
int lastIndex = updatepackageaddress.lastIndexOf('/'); |
|||
|
|||
// 截取最后一个 '/' 后面的字符串 |
|||
String fileName = updatepackageaddress.substring(lastIndex + 1); |
|||
String directoryName = "ota_upgrades"; |
|||
|
|||
final dio = Dio(BaseOptions( |
|||
connectTimeout: const Duration(seconds: 5), |
|||
receiveTimeout: Duration.zero, |
|||
sendTimeout: const Duration(seconds: 5), |
|||
headers: {'Accept-Encoding': 'gzip'}, |
|||
)); |
|||
|
|||
Get.snackbar('升级命令已发送', '开始下载...'); |
|||
|
|||
try { |
|||
final Directory? downloadsDir = await getDownloadsDirectory(); |
|||
if (downloadsDir == null) { |
|||
throw FileSystemException('无法访问下载目录'); |
|||
} |
|||
|
|||
// 创建子目录:Download/APP |
|||
final targetDir = Directory('${downloadsDir.path}/$directoryName'); |
|||
if (!await targetDir.exists()) { |
|||
await targetDir.create(recursive: true); |
|||
} |
|||
|
|||
final savePath = '${targetDir.path}/$fileName'; |
|||
print('文件保存路径: $savePath'); // 关键日志 |
|||
|
|||
final options = Options( |
|||
receiveTimeout: Duration.zero, |
|||
followRedirects: true, |
|||
maxRedirects: 5, |
|||
); |
|||
|
|||
DateTime lastUpdate = DateTime.now(); |
|||
await dio.download( |
|||
updatepackageaddress, |
|||
savePath, |
|||
options: options, |
|||
onReceiveProgress: (received, total) { |
|||
final now = DateTime.now(); |
|||
if (now.difference(lastUpdate).inMilliseconds > 1000) { |
|||
lastUpdate = now; |
|||
String progressMsg = total != -1 |
|||
? '${(received / total * 100).toStringAsFixed(1)}%' |
|||
: '已下载:${(received / 1024 / 1024).toStringAsFixed(1)}MB'; |
|||
Get.snackbar( |
|||
'升级进度', |
|||
progressMsg, |
|||
duration: const Duration(seconds: 1), |
|||
); |
|||
} |
|||
}, |
|||
); |
|||
|
|||
// 下载完成后验证文件存在性 |
|||
File file = File(savePath); |
|||
if (await file.exists()) { |
|||
Get.snackbar('下载完成', '文件已保存至: $savePath'); // 明确提示路径 |
|||
} else { |
|||
Get.snackbar('错误', '文件未找到,请检查写入权限'); |
|||
} |
|||
isUpdating.value = false; |
|||
//下载完成,开始升级 |
|||
_installApp(savePath); |
|||
} on DioException catch (e) { |
|||
Get.snackbar('下载失败', '错误: ${e.message}'); |
|||
} on FileSystemException catch (e) { |
|||
Get.snackbar('存储失败', '无法写入文件: ${e.message}'); |
|||
} catch (e) { |
|||
Get.snackbar('未知错误', e.toString()); |
|||
} |
|||
} |
|||
|
|||
// 开始升级 |
|||
void _installApp(String path) async { |
|||
// final File file = File(path); |
|||
// if (await file.exists()) { |
|||
// // OpenResult result = await OpenFile.open(path); |
|||
// // if (result.type != ResultType.done) { |
|||
// // Get.snackbar('错误', '安装失败: ${result.message}'); |
|||
// // } |
|||
// } |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
class OtaConfigResp { |
|||
// 单例实例 |
|||
static OtaConfigResp? _instance; |
|||
|
|||
// 单例访问器(增加空安全保护) |
|||
static OtaConfigResp get instance { |
|||
if (_instance == null) { |
|||
throw Exception("User must be initialized first"); |
|||
} |
|||
return _instance!; |
|||
} |
|||
|
|||
// 最终属性(不可变) |
|||
final String? id; |
|||
final String? factory; |
|||
final String? equipmentname; |
|||
final String? systemversion; |
|||
final String? updatepackageaddress; |
|||
// 私有构造函数 |
|||
OtaConfigResp._({ |
|||
this.id, |
|||
this.factory, |
|||
this.equipmentname, |
|||
this.systemversion, |
|||
this.updatepackageaddress, |
|||
}); |
|||
|
|||
factory OtaConfigResp.fromJson(Map<String, dynamic> json) { |
|||
print("数据获取fronTokenJson${json}"); |
|||
// 修复1:安全获取嵌套的 user 字段 |
|||
final infoData = json['info'] as Map<String, dynamic>?; // 允许空值 |
|||
|
|||
// 更新单例实例 |
|||
return _instance = OtaConfigResp._( |
|||
id: (infoData?['id'] as String?) ?? '', |
|||
factory: (infoData?['factory'] as String?) ?? '', |
|||
equipmentname: (infoData?['equipmentname'] as String?) ?? '', |
|||
systemversion: (infoData?['systemversion'] as String?) ?? '', |
|||
updatepackageaddress: |
|||
(infoData?['updatepackageaddress'] as String?) ?? '', |
|||
); |
|||
} |
|||
|
|||
// 完整的JSON序列化 |
|||
Map<String, dynamic> toJson() => { |
|||
'user': { |
|||
'id': instance.id, |
|||
'factory': instance.factory, |
|||
'equipmentname': instance.equipmentname, |
|||
'systemversion': instance.systemversion, |
|||
'updatepackageaddress': instance.updatepackageaddress, |
|||
} |
|||
}; |
|||
} |
|||
@ -0,0 +1,109 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import '../controllers/ota_upgrade_controller.dart'; |
|||
|
|||
class OtaUpgradeView extends GetView<OtaUpgradeController> { |
|||
const OtaUpgradeView({Key? key}) : super(key: key); |
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Scaffold( |
|||
body: Center( |
|||
// 添加外层Center |
|||
child: SingleChildScrollView( |
|||
// 添加滚动防止小屏溢出 |
|||
child: Padding( |
|||
padding: EdgeInsets.all(20), |
|||
child: Column( |
|||
mainAxisAlignment: MainAxisAlignment.center, |
|||
crossAxisAlignment: CrossAxisAlignment.center, // 水平居中 |
|||
mainAxisSize: MainAxisSize.min, // 紧凑布局 |
|||
children: [ |
|||
// 设备图标 |
|||
CircleAvatar( |
|||
radius: 60, |
|||
backgroundColor: Colors.blue[100], |
|||
child: Icon( |
|||
Icons.phone_iphone, |
|||
size: 60, |
|||
color: Colors.blue, |
|||
), |
|||
), |
|||
SizedBox(height: 30), |
|||
|
|||
// 版本信息 |
|||
Column( |
|||
children: [ |
|||
Text( |
|||
'当前版本', |
|||
style: TextStyle( |
|||
fontSize: 18, |
|||
color: Colors.grey, |
|||
), |
|||
), |
|||
SizedBox(height: 8), |
|||
Text( |
|||
'v${controller.currentVersion}', |
|||
style: TextStyle( |
|||
fontSize: 24, |
|||
fontWeight: FontWeight.bold, |
|||
color: Colors.blue, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
SizedBox(height: 40), |
|||
|
|||
// 更新按钮/进度条 |
|||
Obx(() { |
|||
if (controller.isUpdating.value) { |
|||
return Column( |
|||
children: [ |
|||
SizedBox( |
|||
width: 200, // 限制进度条宽度 |
|||
child: LinearProgressIndicator( |
|||
value: controller.downloadProgress.value, |
|||
backgroundColor: Colors.grey[200], |
|||
valueColor: |
|||
AlwaysStoppedAnimation<Color>(Colors.blue), |
|||
minHeight: 12, |
|||
borderRadius: BorderRadius.circular(10), |
|||
), |
|||
), |
|||
SizedBox(height: 20), |
|||
Text( |
|||
'${(controller.downloadProgress.value * 100).toStringAsFixed(1)}%', |
|||
style: TextStyle(color: Colors.grey), |
|||
) |
|||
], |
|||
); |
|||
} else { |
|||
return ElevatedButton( |
|||
onPressed: controller.isUpdateAvailable.value |
|||
? null |
|||
: controller.checkUpdate, |
|||
style: ElevatedButton.styleFrom( |
|||
backgroundColor: controller.isUpdateAvailable.value |
|||
? Colors.grey |
|||
: Colors.blue, |
|||
foregroundColor: Colors.white, |
|||
padding: |
|||
EdgeInsets.symmetric(horizontal: 40, vertical: 15), |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(30), |
|||
), |
|||
), |
|||
child: Text( |
|||
controller.isUpdateAvailable.value ? '已是最新版本' : '立即更新', |
|||
style: TextStyle(fontSize: 16), |
|||
), |
|||
); |
|||
} |
|||
}), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue