From abb9abc5ef2d21b8982ecc9d0dae4b90cc5c68e6 Mon Sep 17 00:00:00 2001 From: cdhigh Date: Fri, 4 Mar 2022 10:02:19 -0300 Subject: [PATCH] V1.1.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 增加“自动重连”功能,和电子负载的连接异常中断后能自动重新连接(限制5分钟内) 2. 增加将原始放电数据导出到XLSX(带折线图)和TXT功能 --- README.md | 20 +- android/app/src/main/AndroidManifest.xml | 6 +- assets/fonts/iconfont.ttf | Bin 1632 -> 3180 bytes buildApk.py | 2 +- buildWindows.py | 2 +- changelog.md | 10 + ios/Runner/Info.plist | 4 + lib/common/common_utils.dart | 20 ++ lib/common/globals.dart | 20 +- lib/common/iconfont.dart | 7 + lib/common/widget_utils.dart | 1 + lib/export_data.dart | 234 +++++++++++++++++++++++ lib/i18n/export_data.i18n.dart | 83 ++++++++ lib/i18n/help.i18n.dart | 6 +- lib/i18n/main_drawer.i18n.dart | 12 +- lib/i18n/main_page.i18n.dart | 8 + lib/i18n/settings.i18n.dart | 33 +++- lib/main_page.dart | 150 ++++++++++++--- lib/models/volt_history_provider.dart | 127 ++++++------ lib/routes.dart | 2 + lib/settings/setting_tile.dart | 2 +- lib/settings/settings.dart | 56 +++++- lib/uni_serial.dart | 3 +- lib/version_update/version_check.dart | 28 +-- lib/widgets/curva_chart.dart | 4 +- lib/widgets/main_drawer.dart | 36 +++- lib/widgets/modal_dialogs.dart | 52 ++++- pubspec.lock | 143 +++++++++----- pubspec.yaml | 18 +- ref/connect.svg | 51 +++++ ref/disconnect.svg | 51 +++++ ref/usefullinks.txt | 2 + test/widget_test.dart | 18 -- 33 files changed, 1000 insertions(+), 211 deletions(-) create mode 100644 changelog.md create mode 100644 lib/export_data.dart create mode 100644 lib/i18n/export_data.i18n.dart create mode 100644 ref/connect.svg create mode 100644 ref/disconnect.svg create mode 100644 ref/usefullinks.txt delete mode 100644 test/widget_test.dart diff --git a/README.md b/README.md index 3c7f469..f334a53 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,21 @@ -# m328v6上位机APP +# m328v6数控电子负载上位机APP -这个是针对一乐论坛 M8V6数控电子负载 的 升级版 M328V6数控电子负载 的新版上位机。 -[M328V6数控电子负载](https://www.yleee.com.cn/thread-90734-1-1.html) +这个是一乐论坛[M8V6数控电子负载]的升级版[M328V6数控电子负载]的新版上位机。 +[M328V6数控电子负载制作链接](https://www.yleee.com.cn/thread-90734-1-1.html) ## 特性: -1. 支持Android, iOS, Windows, Mac OS, Linux +1. 支持Android, iOS, Windows, Mac OS, Linux(注1,2) 2. 支持多语种 -3. 支持OTG有线连接USB串口和无线连接蓝牙串口 +3. 支持OTG有线连接USB串口/无线连接蓝牙串口/主板原生串口 4. 支持全功能的控制下位机 -5. 支持放电曲线显示 -6. 支持保持屏幕常亮 +5. 支持放电曲线显示 +6. 支持保持屏幕常亮 + +*注1:仅提供Android/Windows二进制包,其他平台需要自己编译* +*注2:windows版本仅支持64位Windows7及更新的系统* + + + ## 屏幕截图: diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f07b419..d8f4141 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,7 +3,8 @@ + android:icon="@mipmap/ic_launcher" + android:requestLegacyExternalStorage="true"> + + + diff --git a/assets/fonts/iconfont.ttf b/assets/fonts/iconfont.ttf index 4e8498336e2eac7e694f86d86ffb9dcdcbef0955..261770ec56f9f205602e0628aed24a9e26dfffe5 100644 GIT binary patch literal 3180 zcmd^C-ESMm5uZ7dcj8eWcjl3ls7NMtB&|jgCGkipmSjn_YbkXSM2Rih2GXXCL`qC3 zlAsvZR#Swj93+kdG!IpQ3T@FM2;fK-(g3xCqL)9sq3ad(ucr0 zcJ}U=NOt~%9&dJkGrKdpJ2Sg`1OxyVRKNxU(?@3y2F{090Pq2l9((j?e{8Jpp)&EW z692uqrBv~at0UI|IEhGH$eqb7uGBgK>;nLf6N~B8d_WzKQQwb=h89WCkhD7qzfTle zTq>Uqy;}P#*}KK~TwyN7e;|BD`u7p9ETv8tu>+o`_EzEp`P5QcdVTCS0M3_byoF+6 zx%{0!{WnSz{*>BRfaY$i)GF|V{gv9S+UNH3OtR~78WNJ?*4w{)vSsXZaM_IxA5Hzx z&j71Qw_-nUdx6*n;EZNcpxOz`AR0WAiUzLjXcu7s#)13R)WgT1IhQKu!4IZ+Q-3GM zTy`Nv_RSWgeykNqg1}o+yS)Wp0|%}39QPr~er(qGPlzFDa^SF$^lP4(%;9;sQf*_Nmv3#puF^NU{#)~Gg=-ym2$;ebT z)G7D^e~w>pQSKP`BkrP+9bnty9)M1W!U#;m48_jz@Sx+b3(8y zit%yOg0W6inlY?)@rrj8V?*&sn3R-GjEyrBLRTa{6!R*)Z7Y5OQgKxlo_%1V~3rbc{Dly+i$V~2{Z zLZ*7QH@M{X#->YYY*f6gz+bgXWUIZMZ0%{&J)*18S=n7oe$^WswVHK8x@C@kkdhcvMP;QIr+FE5iJ-kL2}?k&SF% zo{C3$Q1*2qd9D-P%w3wN8C8^x)bV;`F5KjHH|f`#-R|b!w*LBP>l$ta5`n?C&?Gt= zoiju@8S)Qy4|hK@%5@BO=u=d5H!Dgr_c2rIwnw6YM9d*vM8SS%5`7ZBC;7OusI94i-`MHyVV|LadY%kj$lYBnu&RdjAv`1@yu)S{^gLW8(F_-}G8O|6+GbhM# z-VRjw9;8JJ{VeZC?t8 zIDT&@-e!{9K~?{qDB$M0ep+9;I+loWATXswOkWU%O+h>o9pt#AE{B4+ckdBHgg*-f z|1F66_s^Zf&8uwgwOdq(-1D>pWZL&Cz%YeS8`eCO`W`i$P-V@>iiGCVJgT5l2FTj- z{zqntmyRF5jQq^ZKd4?gJez%IX6CcJ_Q?r-edJ#k^$Vd6%$)c{`y0c+)o#-`F77!{ zXs3NOM%`+nKGui(-?&h`w0e2v7hBnPRxjiJ)z$wnqs-EXVV9kQbd9}?XXzx9>0SnM zt$DmS?g1FlrsMHx4F|OcHF|N77!qM`h_PB|S7pZXnyrV1a6t>Wfes5g?r7n3B9L1g zgdF4;N?#Rwa3fjKH!4Z0lQl5O2D6nK9k-RuYd!ikuICz?>>l{YxL+A|6PdR+yZ_u4 zrr1&Tm~tD*yDV%ce7}VqG^dz_d6FNnun0YH*upNdSR@aj@og4?RSP5C|7zADdfxb`tkI_ zv$<5M&erQ>y0n}vA7-r zv0N^W^!KxIm^?aHSb{@zXr&vn;{SEgTvW|tvr1y~|9Wz8>5GszJS zPykum(`09kdPnI{W?DtMlKbgbUr)V%)L7>d`-n6j^Sjn!c2rK}(1vz&AWx@I13FOz zVLp8-JD1k1I>wuPLnfO`A3IY_3l!5l`CXjPE?dw(Unpzha=MgF4GzAS|Q$Zbisc@HvI@qMp+F%_11#L}5 zLra3x-qw0Jg@HpuEkRtLch=B@?~l)q@Ap04C^3k2lC9kJjtPhh0LKN}aNc^o3h@WS zDOqPHIq&@*&@AftvS}3OFCSOf4;brZ5|f^G&i;XsF58V}mCKSrKw?<(J!+kyMNfFc;)N8+MhSSZxLW>uxpT W$yq6+&=9SgwTfXaJJnhvkbeM-u22>L diff --git a/buildApk.py b/buildApk.py index b4d7f01..bafffd9 100644 --- a/buildApk.py +++ b/buildApk.py @@ -25,7 +25,7 @@ DART_LIBSERIAL_STUB_PAT = re.compile(r"^(/{0,2})(import 'flutter_libserialport_stub.dart' as lib_serial;.*)$") #"lib/common/globals.dart -GLOBAL_VERSION_PAT = re.compile(r"^( *static +const +version += +[\"'])(1.0.0)([\"'].*)$") +GLOBAL_VERSION_PAT = re.compile(r"^( *static +const +version += +[\"'])([0-9\.]+)([\"'].*)$") VERSION_MASK = 0x01 YAML_LIBSERIAL_MASK = 0x02 diff --git a/buildWindows.py b/buildWindows.py index 3848e8d..7f94042 100644 --- a/buildWindows.py +++ b/buildWindows.py @@ -27,7 +27,7 @@ DART_LIBSERIAL_STUB_PAT = re.compile(r"^(/{0,2})(import 'flutter_libserialport_stub.dart' as lib_serial;.*)$") #"lib/common/globals.dart -GLOBAL_VERSION_PAT = re.compile(r"^( *static +const +version += +[\"'])(1.0.0)([\"'].*)$") +GLOBAL_VERSION_PAT = re.compile(r"^( *static +const +version += +[\"'])([0-9\.]+)([\"'].*)$") VERSION_MASK = 0x01 YAML_LIBSERIAL_MASK = 0x02 diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..d7536ab --- /dev/null +++ b/changelog.md @@ -0,0 +1,10 @@ +# 1.1.0 +1. 增加“自动重连”功能,和电子负载的连接异常中断后能自动重新连接(限制5分钟内) +2. 增加将原始放电数据导出到XLSX(带折线图)和TXT功能 + 注:Android低于5.1版本不能选择导出目录,默认导出到下载目录 + +# 1.0.0 - M328V6 App的第一个版本 +1. 支持放电曲线绘制 +2. 支持控制下位机 + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index c1ee48b..ad267cc 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -53,5 +53,9 @@ https http + LSSupportsOpeningDocumentsInPlace + + UIFileSharingEnabled + diff --git a/lib/common/common_utils.dart b/lib/common/common_utils.dart index 977bbcb..fab5ddc 100644 --- a/lib/common/common_utils.dart +++ b/lib/common/common_utils.dart @@ -14,6 +14,26 @@ String readableSeconds(int seconds) { } } +///比较两个版本号,确定新版本号是否比老版本号更新, +///版本号格式为:1.1.0,可能在最后还有一个修订版本号: 1.1.0+1,但是会忽略修订版本号(我不用修订版本号) +bool isVersionGreaterThan(String newVersion, String currVersion){ + final newV = newVersion.replaceAll("+", ".").split("."); + final currV = currVersion.replaceAll("+", ".").split("."); + final maxSeg = min(newV.length, currV.length); + for (var i = 0 ; i <= maxSeg - 1; i++) { + final vn = int.tryParse(newV[i]); + final vc = int.tryParse(currV[i]); + if ((vn == null) || (vc == null)) { + return false; + } + + if (vn != vc) { + return (vn > vc); + } + } + return false; +} + ///将字节数转换为可以直读的字符串(比如:xxx KB, xxx MB) String formatBytes(int bytes, {int decimals=2}) { if (bytes <= 0) { diff --git a/lib/common/globals.dart b/lib/common/globals.dart index a61b5a7..0ce6eb0 100644 --- a/lib/common/globals.dart +++ b/lib/common/globals.dart @@ -27,14 +27,14 @@ enum KeepScreenOption { ///小部分为程序共用的变量 class Global { //版本号注意需要使用单引号,让buildXXX.py能找的到 - static const version = '1.0.0'; + static const version = '1.1.0'; static const buildNumber = ""; + static late final SharedPreferences prefs; + static bool firstTimeRuning = true; //是否是本应用第一次运行 static int checkUpdateFrequency = 7; //检查新版本频率(天数) static DateTime lastCheckUpdateTime = DateTime(1970); //上次检查新版本的时标 - static int defaultBaudRate = 19200; //默认波特率 - static late final SharedPreferences prefs; static String selectedLanguage = ''; //当前选择的语种,空则为自动 static String selectedTheme = ''; //当前选择的主题,空则为自动 static Color homePageBackgroundColor = const Color(0xff0e0e0e); //主导航页面的背景颜色 @@ -43,10 +43,11 @@ class Global { static KeepScreenOption keepScreenOn = KeepScreenOption.always; static DateTime lastPaused = DateTime.now(); //上次切换到后台的时间 static DateTime lastHeartBeat = DateTime.now(); //上次接收到下位机的时间 - static bool offlineMode = false; //是否处于离线模式 static String lastSerialPort = ""; static int lastBaudRate = 19200; + static bool autoReconnect = true; //是否异常中断后自动重连 static int curvaFilterDotNum = 3; //放电曲线的平滑点数 + static double curvaFilterThreshold = 0.1; //放电曲线平滑阀值,大于此数值的点不被平均滤波 //各种tile的背景色和分割色 static Color get tileBkColor => Colors.white; @@ -69,11 +70,16 @@ class Global { keepScreenOn = KeepScreenOption.values[onValue]; lastSerialPort = prefs.getString('lastSerialPort') ?? ""; lastBaudRate = prefs.getInt('lastBaudRate') ?? 19200; + autoReconnect = prefs.getBool('autoReconnect') ?? true; curvaFilterDotNum = prefs.getInt('curvaFilterDotNum') ?? 3; if (curvaFilterDotNum == 0) { curvaFilterDotNum = 1; - } else if (curvaFilterDotNum > 10) { - curvaFilterDotNum = 10; + } else if (curvaFilterDotNum > 9) { + curvaFilterDotNum = 9; + } + curvaFilterThreshold = prefs.getDouble('curvaFilterThreshold') ?? 0.1; + if (curvaFilterThreshold > 1.0) { + curvaFilterThreshold = 1.0; } } catch (e) { //print(e.toString()); @@ -93,7 +99,9 @@ class Global { prefs.setInt("keepScreenOn", keepScreenOn.index); prefs.setString("lastSerialPort", lastSerialPort); prefs.setInt('lastBaudRate', lastBaudRate); + prefs.setBool('autoReconnect', autoReconnect); prefs.setInt('curvaFilterDotNum', curvaFilterDotNum); + prefs.setDouble('curvaFilterThreshold', curvaFilterThreshold); } ///这个全局key要注册到MaterialApp的navigatorKey属性 diff --git a/lib/common/iconfont.dart b/lib/common/iconfont.dart index ce3472b..f122cf6 100644 --- a/lib/common/iconfont.dart +++ b/lib/common/iconfont.dart @@ -6,5 +6,12 @@ import 'package:flutter/widgets.dart'; class IconFont { static const String _family = 'iconfont'; IconFont._(); + static const IconData device3 = IconData(0xe785, fontFamily: _family); + static const IconData device2 = IconData(0xe664, fontFamily: _family); + static const IconData device = IconData(0xe642, fontFamily: _family); + static const IconData fileType = IconData(0xe618, fontFamily: _family); + static const IconData connect = IconData(0xe6c0, fontFamily: _family); + static const IconData disconnect = IconData(0xe66b, fontFamily: _family); + static const IconData dot3 = IconData(0xe656, fontFamily: _family); static const IconData serialPort = IconData(0xe895, fontFamily: _family); } diff --git a/lib/common/widget_utils.dart b/lib/common/widget_utils.dart index 62f0aac..5d29f12 100644 --- a/lib/common/widget_utils.dart +++ b/lib/common/widget_utils.dart @@ -66,3 +66,4 @@ Decoration containerDivider({bool left=false, bool top=false, bool right=false, ) ); } + diff --git a/lib/export_data.dart b/lib/export_data.dart new file mode 100644 index 0000000..3bfbcf2 --- /dev/null +++ b/lib/export_data.dart @@ -0,0 +1,234 @@ +/// m328v6数控电子负载上位机 +/// 导出数据到EXCEL或其他格式 +/// Author: cdhigh +/// +import 'dart:io'; +//import 'dart:ui' as ui; +//import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as p; +import 'package:syncfusion_flutter_xlsio/xlsio.dart' as xlsio; +import 'package:syncfusion_officechart/officechart.dart' as officechart; +import 'package:file_picker/file_picker.dart'; +import 'i18n/export_data.i18n.dart'; +import 'common/iconfont.dart'; +import 'common/globals.dart'; +import 'common/common_utils.dart'; +import 'common/when.dart'; +import 'widgets/modal_dialogs.dart'; +import 'models/volt_history_provider.dart'; + +class ExportPage extends ConsumerStatefulWidget { + const ExportPage({Key? key}) : super(key: key); + @override + _ExportPageState createState() => _ExportPageState(); +} + +class _ExportPageState extends ConsumerState { + final _folderCtrller = TextEditingController(); + final _nameCtrller = TextEditingController(); + final _remarkCtrller = TextEditingController(); + String _exportType = "XLSX"; //"XLSX","TXT" + late VoltHistoryProvider _vhProvider; + int _dotNum = 0; + //String _baseName = ""; + //String _exportDir = ""; + + @override + void initState() { + super.initState(); + _nameCtrller.text = "m328v6_" + DateTime.now().format('yyyymmdd_HHMMSS') + ".xlsx"; + Future.delayed(const Duration(milliseconds: 500)).then(requestPermission); //延时确认权限并获取路径 + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Export curva data'.i18n)), + body: buildMainList(context), + ); + } + + ///构建页面主体的ListView + Widget buildMainList(BuildContext context) { + _vhProvider = ref.watch(Global.vHistoryProvider); + _dotNum = _vhProvider.dotNum; + return Container(padding: const EdgeInsets.all(10), child: + Column(crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding(padding: const EdgeInsets.only(top: 5), child: Text("Export type".i18n)), + Padding(padding: const EdgeInsets.only(left: 5), child: Row(children: [ + const Padding(padding: EdgeInsets.only(right: 12), child: Icon(IconFont.fileType),), + Expanded(child: DropdownButton(value: _exportType, + //isExpanded: true, + items: [ + DropdownMenuItem(value: "XLSX", child: Text("XLSX file".i18n)), + DropdownMenuItem(value: "TXT", child: Text("TXT file".i18n)),], + onChanged: (newValue) {setState(() { + _exportType = newValue.toString(); + _nameCtrller.text = p.setExtension(_nameCtrller.text, (newValue == "XLSX") ? ".xlsx" : ".txt"); + }); + },),), + ]),), + Padding(padding: const EdgeInsets.only(top: 5), child: Text("Export folder".i18n),), + Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Expanded(child: TextField(controller: _folderCtrller, + decoration: InputDecoration( + labelText: _folderCtrller.text != "" ? null : "Select a folder to save".i18n, + prefixIcon: const Icon(Icons.folder_open)), + ),), + IconButton(icon: const Icon(IconFont.dot3), onPressed: () async { + final ret = await FilePicker.platform.getDirectoryPath( + dialogTitle: "Choose a directory", initialDirectory: _folderCtrller.text).catchError((e){debugPrint(e.toString());}); + if (ret != null) { + setState(() {_folderCtrller.text = ret;}); + } + }),],), + Padding(padding: const EdgeInsets.only(top: 5), child: Text("File name".i18n),), + TextField(controller: _nameCtrller, + decoration: InputDecoration( + labelText: _nameCtrller.text != "" ? null : "Enter a name to save".i18n, + prefixIcon: const Icon(Icons.file_copy_outlined)), + ), + Padding(padding: const EdgeInsets.only(top: 5), child: Text("Remark".i18n),), + TextField(controller: _remarkCtrller, + decoration: InputDecoration( + labelText: _remarkCtrller.text != "" ? null : "Enter a remark".i18n, + prefixIcon: const Icon(Icons.comment_bank_outlined)), + ), + Padding(padding: const EdgeInsets.all(20), child: + ConstrainedBox(constraints: const BoxConstraints(minWidth: 100, maxWidth: 300), + child: ElevatedButton( + onPressed: (_dotNum > 0) ? doExport : null, + child: Text("Export".i18n)), + ),), + ]),); + } + + ///开始导出 + void doExport() { + if (_dotNum == 0) { + return; + } + + final fileName = p.join(_folderCtrller.text, _nameCtrller.text); + if (_exportType == "XLSX") { + exportToXlsx(fileName); + } else if (_exportType == "TXT") { + exportToTxt(fileName); + } else { + return; + } + + showOkAlertDialog(context: context, title: "Success".i18n, content: Text("Export %s file success".i18n.fill([_exportType]))); + } + + ///导出为XLSX + ///fileName: 路径+文件名 + void exportToXlsx(String fileName) async { + //生成XLSX实例 + final workbook = xlsio.Workbook(); + final sheet = workbook.worksheets[0]; + + //填充数据 + final remarkText = _remarkCtrller.text.trim(); + sheet.getRangeByName('A1').setText('Time'.i18n); + sheet.getRangeByName('B1').setText(remarkText.isEmpty ? 'Voltage'.i18n : remarkText); + final list = _vhProvider.vHistory; //cloneList() + for (var idx = 0; idx < list.length; idx++) { + final xlsIdx = idx + 2; + sheet.getRangeByName('A$xlsIdx').setText(readableSeconds(xlsIdx - 1)); + sheet.getRangeByName('B$xlsIdx').setNumber(list[idx]); + } + + //插入图表 + final charts = officechart.ChartCollection(sheet); + final chart = charts.add(); + chart.chartType = officechart.ExcelChartType.line; + chart.dataRange = sheet.getRangeByName('B1:B${_dotNum+2}'); + chart.isSeriesInRows = false; + chart.hasLegend = false; + //chart.legend!.position = officechart.ExcelLegendPosition.bottom; + chart.topRow = 0; + chart.bottomRow = 24; + chart.leftColumn = 3; + chart.rightColumn = 20; + chart.primaryValueAxis.minimumValue = _vhProvider.minV; + chart.primaryValueAxis.maximumValue = _vhProvider.maxV; + + //final officechart.ChartSerie serie = chart.series[0]; + //serie.dataLabels.isValue = true; + //serie.dataLabels.isCategoryName = true; + //serie.dataLabels.isSeriesName = true; + //serie.dataLabels.textArea.bold = true; + //serie.dataLabels.textArea.size = 12; + //serie.dataLabels.textArea.fontName = 'Arial'; + + //chart.plotArea.linePattern = officechart.ExcelChartLinePattern.solid; + //chart.plotArea.linePatternColor = '#00FFFF'; + //chart.linePattern = officechart.ExcelChartLinePattern.longDashDotDot; + //chart.linePatternColor = '#0000FF'; + + sheet.charts = charts; + + //开始保存文件 + final List bytes = workbook.saveAsStream(); + workbook.dispose(); + File(fileName).writeAsBytes(bytes); + } + + ///导出为Txt,一行一个数据 + ///fileName: 路径+文件名 + void exportToTxt(String fileName) async { + final file = File(fileName); + final list = _vhProvider.vHistory; + + var remarkText = _remarkCtrller.text.trim(); + + if (Platform.isWindows) { + if (remarkText.isNotEmpty) { + remarkText += "\r\n"; + } + file.writeAsString(remarkText + list.join("\r\n")); + } else { + if (remarkText.isNotEmpty) { + remarkText += "\n"; + } + file.writeAsString(remarkText + list.join("\n")); + } + } + + ///确认权限,如果没有权限,提示需要申请 + void requestPermission([_]) async { + if (Platform.isAndroid || Platform.isIOS || Platform.isFuchsia) { + var status = await Permission.storage.status; + if (!status.isPermanentlyDenied && !status.isDenied) { //之前没有拒绝过 + await Permission.storage.request(); + fetchDefaultExportDir(); + } + } else { + fetchDefaultExportDir(); + } + } + + ///根据系统不同返回默认的导出目录 + void fetchDefaultExportDir() async { + var ret = ""; + if (Platform.isAndroid || Platform.isFuchsia) { + //使用path_provider返回的Download目录在Android11上是应用内Download目录,外部无法存取 + //在这里使用一个通用的目录,如果以后Android又修改了,再考虑其他方案 + ret = '/storage/emulated/0/Download'; + } else if (Platform.isIOS) { + ret = (await getApplicationSupportDirectory()).path; + } else if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { + ret = (await getDownloadsDirectory())?.path ?? ""; + } else { + ret = ""; + } + setState((){_folderCtrller.text = ret;}); + } + +} diff --git a/lib/i18n/export_data.i18n.dart b/lib/i18n/export_data.i18n.dart new file mode 100644 index 0000000..ff7399c --- /dev/null +++ b/lib/i18n/export_data.i18n.dart @@ -0,0 +1,83 @@ +/// 导出数据页面的国际化翻译文件 +import 'package:i18n_extension/i18n_extension.dart'; + +extension Localization on String { + static var t = Translations("en_us") + + { + "en_us": "Save to folder", + "zh_cn": "保存目录", + } + + { + "en_us": "Save file to this folder", + "zh_cn": "保存到此目录", + } + + { + "en_us": "Export curva data", + "zh_cn": "导出曲线数据", + } + + { + "en_us": "Export type", + "zh_cn": "导出类型", + } + + { + "en_us": "PNG image", + "zh_cn": "PNG图像", + } + + { + "en_us": "XLSX file", + "zh_cn": "XLSX 文件", + } + + { + "en_us": "TXT file", + "zh_cn": "TXT 文件", + } + + { + "en_us": "Export folder", + "zh_cn": "导出目录", + } + + { + "en_us": "Select a folder to save", + "zh_cn": "选择一个保存目录", + } + + { + "en_us": "File name", + "zh_cn": "文件名", + } + + { + "en_us": "Enter a name to save", + "zh_cn": "输入一个保存文件名", + } + + { + "en_us": "Remark", + "zh_cn": "备注", + } + + { + "en_us": "Enter a remark", + "zh_cn": "输入备注信息", + } + + { + "en_us": "Export", + "zh_cn": "导出", + } + + { + "en_us": "Success", + "zh_cn": "成功", + } + + { + "en_us": "Export %s file success", + "zh_cn": "导出 %s 文件成功", + } + + { + "en_us": "Time", + "zh_cn": "时间", + } + + { + "en_us": "Voltage", + "zh_cn": "电压", + }; + + String get i18n => localize(this, t); + + //"Hello %s, this is %s".i18n.fill(["John", "Mary"]) + String fill(List params) => localizeFill(this, params); +} diff --git a/lib/i18n/help.i18n.dart b/lib/i18n/help.i18n.dart index 0f1f6ab..3698325 100644 --- a/lib/i18n/help.i18n.dart +++ b/lib/i18n/help.i18n.dart @@ -9,15 +9,15 @@ extension Localization on String { } + { "en_us": "Double-click 'Vset' to set a new value", - "zh_cn": "双击 '截止电压' 可以设置新的电压值", + "zh_cn": "双击[截止电压]可以设置新的电压值", } + { "en_us": "Double-click 'Iset' to set a new value", - "zh_cn": "双击 '放电电流' 可以设置新的电流值", + "zh_cn": "双击[放电电流]可以设置新的电流值", } + { "en_us": "Double-click 'Capacity' to clear Ah of device", - "zh_cn": "双击 '容量' 可以清零设备的容量值", + "zh_cn": "双击[容量]可以清零设备的容量值", } + { "en_us": "Swipe on the left side of the screen to popup the menu", diff --git a/lib/i18n/main_drawer.i18n.dart b/lib/i18n/main_drawer.i18n.dart index 5c80a72..e9cd93b 100644 --- a/lib/i18n/main_drawer.i18n.dart +++ b/lib/i18n/main_drawer.i18n.dart @@ -44,8 +44,8 @@ extension Localization on String { "zh_cn": "清除电量", } + { - "en_us": "Other Operations", - "zh_cn": "其他操作", + "en_us": "Device Operations", + "zh_cn": "设备操作", } + { "en_us": "Ra On", @@ -94,6 +94,14 @@ extension Localization on String { { "en_us": "Help", "zh_cn": "帮助", + } + + { + "en_us": "Other Operations", + "zh_cn": "其他操作", + } + + { + "en_us": "Export", + "zh_cn": "导出", }; String get i18n => localize(this, t); diff --git a/lib/i18n/main_page.i18n.dart b/lib/i18n/main_page.i18n.dart index 8a67339..859af8c 100644 --- a/lib/i18n/main_page.i18n.dart +++ b/lib/i18n/main_page.i18n.dart @@ -102,6 +102,14 @@ extension Localization on String { { "en_us": "Discharge has ended", "zh_cn": "放电已经结束", + } + + { + "en_us": "Disconnect", + "zh_cn": "断开连接", + } + + { + "en_us": "Connect", + "zh_cn": "连接", }; String get i18n => localize(this, t); diff --git a/lib/i18n/settings.i18n.dart b/lib/i18n/settings.i18n.dart index e0c46e8..26c09aa 100644 --- a/lib/i18n/settings.i18n.dart +++ b/lib/i18n/settings.i18n.dart @@ -175,6 +175,14 @@ extension Localization on String { "en_us": "Data", "zh_cn": "数据", } + + { + "en_us": "Auto reconnect", + "zh_cn": "自动重连", + } + + { + "en_us": "Auto reconnect after connection interruption", + "zh_cn": "连接异常断开后自动尝试重新连接", + } + { "en_us": "Number of points for smooth curve", "zh_cn": "用于平滑曲线的点数", @@ -210,9 +218,32 @@ extension Localization on String { { "en_us": "%s days", "zh_cn": "%s 天", + } + + { + "en_us": "Threshold for smooth curve", + "zh_cn": "曲线平滑电压门限", + } + + { + "en_us": "Enter a value from 0.00 to 1.00 (V)", + "zh_cn": "输入一个0.00到1.00的数值 (伏)", + } + + { + "en_us": "The value must be greater than 0.00 and less than 1.00", + "zh_cn": "数值必须大于0.00并且小于1.00", + } + + { + "en_us": "Found new version [%s]", + "zh_cn": "发现新版本 [%s]", + } + + { + "en_us": "Whatsnew:", + "zh_cn": "版本特性:", + } + + { + "en_us": "[The download link has been copied to the clipboard]", + "zh_cn": "[下载链接已经拷贝到系统剪贴板]", }; - String get i18n => localize(this, t); //"Hello %s, this is %s".i18n.fill(["John", "Mary"]) diff --git a/lib/main_page.dart b/lib/main_page.dart index 2221c49..5a21f4f 100644 --- a/lib/main_page.dart +++ b/lib/main_page.dart @@ -19,6 +19,7 @@ import 'common/globals.dart'; import 'common/event_bus.dart'; import 'common/serial_resp_buffer.dart'; import 'common/widget_utils.dart'; +import 'common/iconfont.dart'; import 'models/connection_provider.dart'; import 'models/running_data_provider.dart'; import 'models/app_info_provider.dart'; @@ -30,6 +31,7 @@ import 'widgets/modal_dialogs.dart'; import 'widgets/colored_safe_area.dart'; import 'widgets/curva_chart.dart'; import 'version_update/version_check.dart'; +import 'uni_serial.dart'; class MainPage extends ConsumerStatefulWidget { const MainPage({Key? key}) : super(key: key); @@ -41,16 +43,19 @@ class MainPage extends ConsumerStatefulWidget { class _MainPageState extends ConsumerState with AutomaticKeepAliveClientMixin, WidgetsBindingObserver { AppLifecycleState _lifeState = AppLifecycleState.inactive; final GlobalKey _scaffoldKey = GlobalKey(); - //var _realBkColor = Colors.white; //用户设置的背景色和flutter的背景色混合后的颜色,在build设置为正确的值 double _scrWidth = 100.0; //随便给一个值,保证不为空,会在build里面设置为正确的值 double _segDisplaySize = 1; //电压电流字体大小随屏幕大小变化,尽量填满水平方向 var availablePorts = []; //系统中的串口列表 - //SerialPortReader? _srlReader; //用于读取串口数据的对象 final _srlBuff = SerialRespBuffer(256); //串口接收缓存 + final _uniSerial = UniSerial(); + bool _prevReceivedOff = true; //上次接收到的报文是否是OFF状态 //如隔一段时间后没有收到EXTRA数据,重发一次请求额外数据的命令,避免下位机中间复位了 late final PausableTimer _timerForExtraData; var _lastSetLoadOffTime = DateTime.now(); + //如果异常中断并且启用了“自动重连”选项,则每隔5s自动尝试重连一次 + late final PausableTimer _timerForReconnect; + DateTime _lostConnectionTime = DateTime(2022); //丢失连接时间,自动重连限时5分钟内 @override bool get wantKeepAlive =>true; @@ -67,16 +72,46 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie _timerForExtraData = PausableTimer(const Duration(seconds: 3), qeuryVersionPeriodic); //_timerForExtraData.start(); //需要等连接后再启动定时器 + _timerForReconnect = PausableTimer(const Duration(seconds: 1), reconnectPeriodic); } //每隔一段时间重发一次请求额外数据的命令,避免下位机中间复位了 void qeuryVersionPeriodic() { - final load = ref.watch(Global.connectionProvider).load; + final load = ref.read(Global.connectionProvider).load; load.requestExtraData(); Future.delayed(const Duration(milliseconds: 500)).then((_) => load.queryVersion()); _timerForExtraData..reset()..start(); } + //如果异常中断并且启用了“自动重连”选项,则每隔5s自动尝试重连一次 + void reconnectPeriodic() async { + final connProvider = ref.read(Global.connectionProvider); + final lostSeconds = DateTime.now().difference(_lostConnectionTime).inSeconds; + if (connProvider.name.isNotEmpty || (lostSeconds > (5 * 60))) { //重连成功或超时5分钟,不需要再执行 + return; + } + + final name = Global.lastSerialPort; + final baudRate = Global.lastBaudRate; + if (name.isEmpty || (baudRate < 9600) || (baudRate > 115200)) { + return; + } + + bool ret = false; + try { + ret = await _uniSerial.open(name, baudRate); + } catch (e) { + ret = false; + } + + if (ret) { //如果重连成功 + connProvider.setPort(name, baudRate); + Global.bus.sendBroadcast(EventBus.connectionChanged, arg: "1", sendAsync: false); + } else { //2s后继续重试 + _timerForReconnect..reset()..start(); + } + } + ///确定是否需要检查新版本,如果有新版本,提示用户 void checkNewVersion([_]) { if (Global.checkUpdateFrequency == 0) { @@ -86,19 +121,20 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie final now = DateTime.now(); final days = now.difference(Global.lastCheckUpdateTime).inDays; if (days >= Global.checkUpdateFrequency) { - checkUpdateNow(silent: true); + checkUpdate(silent: true); } } @override void dispose() { _timerForExtraData.cancel(); + _timerForReconnect.cancel(); Global.bus.removeListener(EventBus.connectionChanged, connectionChanged); Global.bus.removeListener(EventBus.curvaFilterDotNumChanged, curvaFilterDotNumChanged); Global.bus.removeListener(EventBus.setLoadOnOff, setLoadOnOffReceived); Wakelock.disable(); WidgetsBinding.instance?.removeObserver(this); - ref.watch(Global.connectionProvider).closePort(); + ref.read(Global.connectionProvider).closePort(); super.dispose(); } @@ -121,20 +157,28 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie ///连接状态变化的事件 void connectionChanged(String isConnect) { if (mounted) { - final connProvider = ref.watch(Global.connectionProvider); + final connProvider = ref.read(Global.connectionProvider); if (isConnect == "1") { //连接 + //注册监听函数 connProvider.serial.registerListenFunction(newSrlDataReceived); + //连接后马上查询下位机版本号,请求上报额外数据 Future.delayed(const Duration(milliseconds: 500)).then((_) => connProvider.load.queryVersion()); Future.delayed(const Duration(seconds: 1)).then((_) => connProvider.load.requestExtraData()); _timerForExtraData..reset()..start(); + _timerForReconnect.pause(); } else { //断开连接 - final rdProvider = ref.watch(Global.runningDataProvider); + final rdProvider = ref.read(Global.runningDataProvider); connProvider.serial.close(); connProvider.closePort(); rdProvider.reset(); _timerForExtraData.pause(); - if (Global.keepScreenOn != KeepScreenOption.always) { //关闭屏幕常亮 + + //-1表示异常中断,如果启用“自动重连”,则启动重连定时器 + if ((isConnect == "-1") && Global.autoReconnect) { + _lostConnectionTime = DateTime.now(); + _timerForReconnect..reset()..start(); + } else if (Global.keepScreenOn != KeepScreenOption.always) { //关闭屏幕常亮 Wakelock.disable(); } } @@ -145,14 +189,18 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie ///平滑点数有变化,清空缓冲区内的点数滤波器 void curvaFilterDotNumChanged([_]) { - final vhProvider = ref.watch(Global.vHistoryProvider); - vhProvider.resetFilter(); + if (mounted) { + final vhProvider = ref.read(Global.vHistoryProvider); + vhProvider.resetFilter(); + } } ///接收到打开关闭放电的消息,保存现在的时间,避免弹出放电停止提示 void setLoadOnOffReceived(String isOn) { - if (isOn == "0") { - _lastSetLoadOffTime = DateTime.now(); + if (mounted) { + if (isOn == "0") { + _lastSetLoadOffTime = DateTime.now(); + } } } @@ -195,29 +243,50 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie }), title: Text(((portName == "") ? "Unconnected".i18n : "Connected".i18n)), titleSpacing: 1.0, - actions: (portName == "") ? null : - [Padding(padding: const EdgeInsets.only(right: 15), child: FlutterSwitch( - width: 80.0, - height: 35.0, - valueFontSize: 18.0, - toggleSize: 30.0, - borderRadius: 18, - value: rdProvider.running, - activeColor: Colors.green, - inactiveColor: Colors.red[600]!, - activeTextColor: Colors.white, - inactiveTextColor: Colors.white, - activeText: "ON", - inactiveText: "OFF", - showOnOff: true, - switchBorder: Border.all(color: Colors.white60, width: 1.0,), - onToggle:onTapAppBarSwitch,),), - ], + actions: buildAppBarActions((portName != ""), rdProvider.running), ) : null, body: (orientation == Orientation.portrait) ? portraitUi(context) : landscapeUi(context), )); } + ///构建AppBar右侧的按钮 + List buildAppBarActions(bool isConnected, bool isRunning) { + return [ + Padding(padding: const EdgeInsets.only(right: 20), child: + IconButton(icon: Icon(isConnected ? IconFont.disconnect : IconFont.connect, color: Colors.white), + tooltip: isConnected ? "Disconnect".i18n : "Connect".i18n, + constraints: const BoxConstraints(), + padding: EdgeInsets.zero, + iconSize: 40, + onPressed: () { + if (isConnected) { + Global.bus.sendBroadcast(EventBus.connectionChanged, arg: "0", sendAsync: false); + } else { + Navigator.pushNamed(context, "/connection"); + }}, + ),), + + //开始放电和结束放电的按钮 + Padding(padding: const EdgeInsets.only(right: 15), child: FlutterSwitch( + disabled: !isConnected, + width: 80.0, + height: 35.0, + valueFontSize: 18.0, + toggleSize: 30.0, + borderRadius: 18, + value: isRunning, + activeColor: Colors.green, + inactiveColor: Colors.red[600]!, + activeTextColor: Colors.white, + inactiveTextColor: Colors.white, + activeText: "ON", + inactiveText: "OFF", + showOnOff: true, + switchBorder: Border.all(color: Colors.white60, width: 1.0,), + onToggle:onTapAppBarSwitch,),), + ]; + } + ///构建主页中间显示的竖屏界面 Widget portraitUi(BuildContext context) { final appInfo = ref.watch(Global.infoProvider); @@ -620,12 +689,13 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie } //从未放电到放电状态,则自动清除原先的曲线数据,如果需要,启用屏幕常亮 - if (!rdProvider.running && !isOff) { + if (_prevReceivedOff && !isOff) { vhProvider.clear(); if (Global.keepScreenOn == KeepScreenOption.onWhenDischarge) { Wakelock.enable(); } } + _prevReceivedOff = isOff; //从放电状态到未放电状态,如果需要,关闭屏幕常亮 if (rdProvider.running && isOff) { @@ -696,6 +766,24 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie rdProvider.notifyDataChanged(); } } + } else if (respCmd == "V".codeUnitAt(0)) { //设置/查询当前电压的返回包 + if (size >= 7) { + final value = int.tryParse(String.fromCharCodes(bag, 2, 7)); + if ((value != null) && (value >= 0) && (value <= 65000)) { + rdProvider.vNow = value / 1000; + rdProvider.powerIn = (((rdProvider.vNow ~/ 10) * rdProvider.iNow).toInt() ~/ 10000) / 10; + rdProvider.notifyDataChanged(); + } + } + } else if (respCmd == "I".codeUnitAt(0)) {//设置/查询当前电流的返回包 + if (size >= 7) { + final value = int.tryParse(String.fromCharCodes(bag, 2, 7)); + if ((value != null) && (value >= 0) && (value <= 15000)) { + rdProvider.iNow = value / 1000; + rdProvider.powerIn = (((rdProvider.vNow ~/ 10) * rdProvider.iNow).toInt() ~/ 10000) / 10; + rdProvider.notifyDataChanged(); + } + } } } } diff --git a/lib/models/volt_history_provider.dart b/lib/models/volt_history_provider.dart index 6e47cb3..e59d86f 100644 --- a/lib/models/volt_history_provider.dart +++ b/lib/models/volt_history_provider.dart @@ -3,61 +3,8 @@ /// Author: cdhigh /// import 'package:flutter/material.dart'; -import 'package:collection/collection.dart'; //for sum import '../common/globals.dart'; -///用于电压历史数据滤波 -class _VoltHistoryFilter { - static const maxFilterNum = 10; //最多10个数据进行平滑 - - final _list = List.filled(maxFilterNum, 0.0); - var _cnt = 0; //数据个数 - var _idx = 0; //下一个数据的索引 - var _prevValue = 0.0; - - ///往滤波器里面添加一个数据,并且返回一个滤波后的数据 - double add(double volt) { - assert(Global.curvaFilterDotNum <= maxFilterNum); - - //避免因为传输误码等导致解析失败,产生偶尔的零,来两个零才输出零 - if (volt == 0.0) { - if (_prevValue != 0.0) { - final prevTemp = _prevValue; - _prevValue = 0.0; - return prevTemp; - } else { //清空缓冲区,返回零 - reset(); - return 0.0; - } - } - - _prevValue = volt; - _list[_idx] = volt; - _idx++; - if (_idx >= Global.curvaFilterDotNum) { - _idx = 0; - } - - if (_cnt >= Global.curvaFilterDotNum) { - //仅保留三位小数,避免曲线不够平滑 - return ((_list.sum / Global.curvaFilterDotNum) * 1000).round() / 1000; - } else { - _cnt++; - return volt; - } - } - - //复位过滤器 - void reset() { - for (var i = 0; i < maxFilterNum; i++) { - _list[_idx] = 0.0; - } - _prevValue = 0.0; - _cnt = 0; - _idx = 0; - } -} - ///用于Provider的容器类 class VoltHistoryProvider extends ChangeNotifier { final _filter = _VoltHistoryFilter(); @@ -72,7 +19,7 @@ class VoltHistoryProvider extends ChangeNotifier { //double get minV => min(_minV, (_maxV > 1.0) ? (_maxV - 1.0) : 0.0); double get minV => _minV; int get dotNum => _vHistory.length; - List get vHistory => _vHistory; //暴露此列表有些不合适,以后再改吧 + List get vHistory => _vHistory; //暴露此列表有些不合适,但是效率高,以后再改吧 ///添加一个实时电压 void add(double volt) { @@ -136,5 +83,77 @@ class VoltHistoryProvider extends ChangeNotifier { void resetFilter() { _filter.reset(); } + + ///创建一个原始数据备份返回,用于数据导出 + List cloneList() { + return [..._vHistory]; + } + + ///返回一个迭代器,第一个元素为索引,第二个元素为元素本身 + Iterable mapIndexed(T Function(int index, double elem) convert) sync* { + for (var index = 0; index < _vHistory.length; index++) { + yield convert(index, _vHistory[index]); + } + } } +///用于电压历史数据滤波 +class _VoltHistoryFilter { + static const maxFilterNum = 10; //最多10个数据进行平滑 + + final _list = List.filled(maxFilterNum, 0.0); + var _cnt = 0; //数据个数 + var _idx = 0; //下一个数据的索引 + var _prevValue = 0.0; + + ///往滤波器里面添加一个数据,并且返回一个滤波后的数据 + double add(double volt) { + assert(Global.curvaFilterDotNum <= maxFilterNum); + + //避免因为传输误码等导致解析失败,产生偶尔的零,来两个零才输出零 + if (volt == 0.0) { + if (_prevValue != 0.0) { + final prevTemp = _prevValue; + _prevValue = 0.0; + return prevTemp; + } else { //清空缓冲区,返回零 + reset(); + return 0.0; + } + } + + //对阀值进行处理 + if ((volt - _prevValue).abs() > Global.curvaFilterThreshold) { + reset(); + } + + _prevValue = volt; + _list[_idx] = volt; + _idx++; + if (_idx >= Global.curvaFilterDotNum) { + _idx = 0; + } + + if (_cnt >= Global.curvaFilterDotNum) { + var sum = 0.0; + for (var i = 0; i < Global.curvaFilterDotNum; i++) { + sum += _list[i]; + } + //仅保留三位小数,避免曲线不够平滑 + return ((sum / Global.curvaFilterDotNum) * 1000).round() / 1000; + } else { + _cnt++; + return volt; + } + } + + //复位过滤器 + void reset() { + for (var i = 0; i < maxFilterNum; i++) { + _list[_idx] = 0.0; + } + _prevValue = 0.0; + _cnt = 0; + _idx = 0; + } +} diff --git a/lib/routes.dart b/lib/routes.dart index b006d0d..ef2ea16 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -10,6 +10,7 @@ import 'connection.dart'; import 'delay_period_on_off.dart'; import 'switch_mode.dart'; import 'help_page.dart'; +import 'export_data.dart'; //为什么用函数 onGenerateRoute, 而不用 变量 routes //是因为 MaterialApp通过onGenerateRoute才方便传递参数给其他路由,尽管现在还没使用此特性 @@ -25,6 +26,7 @@ MaterialPageRoute routesPath(RouteSettings settings) { '/delay_period_on_off': (context) => I18n(child: const DelayPeriodOnOffPage(),), '/mode': (context) => I18n(child: const SwitchModePage(),), '/help': (context) => I18n(child: const HelpPage(),), + '/export': (context) => I18n(child: const ExportPage(),), }; WidgetBuilder builder = routes[settings.name] ?? routes['/']!; return MaterialPageRoute(builder: (ctx) => builder(ctx)); diff --git a/lib/settings/setting_tile.dart b/lib/settings/setting_tile.dart index 1801e7f..9498098 100644 --- a/lib/settings/setting_tile.dart +++ b/lib/settings/setting_tile.dart @@ -25,7 +25,7 @@ class SettingTile extends StatelessWidget { @override Widget build(BuildContext context) { Widget titleWidget = Text(title) - .intoPadding(padding: const EdgeInsets.only(bottom: 3.0)) + .intoPadding(padding: const EdgeInsets.only(bottom: 5.0)) .addNeighbor(subTitle is String ? Text(subTitle, style: TextStyle(color: Global.isDarkMode ? Colors.white38 : Colors.grey[800]), textScaleFactor: 0.9, maxLines: subMaxLines, overflow: TextOverflow.ellipsis) diff --git a/lib/settings/settings.dart b/lib/settings/settings.dart index a76c2ae..f12cf11 100644 --- a/lib/settings/settings.dart +++ b/lib/settings/settings.dart @@ -52,8 +52,13 @@ class _SettingsPageState extends ConsumerState { //数据组 buildSettingGroupTile('Data'.i18n), + SettingTile(title: 'Auto reconnect'.i18n, subTitle: 'Auto reconnect after connection interruption'.i18n, + switchValue: Global.autoReconnect, switchCallback: (_)=>onTapAutoReconnect()) + .intoGestureDetector(onTap: onTapAutoReconnect), SettingTile(title: 'Number of points for smooth curve'.i18n, subTitle: Text(Global.curvaFilterDotNum.toString())) .intoInkWell(onTap: onTapDotSmoothNum), + SettingTile(title: 'Threshold for smooth curve'.i18n, subTitle: Text(Global.curvaFilterThreshold.toStringAsFixed(2) + " V")) + .intoInkWell(onTap: onTapSmoothThreshold), SettingTile(title: 'Curve line start color'.i18n, subTitle: displayCurvaStartColor()) .intoInkWell(onTap: onTapCurvaStartColor), SettingTile(title: 'Curve line end color'.i18n, subTitle: displayCurvaEndColor()) @@ -61,11 +66,11 @@ class _SettingsPageState extends ConsumerState { //版本号 buildSettingGroupTile('Version'.i18n), - SettingTile(title: 'Current version'.i18n, subTitle: (Global.version != "") ? (" V" + Global.version) : ""), + SettingTile(title: 'Current version'.i18n, subTitle: (Global.version != "") ? ("V" + Global.version) : ""), SettingTile(title: 'Check frequency'.i18n, subTitle: displayCheckFrequency()) .intoInkWell(onTap: onTapCheckFrequency), SettingTile(title: 'Check for update now'.i18n, subTitle: "https//github.com/cdhigh/m328v6host") - .intoInkWell(onTap: () {checkUpdateNow(silent: false);}), + .intoInkWell(onTap: checkUpdateNow), const SizedBox(height: 100), ])); @@ -294,6 +299,12 @@ class _SettingsPageState extends ConsumerState { } } + ///点击了‘自动重连’功能 + void onTapAutoReconnect() { + setState(() => Global.autoReconnect = !Global.autoReconnect); + Global.saveProfile(); + } + ///点击了“用于平滑曲线的点数” void onTapDotSmoothNum() async { //简单的闭包函数,根据当前平滑点数创建不同的对话框行 @@ -335,6 +346,24 @@ class _SettingsPageState extends ConsumerState { } } + ///点击了曲线平滑阀值 + void onTapSmoothThreshold() async { + String? ret = await showInputDialog(context: context, + title: "Enter a value from 0.00 to 1.00 (V)".i18n, + initialText: Global.curvaFilterThreshold.toStringAsFixed(2), + formatters: [DecimalTextInputFormatter(), CustomMaxValueInputFormatter(1.0)], + ); + + if (ret != null) { + final value = double.tryParse(ret); + if ((value == null) || (value < 0.0) || (value > 1.0)) { + showToast("The value must be greater than 0.00 and less than 1.00".i18n); + } else { + setState(() {Global.curvaFilterThreshold = value;}); + } + } + } + ///显示检查新版本频率 String displayCheckFrequency() { switch (Global.checkUpdateFrequency) { @@ -381,5 +410,28 @@ class _SettingsPageState extends ConsumerState { Global.saveProfile(); } } + + ///点击了现在检查新版本 + void checkUpdateNow() async { + final ret = await checkUpdate(silent: false); + if (ret == null) { + return; + } + + final newVer = ret.version; + final whatsnew = ret.whatsNew.replaceAll("
", "\n"); + showOkAlertDialog(context: context, title: "Found new version [%s]".i18n.fill([newVer]), + content: Column(mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Whatsnew:".i18n, style: const TextStyle(fontWeight: FontWeight.bold)), + const Divider(), + Text(whatsnew), + const Divider(), + Padding(padding: const EdgeInsets.only(top: 20), + child: Text("[The download link has been copied to the clipboard]".i18n, textScaleFactor: 0.8)), + ],), + ); + } } diff --git a/lib/uni_serial.dart b/lib/uni_serial.dart index a703bb2..ae145da 100644 --- a/lib/uni_serial.dart +++ b/lib/uni_serial.dart @@ -243,7 +243,8 @@ class UniSerial { } ///连接被关闭后发送端口关闭广播 + /// 用参数-1表示异常关闭 void sendDisconnectBroadcast([_]) { - Global.bus.sendBroadcast(EventBus.connectionChanged, arg: "0", sendAsync: false); + Global.bus.sendBroadcast(EventBus.connectionChanged, arg: "-1", sendAsync: false); } } diff --git a/lib/version_update/version_check.dart b/lib/version_update/version_check.dart index 56eeb0b..c2c5a69 100644 --- a/lib/version_update/version_check.dart +++ b/lib/version_update/version_check.dart @@ -11,6 +11,7 @@ import 'package:http/http.dart' as http; import '../i18n/common.i18n.dart'; import '../common/widget_utils.dart'; import '../common/globals.dart'; +import '../common/common_utils.dart'; import 'version_models.dart'; const kVersionJsonUri = 'https://raw.githubusercontent.com/cdhigh/m328v6host/main/versions/version.json'; @@ -18,10 +19,10 @@ const kVersionJsonUri = 'https://raw.githubusercontent.com/cdhigh/m328v6host/mai ///现在检查更新版本 /// version: 目前的版本 /// silent: 是否静默检查 -/// 返回是否有新版本 -Future checkUpdateNow({bool silent=true}) async { +/// 如果有心版本,返回新版本详细信息,否则返回null +Future checkUpdate({bool silent=true}) async { if (Global.version.isEmpty) { - return false; + return null; } Global.lastCheckUpdateTime = DateTime.now(); @@ -29,7 +30,7 @@ Future checkUpdateNow({bool silent=true}) async { if (!silent) { BotToast.showLoading(); } - final ret = await getUpdateInfo(); + final ret = await getAllUpdateInfo(); if (!silent) { BotToast.closeAllLoading(); } @@ -37,35 +38,38 @@ Future checkUpdateNow({bool silent=true}) async { if (!silent) { showToast("Check for update failed".i18n); } - return false; + return null; } final lastest = ret.lastest; - if (lastest.isEmpty || (lastest.compareTo(Global.version) <= 0)) { + if (lastest.isEmpty || !isVersionGreaterThan(lastest, Global.version)) { if (!silent) { showToast("Your version is up to date".i18n); } - return false; + return null; } //具体版本的详细信息 final lastestVersion = getVersionDetails(ret, lastest); if (lastestVersion != null) { - BotToast.showText(text: "There is a new version (%s), the download link has been copied to the clipboard".i18n.fill([lastest]), - duration: const Duration(seconds: 5)); + if (silent) { //如果是静默检查,则检查到新版本后显示一个Toast,否则调用方会显示一个对话框 + BotToast.showText(text: "There is a new version (%s), the download link has been copied to the clipboard".i18n.fill([lastest]), + duration: const Duration(seconds: 5)); + } + if (Platform.isAndroid || Platform.isIOS || Platform.isFuchsia) { pasteText(lastestVersion.androidFile); } else { pasteText(lastestVersion.windowsFile); } - return true; + return lastestVersion; } else { - return false; + return null; } } ///连接服务器,检查更新,返回更新信息包 -Future getUpdateInfo() async { +Future getAllUpdateInfo() async { final url = Uri.parse(kVersionJsonUri); final response = await http.get(url).timeout(const Duration(seconds: 5), onTimeout: () {return http.Response('Error', 408);},); diff --git a/lib/widgets/curva_chart.dart b/lib/widgets/curva_chart.dart index a6258bf..b205326 100644 --- a/lib/widgets/curva_chart.dart +++ b/lib/widgets/curva_chart.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fl_chart/fl_chart.dart'; -import 'package:collection/collection.dart'; //for mapIndexed +//import 'package:collection/collection.dart'; //for mapIndexed import '../common/globals.dart'; import '../common/common_utils.dart'; import '../models/volt_history_provider.dart'; @@ -80,7 +80,7 @@ LineChartData _fillCurvaData(VoltHistoryProvider vhProvider, AppInfoProvider app maxY: vhProvider.maxV, //Y轴最大值 lineBarsData: [ //曲线实际数据,如果需要多个曲线,添加多个 LineChartBarData() LineChartBarData( - spots: vhProvider.vHistory.mapIndexed((idx, elem) => FlSpot(idx.toDouble(), elem)).toList(), + spots: vhProvider.mapIndexed((idx, elem) => FlSpot(idx.toDouble(), elem)).toList(), isCurved: false, colors: [appInfo.curvaStartColor, appInfo.curvaEndColor], barWidth: 2, //曲线宽度 diff --git a/lib/widgets/main_drawer.dart b/lib/widgets/main_drawer.dart index 102fedc..c494ce1 100644 --- a/lib/widgets/main_drawer.dart +++ b/lib/widgets/main_drawer.dart @@ -33,7 +33,7 @@ class MainDrawer extends ConsumerWidget { final iStart = portName.indexOf("["); //final iEnd = portName.indexOf("]"); if (iStart >= 0) { - portName = portName.substring(0, iStart); + portName = portName.substring(0, iStart).trim(); } } } @@ -55,7 +55,7 @@ class MainDrawer extends ConsumerWidget { Icon(IconFont.serialPort, color: Colors.white54)], ), Column(children: [ - if (!portConnected) ListTile(leading: const Icon(Icons.bluetooth), + if (!portConnected) ListTile(leading: const Icon(IconFont.connect), title: Text("Connect".i18n), trailing: const Icon(Icons.arrow_forward), onTap: () { @@ -63,7 +63,7 @@ class MainDrawer extends ConsumerWidget { Navigator.pushNamed(context, "/connection"); }, ), - if (portConnected) ListTile(leading: const Icon(Icons.bluetooth_disabled), + if (portConnected) ListTile(leading: const Icon(IconFont.disconnect), title: Text("Disconnect [%s]".i18n.fill([portName])), onTap: () { Global.bus.sendBroadcast(EventBus.connectionChanged, arg: "0", sendAsync: false); @@ -87,9 +87,12 @@ class MainDrawer extends ConsumerWidget { connProvider.load.setLoadOn(false); }, ), + ExpansionTile(title: Text("Device Operations".i18n), + leading: const Icon(IconFont.device3), + children: [MainDrawerDeviceOperations(topLoadMenu: topLoadMenu)]), ExpansionTile(title: Text("Other Operations".i18n), leading: const Icon(Icons.account_tree), - children: [MainDrawerOperations(topLoadMenu: topLoadMenu)]), + children: const [MainDrawerOtherOperations()]), const Divider(), ListTile(title: Text("Settings".i18n), leading: const Icon(Icons.settings), @@ -112,9 +115,9 @@ class MainDrawer extends ConsumerWidget { } ///侧滑菜单-针对下位机的操作列表 -class MainDrawerOperations extends ConsumerWidget { +class MainDrawerDeviceOperations extends ConsumerWidget { final bool topLoadMenu; //Load On/Load Off菜单是否置于顶层 - const MainDrawerOperations({Key? key, required this.topLoadMenu}) : super(key: key); + const MainDrawerDeviceOperations({Key? key, required this.topLoadMenu}) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { @@ -212,3 +215,24 @@ class MainDrawerOperations extends ConsumerWidget { ); } } + + +///侧滑菜单-其他操作列表 +class MainDrawerOtherOperations extends ConsumerWidget { + const MainDrawerOtherOperations({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Column( + children: [ + ListTile(title: Text("Export".i18n), + trailing: const Icon(Icons.arrow_forward), + onTap: () { + Navigator.of(context).pop(); //关闭drawer + Navigator.pushNamed(context, "/export"); + }, + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/widgets/modal_dialogs.dart b/lib/widgets/modal_dialogs.dart index 45ffffd..d9af877 100644 --- a/lib/widgets/modal_dialogs.dart +++ b/lib/widgets/modal_dialogs.dart @@ -1,6 +1,7 @@ /// 定义几种弹出的模态对话框 /// Author: cdhigh /// +import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import '../common/my_widget_chain.dart'; @@ -20,8 +21,8 @@ Future showOkCancelAlertDialog({required BuildContext context, required S title: Text(title), content: content, actions: [ - Text(cancelText!, style: const TextStyle(color: Colors.black)).intoTextButton(onPressed: ()=>Navigator.of(ctx).pop(false)), - Text(okText!, style: const TextStyle(color: Colors.black)).intoTextButton(onPressed: ()=>Navigator.of(ctx).pop(true)), + Text(cancelText!, style: const TextStyle(color: Colors.black)).intoTextButton(onPressed: ()=>Navigator.of(ctx).pop(false)), + Text(okText!, style: const TextStyle(color: Colors.black)).intoTextButton(onPressed: ()=>Navigator.of(ctx).pop(true)), ], ); }, @@ -48,8 +49,8 @@ Future showOkAlertDialog({required BuildContext context, required String t } ///弹出输入框,要求输入一个字符串 -Future showInputDialog({required BuildContext context, required String title, String? okText, String? cancelText, - String? initialText, TextInputType? keyboardType}) async { +Future showInputDialog({required BuildContext context, required String title, String? okText, String? cancelText, + String? initialText, TextInputType? keyboardType, List? formatters}) async { okText ??= 'Okay'.i18n; cancelText ??= 'Cancel'.i18n; var controller = TextEditingController(); @@ -65,18 +66,18 @@ Future showInputDialog({required BuildContext context, required String t title: Text(title), content: Row( children: [ - Expanded(child: TextField(autofocus: true, - keyboardType: keyboardType, controller: controller)) + Expanded(child: TextField(autofocus: true, keyboardType: keyboardType, controller: controller, + inputFormatters: formatters,)) ]), actions: [ Text(cancelText!, style: const TextStyle(color: Colors.black)).intoTextButton(onPressed: ()=>Navigator.of(ctx).pop()), - Text(okText!, style: const TextStyle(color: Colors.black)).intoTextButton(onPressed: ()=>Navigator.of(ctx).pop(controller.text.trim())), + Text(okText!, style: const TextStyle(color: Colors.black)).intoTextButton(onPressed: ()=>Navigator.of(ctx).pop(controller.text.trim())), ], ); }, ); - return ret ?? ""; + return ret; } ///显示取色对话框 @@ -104,11 +105,42 @@ Future showColorPickerDialog({required BuildContext context, String? tit ), actions: [ Text(cancelText!, style: const TextStyle(color: Colors.black)).intoTextButton(onPressed: ()=>Navigator.of(ctx).pop()), - Text(okText!, style: const TextStyle(color: Colors.black)).intoTextButton(onPressed: ()=>Navigator.of(ctx).pop(currentColor)), + Text(okText!, style: const TextStyle(color: Colors.black)).intoTextButton(onPressed: ()=>Navigator.of(ctx).pop(currentColor)), ], ); } ); return ret; -} \ No newline at end of file +} + +///TextField可用的几个定制的inputFormatter +///DecimalTextInputFormatter: 仅允许输入一个数字 +class DecimalTextInputFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { + final regEx = RegExp(r'^\d*\.?\d*'); + final String newStr = regEx.stringMatch(newValue.text) ?? ''; + return (newStr == newValue.text) ? TextEditingValue(text: newStr) : oldValue; + } +} + +///仅允许小于某个浮点数的数值 +class CustomMaxValueInputFormatter extends TextInputFormatter { + final double maxInputValue; + CustomMaxValueInputFormatter(this.maxInputValue); + + @override + TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { + final TextSelection newSel = newValue.selection; + String truncated = newValue.text; + final double? value = double.tryParse(newValue.text); + if (value == null) { + return newValue; + } else if (value > maxInputValue) { + truncated = maxInputValue.toString(); + } + return TextEditingValue(text: truncated, selection: newSel); + } +} + diff --git a/pubspec.lock b/pubspec.lock index 6977dc3..43fe11b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -15,6 +15,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.8.0" + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.1" args: dependency: transitive description: @@ -29,13 +36,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.8.2" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" bot_toast: dependency: "direct main" description: @@ -113,13 +113,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.3" - fake_async: - dependency: transitive - description: - name: fake_async - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" ffi: dependency: transitive description: @@ -134,6 +127,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.1.2" + file_picker: + dependency: "direct main" + description: + name: file_picker + url: "https://pub.dartlang.org" + source: hosted + version: "4.5.0" fl_chart: dependency: "direct main" description: @@ -179,6 +179,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" flutter_riverpod: dependency: "direct main" description: @@ -193,11 +200,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.3.2" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" flutter_web_plugins: dependency: transitive description: flutter @@ -238,6 +240,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.2.0" + image: + dependency: transitive + description: + name: image + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.3" intl: dependency: transitive description: @@ -266,13 +275,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.1" - matcher: - dependency: transitive - description: - name: matcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.12.11" material_color_utilities: dependency: transitive description: @@ -295,12 +297,33 @@ packages: source: hosted version: "2.0.2" path: - dependency: transitive + dependency: "direct main" description: name: path url: "https://pub.dartlang.org" source: hosted version: "1.8.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.9" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.12" + path_provider_ios: + dependency: transitive + description: + name: path_provider_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" path_provider_linux: dependency: transitive description: @@ -308,6 +331,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.5" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" path_provider_platform_interface: dependency: transitive description: @@ -357,6 +387,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.7.0" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "4.4.0" platform: dependency: transitive description: @@ -474,13 +511,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.0.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.10.0" state_notifier: dependency: transitive description: @@ -488,34 +518,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.7.2+1" - stream_channel: + string_scanner: dependency: transitive description: - name: stream_channel + name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" - string_scanner: + version: "1.1.0" + syncfusion_flutter_core: dependency: transitive description: - name: string_scanner + name: syncfusion_flutter_core url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" - term_glyph: + version: "19.4.54" + syncfusion_flutter_xlsio: + dependency: "direct main" + description: + name: syncfusion_flutter_xlsio + url: "https://pub.dartlang.org" + source: hosted + version: "19.4.54-beta" + syncfusion_officechart: + dependency: "direct main" + description: + name: syncfusion_officechart + url: "https://pub.dartlang.org" + source: hosted + version: "19.4.54-beta" + syncfusion_officecore: dependency: transitive description: - name: term_glyph + name: syncfusion_officecore url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" - test_api: + version: "19.4.54-beta" + term_glyph: dependency: transitive description: - name: test_api + name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "0.4.8" + version: "1.2.0" typed_data: dependency: transitive description: @@ -649,6 +693,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.0+1" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "5.3.1" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a8e4032..6c2b636 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.0.0 +version: 1.1.0 environment: sdk: ">=2.16.1 <3.0.0" @@ -52,13 +52,19 @@ dependencies: flutter_bluetooth_serial: ^0.4.0 #for android #serial_port_win32: ^0.4.9 #for windows permission_handler: ^9.1.0 - http: ^0.13.4 - pausable_timer: ^1.0.0+4 - flutter_switch: ^0.3.2 + http: ^0.13.4 #auto check update + pausable_timer: ^1.0.0+4 #query version of device + flutter_switch: ^0.3.2 #on/off in AppBar + syncfusion_flutter_xlsio: ^19.4.53-beta + syncfusion_officechart: ^19.4.54-beta + #filesystem_picker: ^2.0.0 #browse the file system and pick a folder + path_provider: ^2.0.9 + path: ^1.8.0 + file_picker: ^4.5.0 dev_dependencies: - flutter_test: - sdk: flutter + #flutter_test: + # sdk: flutter # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is diff --git a/ref/connect.svg b/ref/connect.svg new file mode 100644 index 0000000..bf9e781 --- /dev/null +++ b/ref/connect.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + diff --git a/ref/disconnect.svg b/ref/disconnect.svg new file mode 100644 index 0000000..598dc19 --- /dev/null +++ b/ref/disconnect.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + diff --git a/ref/usefullinks.txt b/ref/usefullinks.txt new file mode 100644 index 0000000..acc5829 --- /dev/null +++ b/ref/usefullinks.txt @@ -0,0 +1,2 @@ +[SVG to IconFont](https://icomoon.io/) +[SVG to IconFont](http://fontello.com/) \ No newline at end of file diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index dce49e8..0000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,18 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -//import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:m328v6/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const M328v6App()); - }); -}