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