diff --git a/lib/core/utils/ftpconnect/src/ftp_entry.dart b/lib/core/utils/ftpconnect/src/ftp_entry.dart index bdf1a3623..fd241b4f4 100644 --- a/lib/core/utils/ftpconnect/src/ftp_entry.dart +++ b/lib/core/utils/ftpconnect/src/ftp_entry.dart @@ -27,11 +27,8 @@ class FTPEntry { r"(.+)$" //file/dir name[8] ); - static final regexpLISTSiiServers = RegExp(r"^(.{8}\s+.{7})\s+" //date[1] - r"(.{0,5})\s+" //type file or dir [2] - r"(\d{0,24})\s+" //size [3] - r"(.+)$" //file/ dir name [4] - ); + static final regexpLISTSiiServers = RegExp( + r"^(\d{2}-\d{2}-\d{4}\s+\d{2}:\d{2}[AP]M)\s+(\)?\s*(\d*)\s+(.+)$"); // Hide constructor FTPEntry._( @@ -218,35 +215,22 @@ class FTPEntry { int _uid = -1; Iterable matches = regexpLISTSiiServers.allMatches(responseLine); for (Match match in matches) { - //date - String date = - match.group(1)!.split(" ").fold('', (previousValue, element) { - //keep only one space and add fullyear if only last 2 digits in year - if (element.isEmpty) return previousValue; - if (previousValue.isEmpty) { - return element.length <= 8 - ? element.substring(0, 6) + - DateTime.now().year.toString().substring(0, 2) + - element.substring(6, 8) - : element; - } - return '$previousValue $element'; - }); + // 日期,合并多余空格 + String date = match.group(1)!.replaceAll(RegExp(r'\s+'), ' '); _modifyTime = DateFormat('MM-dd-yyyy hh:mma').parse(date); - //type - if (match.group(2)!.trim().isEmpty) { - _type = FTPEntryType.FILE; - } else if (match.group(2)!.toLowerCase().contains("dir")) { + // 类型 + if ((match.group(2) ?? '').trim().isNotEmpty) { _type = FTPEntryType.DIR; } else { - _type = FTPEntryType.LINK; + _type = FTPEntryType.FILE; } - //size - _size = int.tryParse(match.group(3)!) ?? 0; - //file/dir name - _name = match.group(4)!; + // 大小 + _size = int.tryParse(match.group(3) ?? '') ?? 0; + + // 文件/目录名 + _name = match.group(4)!.trim(); } return FTPEntry._(_name, _modifyTime, _persmission, _type, _size, _unique, _group, _gid, _mode, _owner, _uid, {}); diff --git a/lib/modules/meeting/controllers/meeting_connect_controller.dart b/lib/modules/meeting/controllers/meeting_connect_controller.dart index af101e1cb..31a1b47fc 100644 --- a/lib/modules/meeting/controllers/meeting_connect_controller.dart +++ b/lib/modules/meeting/controllers/meeting_connect_controller.dart @@ -1,13 +1,19 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:get/get.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:wifi_iot/wifi_iot.dart'; +import '../../../core/utils/ftpconnect/ftpconnect.dart'; +import 'meeting_controller.dart'; + class MeetingConnectController extends GetxController { + final _meetingController = Get.find(); + RxString stateStr = '搜索中...'.obs; RxBool isRescan = false.obs; - RxBool isImportedFiles = false.obs; RxString importedFilesStr = ''.obs; late StreamSubscription> _scanResultsSubscription; @@ -16,8 +22,8 @@ class MeetingConnectController extends GetxController { bool isScanFailed = false; - late BluetoothCharacteristic? _writeCharacteristic; - late StreamSubscription>? _valueReceivedSubscription; + BluetoothCharacteristic? _writeCharacteristic; + StreamSubscription>? _valueReceivedSubscription; String _wifiSSID = ''; String _wifiPassword = ''; @@ -26,6 +32,13 @@ class MeetingConnectController extends GetxController { String _ftpAccount = ''; String _ftpPassword = ''; + FTPConnect? _ftpConnect; + + RxBool isDownload = false.obs; + RxDouble downloadProgress = 0.0.obs; + RxInt totalReceived = 0.obs; + RxInt fileSize = 0.obs; + @override void onInit() { super.onInit(); @@ -56,6 +69,7 @@ class MeetingConnectController extends GetxController { _scanResultsSubscription.cancel(); _isScanningSubscription.cancel(); _valueReceivedSubscription?.cancel(); + _ftpConnect?.disconnect(); _stopScan(); _disconnect(); super.onClose(); @@ -86,6 +100,7 @@ class MeetingConnectController extends GetxController { } Future _disconnect() async { + await WiFiForIoTPlugin.forceWifiUsage(false); await WiFiForIoTPlugin.disconnect(); await _device?.disconnect(); } @@ -131,26 +146,25 @@ class MeetingConnectController extends GetxController { } void _listenData(List value) { - print('----$value--------'); int command = value[2]; switch (command) { case 0x0d: // 是否有导入文件 if (value[3] == 0x01) { - isImportedFiles.value = true; - importedFilesStr.value = '检查到有新文件,是否导入'; + stateStr.value = '正在开启Wi-Fi...'; + importedFilesStr.value = '检查到有新文件,正在准备导入'; + sendData([0x5a, 0x02, 0x87, 0x01]); } else { - isImportedFiles.value = false; importedFilesStr.value = '没有新文件需要导入'; } break; case 0x87: // Wi-Fi 开启/关闭 if (value[3] == 0x01 && value[4] == 0x01) { - _getWifi(); + sendData([0x5a, 0x01, 0x0b]); } break; case 0x0b: // WiFi 热点 SSID _wifiSSID = String.fromCharCodes(value.sublist(3, value.length - 2)); - _connectWifi(); + sendData([0x5a, 0x01, 0x0c]); break; case 0x0c: // WiFi 热点密码 _wifiPassword = _decodePassword(value); @@ -176,11 +190,6 @@ class MeetingConnectController extends GetxController { } } - void _getWifi() async { - await sendData([0x5a, 0x01, 0x0b]); - await sendData([0x5a, 0x01, 0x0c]); - } - void _connectWifi() async { if (_wifiSSID.isNotEmpty && _wifiPassword.isNotEmpty) { stateStr.value = '正在连接 Wi-Fi...'; @@ -189,16 +198,71 @@ class MeetingConnectController extends GetxController { password: _wifiPassword, security: NetworkSecurity.WPA, ); + await WiFiForIoTPlugin.forceWifiUsage(true); stateStr.value = 'Wi-Fi 连接成功'; sendData([0x5a, 0x01, 0x03]); } } - void _connectFtp() { + void _connectFtp() async { if (_ftpAddress.isNotEmpty && _ftpPort.isNotEmpty && _ftpAccount.isNotEmpty && - _ftpPassword.isNotEmpty) {} + _ftpPassword.isNotEmpty) { + _ftpConnect = FTPConnect( + _ftpAddress, + port: int.tryParse(_ftpPort) ?? 21, + user: _ftpAccount, + pass: _ftpPassword, + ); + await _ftpConnect?.connect(); + _ftpConnect?.listCommand = ListCommand.LIST; + List? listDirectory = await _ftpConnect?.listDirectoryContent(); + if (listDirectory != null && listDirectory.isNotEmpty) { + stateStr.value = '正在下载文件...'; + isDownload.value = true; + int totalSize = listDirectory.fold( + 0, + (sum, entry) => sum + (entry.size ?? 0), + ); + for (var i = 0; i < listDirectory.length; i++) { + importedFilesStr.value = + '共${listDirectory.length}个文件,正在下载第${i + 1}个文件'; + int previousSize = i > 0 ? listDirectory[i - 1].size ?? 0 : 0; + FTPEntry element = listDirectory[i]; + await _downloadFile(totalSize, previousSize, element); + await _ftpConnect?.deleteFile(element.name); + } + stateStr.value = '下载完成'; + importedFilesStr.value = '下载完成,共${listDirectory.length}个文件,2秒后自动返回'; + _disconnect(); + Future.delayed(const Duration(seconds: 2), () { + Get.back(); + }); + } + } + } + + Future _downloadFile( + int totalSize, int previousSize, FTPEntry entry) async { + final directory = await getApplicationDocumentsDirectory(); + final directoryDir = Directory('${directory.path}/RecordingPen'); + if (!await directoryDir.exists()) { + await directoryDir.create(recursive: true); + } + final String filePath = '${directory.path}/RecordingPen/${entry.name}'; + final File localFile = File(filePath); + await _ftpConnect?.downloadFileWithRetry( + entry.name, + localFile, + pRetryCount: 3, + onProgress: (double progress, int received, int size) { + downloadProgress.value = (previousSize + received) / totalSize; + totalReceived.value = received; + fileSize.value = size; + }, + ); + await _meetingController.insertMeetingAudio(entry.name, filePath, ''); } String _decodePassword(List data) { diff --git a/lib/modules/meeting/controllers/meeting_controller.dart b/lib/modules/meeting/controllers/meeting_controller.dart index b9767ee74..de9a05a63 100644 --- a/lib/modules/meeting/controllers/meeting_controller.dart +++ b/lib/modules/meeting/controllers/meeting_controller.dart @@ -190,13 +190,11 @@ class MeetingController extends GetxController { int addUp = 0; final appDir = await appDocDir; -// 定义源目录列表 final sourceDirs = [ Directory("${appDir.path}/MeetingAudio/"), Directory("${appDir.path}/DestAudio/"), ]; -// 创建目标目录(如果不存在) final destDir = Directory("${appDir.path}/LocalAudio/"); if (!await destDir.exists()) { await destDir.create(recursive: true); @@ -212,8 +210,6 @@ class MeetingController extends GetxController { } } -// 此时 allFiles 包含 MeetingAudio 和 DestAudio 目录下所有的文件 - for (final fileEntity in allFiles) { try { final file = File(fileEntity.path); @@ -334,9 +330,21 @@ class MeetingController extends GetxController { } } - await _audioPlayer.setAudioSource(AudioSource.file(filePath!)); var audiourl = await UploadOss.upload(filepath: filePath, rootDir: 'ExternalAudio'); + final meetingId = await insertMeetingAudio(fileName, filePath, audiourl); + + if (isParamProvided) { + Get.toNamed(Routes.meetingDetails, arguments: { + 'id': meetingId, + }); + } + EasyLoading.dismiss(); + } + + Future insertMeetingAudio( + String fileName, String filePath, String audiourl) async { + await _audioPlayer.setAudioSource(AudioSource.file(filePath)); int seconds = _audioPlayer.duration?.inSeconds ?? 0; int creationTime = DateTime.now().millisecondsSinceEpoch; Map data = { @@ -357,13 +365,7 @@ class MeetingController extends GetxController { }); addQuantity++; } - - if (isParamProvided) { - Get.toNamed(Routes.meetingDetails, arguments: { - 'id': meetingId, - }); - } - EasyLoading.dismiss(); + return meetingId; } Future _saveFile(PlatformFile file) async { diff --git a/lib/modules/meeting/views/meeting_connect_view.dart b/lib/modules/meeting/views/meeting_connect_view.dart index e8e86fad4..82c3e3d97 100644 --- a/lib/modules/meeting/views/meeting_connect_view.dart +++ b/lib/modules/meeting/views/meeting_connect_view.dart @@ -76,19 +76,29 @@ class MeetingConnectView extends GetView { ), ), ), - Obx( - () => controller.isImportedFiles.value - ? Padding( - padding: EdgeInsets.symmetric(vertical: 10.w), - child: _button('导入文件', Icons.file_download, onTap: () { - controller.stateStr.value = '正在开启Wi-Fi...'; - controller.importedFilesStr.value = ''; - controller.isImportedFiles.value = false; - controller.sendData([0x5a, 0x02, 0x87, 0x01]); - }), - ) - : const SizedBox(), - ), + Obx(() => controller.isDownload.value + ? Column( + children: [ + Container( + width: 300.w, + height: 3.w, + margin: EdgeInsets.symmetric(vertical: 10.w), + child: LinearProgressIndicator( + value: controller.downloadProgress.value, + valueColor: + const AlwaysStoppedAnimation(Colors.red), + ), + ), + Text( + '${controller.totalReceived.value} / ${controller.fileSize.value} bytes', + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + ], + ) + : const SizedBox()), ], ), ); diff --git a/lib/modules/meeting/views/meeting_view.dart b/lib/modules/meeting/views/meeting_view.dart index db4fe3ba8..33ee92176 100644 --- a/lib/modules/meeting/views/meeting_view.dart +++ b/lib/modules/meeting/views/meeting_view.dart @@ -423,6 +423,7 @@ class MeetingView extends GetView { PreferredSizeWidget _buildAppBar(BuildContext context, bool isDarkMode) { return AppBar( backgroundColor: isDarkMode ? Colors.black : Colors.white, + surfaceTintColor: Colors.transparent, elevation: 0.5, leading: IconButton( icon: Icon( @@ -459,7 +460,7 @@ class MeetingView extends GetView { () => ListView.builder( controller: controller.listController, itemCount: controller.dataList.length, - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w), + padding: EdgeInsets.fromLTRB(12.w, 5.w, 12.w, 90.w), itemBuilder: (BuildContext context, int index) { MeetingModel data = MeetingModel.fromJson(controller.dataList[index]); return _item(context, isDarkMode, data);