From b6fc6ec4a2765ea3862695e17b20234eda1e66ff Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Sat, 5 Jul 2025 11:30:03 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BC=9A=E8=AE=AE?= =?UTF-8?q?=E5=8A=A9=E6=89=8Bble=E8=8E=B7=E5=8F=96ftp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../meeting_connect_controller.dart | 60 ++++++++++++++++--- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/lib/modules/meeting/controllers/meeting_connect_controller.dart b/lib/modules/meeting/controllers/meeting_connect_controller.dart index f6d16e214..cceb82ddf 100644 --- a/lib/modules/meeting/controllers/meeting_connect_controller.dart +++ b/lib/modules/meeting/controllers/meeting_connect_controller.dart @@ -20,6 +20,12 @@ class MeetingConnectController extends GetxController { String _wifiSSID = ''; String _wifiPassword = ''; + String _ftpAddress = ''; + String _ftpPort = ''; + String _ftpAccount = ''; + String _ftpPassword = ''; + + bool _isConnectWifi = false; @override void onInit() { @@ -139,27 +145,63 @@ class MeetingConnectController extends GetxController { break; case 0x87: // Wi-Fi 开启/关闭 if (value[3] == 0x01 && value[4] == 0x01) { - sendData([0x5a, 0x01, 0x0b]); - sendData([0x5a, 0x01, 0x0c]); + _getWifi(); } break; case 0x0b: // WiFi 热点 SSID _wifiSSID = String.fromCharCodes(value.sublist(3, value.length - 2)); - if (_wifiSSID.isNotEmpty && _wifiPassword.isNotEmpty) { - _connectWifi(); - } + _connectWifi(); break; case 0x0c: // WiFi 热点密码 _wifiPassword = _decodePassword(value); - if (_wifiSSID.isNotEmpty && _wifiPassword.isNotEmpty) { - _connectWifi(); - } + _connectWifi(); + break; + case 0x03: // FTP 地址 + _ftpAddress = String.fromCharCodes(value.sublist(3, value.length - 2)); + _connectFtp(); + break; + case 0x04: // FTP 端口 + _ftpPort = String.fromCharCodes(value.sublist(3, value.length - 2)); + _connectFtp(); + break; + case 0x05: // FTP 账号 + _ftpAccount = String.fromCharCodes(value.sublist(3, value.length - 2)); + _connectFtp(); + break; + case 0x06: // FTP 密码 + _ftpPassword = _decodePassword(value); + _connectFtp(); break; default: } } - void _connectWifi() {} + void _getWifi() async { + await sendData([0x5a, 0x01, 0x0b]); + await sendData([0x5a, 0x01, 0x0c]); + } + + void _getFtp() async { + await sendData([0x5a, 0x01, 0x03]); + await sendData([0x5a, 0x01, 0x04]); + await sendData([0x5a, 0x01, 0x05]); + await sendData([0x5a, 0x01, 0x06]); + } + + void _connectWifi() { + if (_wifiSSID.isNotEmpty && _wifiPassword.isNotEmpty) { + _getFtp(); + } + _connectFtp(); + } + + void _connectFtp() { + if (_isConnectWifi && + _ftpAddress.isNotEmpty && + _ftpPort.isNotEmpty && + _ftpAccount.isNotEmpty && + _ftpPassword.isNotEmpty) {} + } String _decodePassword(List data) { final passwordData = data.sublist(3, data.length - 2); From 993d16b3c1775d550dd6b6c42df80a9018d2ba6a Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Sat, 5 Jul 2025 15:56:31 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BC=9A=E8=AE=AE?= =?UTF-8?q?=E5=8A=A9=E6=89=8B=E8=87=AA=E5=8A=A8=E8=BF=9E=E6=8E=A5Wi-Fi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../meeting_connect_controller.dart | 32 +++++++++---------- pubspec.yaml | 1 + 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/lib/modules/meeting/controllers/meeting_connect_controller.dart b/lib/modules/meeting/controllers/meeting_connect_controller.dart index cceb82ddf..af101e1cb 100644 --- a/lib/modules/meeting/controllers/meeting_connect_controller.dart +++ b/lib/modules/meeting/controllers/meeting_connect_controller.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:get/get.dart'; +import 'package:wifi_iot/wifi_iot.dart'; class MeetingConnectController extends GetxController { RxString stateStr = '搜索中...'.obs; @@ -25,8 +26,6 @@ class MeetingConnectController extends GetxController { String _ftpAccount = ''; String _ftpPassword = ''; - bool _isConnectWifi = false; - @override void onInit() { super.onInit(); @@ -87,6 +86,7 @@ class MeetingConnectController extends GetxController { } Future _disconnect() async { + await WiFiForIoTPlugin.disconnect(); await _device?.disconnect(); } @@ -158,15 +158,15 @@ class MeetingConnectController extends GetxController { break; case 0x03: // FTP 地址 _ftpAddress = String.fromCharCodes(value.sublist(3, value.length - 2)); - _connectFtp(); + sendData([0x5a, 0x01, 0x04]); break; case 0x04: // FTP 端口 _ftpPort = String.fromCharCodes(value.sublist(3, value.length - 2)); - _connectFtp(); + sendData([0x5a, 0x01, 0x05]); break; case 0x05: // FTP 账号 _ftpAccount = String.fromCharCodes(value.sublist(3, value.length - 2)); - _connectFtp(); + sendData([0x5a, 0x01, 0x06]); break; case 0x06: // FTP 密码 _ftpPassword = _decodePassword(value); @@ -181,23 +181,21 @@ class MeetingConnectController extends GetxController { await sendData([0x5a, 0x01, 0x0c]); } - void _getFtp() async { - await sendData([0x5a, 0x01, 0x03]); - await sendData([0x5a, 0x01, 0x04]); - await sendData([0x5a, 0x01, 0x05]); - await sendData([0x5a, 0x01, 0x06]); - } - - void _connectWifi() { + void _connectWifi() async { if (_wifiSSID.isNotEmpty && _wifiPassword.isNotEmpty) { - _getFtp(); + stateStr.value = '正在连接 Wi-Fi...'; + await WiFiForIoTPlugin.connect( + _wifiSSID, + password: _wifiPassword, + security: NetworkSecurity.WPA, + ); + stateStr.value = 'Wi-Fi 连接成功'; + sendData([0x5a, 0x01, 0x03]); } - _connectFtp(); } void _connectFtp() { - if (_isConnectWifi && - _ftpAddress.isNotEmpty && + if (_ftpAddress.isNotEmpty && _ftpPort.isNotEmpty && _ftpAccount.isNotEmpty && _ftpPassword.isNotEmpty) {} diff --git a/pubspec.yaml b/pubspec.yaml index e01fc5fb8..32cdf99ff 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,6 +27,7 @@ dependencies: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git ref: main + wifi_iot: ^0.3.19+2 permission_handler: ^10.4.5 web_socket_channel: ^2.4.0 flutter_dotenv: ^5.1.0 From b617e4b847e483414839addeb052484feb43827e Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Sat, 5 Jul 2025 16:32:12 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E6=B7=BB=E5=8A=A0ftp=E8=BF=9E=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/utils/ftpconnect/ftpconnect.dart | 6 + .../ftpconnect/src/commands/directory.dart | 98 ++++++ .../utils/ftpconnect/src/commands/file.dart | 190 +++++++++++ lib/core/utils/ftpconnect/src/ftp_entry.dart | 265 +++++++++++++++ .../utils/ftpconnect/src/ftp_exceptions.dart | 11 + lib/core/utils/ftpconnect/src/ftp_reply.dart | 24 ++ lib/core/utils/ftpconnect/src/ftp_socket.dart | 221 +++++++++++++ .../utils/ftpconnect/src/ftpconnect_base.dart | 307 ++++++++++++++++++ lib/core/utils/ftpconnect/src/logger.dart | 13 + lib/core/utils/ftpconnect/src/utils.dart | 57 ++++ 10 files changed, 1192 insertions(+) create mode 100644 lib/core/utils/ftpconnect/ftpconnect.dart create mode 100644 lib/core/utils/ftpconnect/src/commands/directory.dart create mode 100644 lib/core/utils/ftpconnect/src/commands/file.dart create mode 100644 lib/core/utils/ftpconnect/src/ftp_entry.dart create mode 100644 lib/core/utils/ftpconnect/src/ftp_exceptions.dart create mode 100644 lib/core/utils/ftpconnect/src/ftp_reply.dart create mode 100644 lib/core/utils/ftpconnect/src/ftp_socket.dart create mode 100644 lib/core/utils/ftpconnect/src/ftpconnect_base.dart create mode 100644 lib/core/utils/ftpconnect/src/logger.dart create mode 100644 lib/core/utils/ftpconnect/src/utils.dart diff --git a/lib/core/utils/ftpconnect/ftpconnect.dart b/lib/core/utils/ftpconnect/ftpconnect.dart new file mode 100644 index 000000000..8d4f8c3da --- /dev/null +++ b/lib/core/utils/ftpconnect/ftpconnect.dart @@ -0,0 +1,6 @@ +library ftpconnect; + +export 'src/ftp_exceptions.dart'; +export 'src/ftpconnect_base.dart'; +export 'src/ftp_entry.dart'; +export 'src/logger.dart'; diff --git a/lib/core/utils/ftpconnect/src/commands/directory.dart b/lib/core/utils/ftpconnect/src/commands/directory.dart new file mode 100644 index 000000000..5a3d17606 --- /dev/null +++ b/lib/core/utils/ftpconnect/src/commands/directory.dart @@ -0,0 +1,98 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import '../ftp_entry.dart'; +import '../ftp_exceptions.dart'; +import '../ftp_reply.dart'; +import '../ftp_socket.dart'; +import '../ftpconnect_base.dart'; +import '../utils.dart'; + +class FTPDirectory { + final FTPSocket _socket; + + FTPDirectory(this._socket); + + Future makeDirectory(String sName) async { + FTPReply sResponse = await (_socket.sendCommand('MKD $sName')); + + return sResponse.isSuccessCode(); + } + + Future deleteEmptyDirectory(String? sName) async { + FTPReply sResponse = await (_socket.sendCommand('rmd $sName')); + + return sResponse.isSuccessCode(); + } + + Future changeDirectory(String? sName) async { + FTPReply sResponse = await (_socket.sendCommand('CWD $sName')); + + return sResponse.isSuccessCode(); + } + + Future currentDirectory() async { + FTPReply sResponse = await _socket.sendCommand('PWD'); + if (!sResponse.isSuccessCode()) { + throw FTPConnectException( + 'Failed to get current working directory', sResponse.message); + } + + int iStart = sResponse.message.indexOf('"') + 1; + int iEnd = sResponse.message.lastIndexOf('"'); + + return sResponse.message.substring(iStart, iEnd); + } + + Future> directoryContent() async { + // Enter passive mode + FTPReply response = await _socket.openDataTransferChannel(); + + // Directoy content listing, the response will be handled by another socket + _socket.sendCommandWithoutWaitingResponse(_socket.listCommand.describeEnum); + + // Data transfer socket + int iPort = Utils.parsePort(response.message, _socket.supportIPV6); + Socket dataSocket = await Socket.connect(_socket.host, iPort, + timeout: Duration(seconds: _socket.timeout)); + //Test if second socket connection accepted or not + response = await _socket.readResponse(); + //some server return two lines 125 and 226 for transfer finished + bool isTransferCompleted = response.isSuccessCode(); + if (!isTransferCompleted && response.code != 125 && response.code != 150) { + throw FTPConnectException('Connection refused. ', response.message); + } + + List lstDirectoryListing = []; + await dataSocket.listen((Uint8List data) { + lstDirectoryListing.addAll(data); + }).asFuture(); + + await dataSocket.close(); + + if (!isTransferCompleted) { + response = await _socket.readResponse(); + if (!response.isSuccessCode()) { + throw FTPConnectException('Transfer Error.', response.message); + } + } + + // Convert MLSD response into FTPEntry + List lstFTPEntries = []; + String.fromCharCodes(lstDirectoryListing).split('\n').forEach((line) { + if (line.trim().isNotEmpty) { + lstFTPEntries.add( + FTPEntry.parse(line.replaceAll('\r', ""), _socket.listCommand), + ); + } + }); + + return lstFTPEntries; + } + + Future> directoryContentNames() async { + var list = await directoryContent(); + return list.map((f) => f.name).whereType().toList(); + } +} diff --git a/lib/core/utils/ftpconnect/src/commands/file.dart b/lib/core/utils/ftpconnect/src/commands/file.dart new file mode 100644 index 000000000..243e9a423 --- /dev/null +++ b/lib/core/utils/ftpconnect/src/commands/file.dart @@ -0,0 +1,190 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:path/path.dart'; + +import '../../ftpconnect.dart'; +import '../ftp_reply.dart'; +import '../ftp_socket.dart'; +import '../utils.dart'; + +typedef FileProgress = void Function( + double progressInPercent, int totalReceived, int fileSize); + +class FTPFile { + final FTPSocket _socket; + + FTPFile(this._socket); + + Future rename(String sOldName, String sNewName) async { + FTPReply sResponse = await (_socket.sendCommand('RNFR $sOldName')); + if (sResponse.code != 350) { + return false; + } + + sResponse = await (_socket.sendCommand('RNTO $sNewName')); + return sResponse.isSuccessCode(); + } + + Future delete(String? sFilename) async { + FTPReply sResponse = await (_socket.sendCommand('DELE $sFilename')); + + return sResponse.isSuccessCode(); + } + + Future exist(String sFilename) async { + return await size(sFilename) != -1; + } + + Future size(String? sFilename) async { + try { + FTPReply sResponse = await (_socket.sendCommand('SIZE $sFilename')); + if (!sResponse.isSuccessCode() && + _socket.transferType != TransferType.binary) { + //check if ascii mode get refused + //change to binary mode if ascii mode refused + final _socketTransferTypeBackup = _socket.transferType; + await _socket.setTransferType(TransferType.binary); + sResponse = await (_socket.sendCommand('SIZE $sFilename')); + //back to default mode + await _socket.setTransferType(_socketTransferTypeBackup); + } + return int.parse(sResponse.message.replaceAll('213 ', '')); + } catch (e) { + return -1; + } + } + + Future download( + String? sRemoteName, + File fLocalFile, { + FileProgress? onProgress, + }) async { + _socket.logger.log('Download $sRemoteName to ${fLocalFile.path}'); + //check for file existence and init totalData to receive + int fileSize = 0; + fileSize = await FTPFile(_socket).size(sRemoteName); + if (fileSize == -1) { + throw FTPConnectException('Remote File $sRemoteName does not exist!'); + } + + // Enter passive mode + FTPReply response = await _socket.openDataTransferChannel(); + + //the response will be the file, witch will be loaded with another socket + _socket.sendCommandWithoutWaitingResponse('RETR $sRemoteName'); + + // Data Transfer Socket + int lPort = Utils.parsePort(response.message, _socket.supportIPV6); + _socket.logger.log('Opening DataSocket to Port $lPort'); + final Socket dataSocket = await Socket.connect(_socket.host, lPort, + timeout: Duration(seconds: _socket.timeout)); + // Test if second socket connection accepted or not + response = await _socket.readResponse(); + //some server return two lines 125 and 226 for transfer finished + bool isTransferCompleted = response.isSuccessCode(); + if (!isTransferCompleted && response.code != 125 && response.code != 150) { + throw FTPConnectException('Connection refused. ', response.message); + } + + // Changed to listen mode instead so that it's possible to send information back on downloaded amount + _socket.logger.log('Start downloading...'); + var sink = fLocalFile.openWrite(mode: FileMode.writeOnly); + var received = 0; + await dataSocket.listen((data) { + sink.add(data); + if (onProgress != null) { + received += data.length; + var percent = ((received / fileSize) * 100).toStringAsFixed(2); + //in case that the file size is 0, then pass directly 100 + double percentVal = double.tryParse(percent) ?? 100; + if (percentVal.isInfinite || percentVal.isNaN) percentVal = 100; + onProgress(percentVal, received, fileSize); + } + }).asFuture(); + + await dataSocket.close(); + await sink.flush(); + await sink.close(); + + if (!isTransferCompleted) { + //Test if All data are well transferred + response = await _socket.readResponse(); + if (!response.isSuccessCode()) { + throw FTPConnectException('Transfer Error.', response.message); + } + } + + _socket.logger.log('File Downloaded!'); + return true; + } + + /// Upload File [fFile] to the current directory with [remoteName] (using filename if not set) + Future upload( + File fFile, { + String remoteName = '', + FileProgress? onProgress, + }) async { + _socket.logger.log('Upload File: ${fFile.path}'); + + // Enter passive mode + FTPReply response = await _socket.openDataTransferChannel(); + + // Store File + String sFilename = remoteName; + if (sFilename.isEmpty) { + sFilename = basename(fFile.path); + } + + // The response is the file to upload, witch will be managed by another socket + _socket.sendCommandWithoutWaitingResponse('STOR $sFilename'); + + // Data Transfer Socket + int iPort = Utils.parsePort(response.message, _socket.supportIPV6); + _socket.logger.log('Opening DataSocket to Port $iPort'); + final Socket dataSocket = await Socket.connect(_socket.host, iPort); + //Test if second socket connection accepted or not + response = await _socket.readResponse(); + //some server return two lines 125 and 226 for transfer finished + bool isTransferCompleted = response.isSuccessCode(); + if (!isTransferCompleted && response.code != 125 && response.code != 150) { + throw FTPConnectException('Connection refused. ', response.message); + } + + _socket.logger.log('Start uploading...'); + + var received = 0; + int fileSize = await fFile.length(); + + Stream> readStream = fFile.openRead().transform( + StreamTransformer.fromHandlers( + handleData: (data, sink) { + sink.add(data); + if (onProgress != null) { + received += data.length; + var percent = ((received / fileSize) * 100).toStringAsFixed(2); + //in case that the file size is 0, then pass directly 100 + double percentVal = double.tryParse(percent) ?? 100; + if (percentVal.isInfinite || percentVal.isNaN) percentVal = 100; + onProgress(percentVal, received, fileSize); + } + }, + ), + ); + + await dataSocket.addStream(readStream); + await dataSocket.flush(); + await dataSocket.close(); + + if (!isTransferCompleted) { + // Test if All data are well transferred + response = await _socket.readResponse(); + if (!response.isSuccessCode()) { + throw FTPConnectException('Transfer Error.', response.message); + } + } + + _socket.logger.log('File Uploaded!'); + return true; + } +} diff --git a/lib/core/utils/ftpconnect/src/ftp_entry.dart b/lib/core/utils/ftpconnect/src/ftp_entry.dart new file mode 100644 index 000000000..bdf1a3623 --- /dev/null +++ b/lib/core/utils/ftpconnect/src/ftp_entry.dart @@ -0,0 +1,265 @@ +import 'package:intl/intl.dart'; + +import 'ftp_exceptions.dart'; +import 'ftpconnect_base.dart'; + +class FTPEntry { + final String name; + final DateTime? modifyTime; + final String? permission; + final FTPEntryType type; + final int? size; + final String? unique; + final String? group; + final int? gid; + final String? mode; + final String? owner; + final int? uid; + final Map? additionalProperties; + + static final RegExp regexpLIST = RegExp(r"^([\-ld])" // Directory flag [1] + r"([\-rwxs]{9})\s+" // Permissions [2] + r"(\d+)\s+" // Number of items [3] + r"(\w+)\s+" // File owner [4] + r"(\w+)\s+" // File group [5] + r"(\d+)\s+" // File size in bytes [6] + r"(\w{3}\s+\d{1,2}\s+(?:\d{1,2}:\d{1,2}|\d{4}))\s+" // date[7] + 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] + ); + + // Hide constructor + FTPEntry._( + this.name, + this.modifyTime, + this.permission, + this.type, + this.size, + this.unique, + this.group, + this.gid, + this.mode, + this.owner, + this.uid, + this.additionalProperties); + + factory FTPEntry.parse(String responseLine, ListCommand cmd) { + if (responseLine.trim().isEmpty) { + throw FTPConnectException("Can't parse a null or blank response line"); + } + if (cmd == ListCommand.LIST) { + return FTPEntry._parseListCommand(responseLine); + } else if (cmd == ListCommand.NLST) { + return FTPEntry._(responseLine, null, null, FTPEntryType.UNKNOWN, null, + null, null, null, null, null, null, null); + } else { + return FTPEntry._parseMLSDCommand(responseLine); + } + } + + factory FTPEntry._parseMLSDCommand(final String responseLine) { + String _name = ""; + DateTime? _modifyTime; + String? _permission; + FTPEntryType _type = FTPEntryType.UNKNOWN; + int _size = 0; + String? _unique; + String? _group; + int _gid = -1; + String? _mode; + String? _owner; + int _uid = -1; + Map _additional = {}; + + // Split and trim line + responseLine.trim().split(';').forEach((property) { + final prop = property + .split('=') + .map((part) => part.trim()) + .toList(growable: false); + + if (prop.length == 1) { + // Name + _name = prop[0]; + } else { + // Other attributes + switch (prop[0].toLowerCase()) { + case 'modify': + final String date = + prop[1].substring(0, 8) + 'T' + prop[1].substring(8); + _modifyTime = DateTime.tryParse(prop[1]) ?? DateTime.tryParse(date); + break; + case 'perm': + _permission = prop[1]; + break; + case 'size': + _size = int.parse(prop[1]); + break; + case 'type': + if (prop[1] == 'dir') { + _type = FTPEntryType.DIR; + } else if (prop[1] == 'file') { + _type = FTPEntryType.FILE; + } else { + _type = FTPEntryType.LINK; + } + break; + case 'unique': + _unique = prop[1]; + break; + case 'unix.group': + _group = prop[1]; + break; + case 'unix.gid': + _gid = int.parse(prop[1]); + break; + case 'unix.mode': + _mode = prop[1]; + break; + case 'unix.owner': + _owner = prop[1]; + break; + case 'unix.uid': + _uid = int.parse(prop[1]); + break; + default: + _additional.putIfAbsent(prop[0], () => prop[1]); + break; + } + } + }); + + return FTPEntry._(_name, _modifyTime, _permission, _type, _size, _unique, + _group, _gid, _mode, _owner, _uid, Map.unmodifiable(_additional)); + } + + ///reference http://cr.yp.to/ftp/list/binls.html + ///-rw-r--r-- 1 owner group 213 Aug 26 16:31 FileName.txt + ///d for Dir + ///- for file + /// + /// SII servers format: + /// 02-11-15 03:05PM 1410887680 directory + /// 02-11-15 03:05PM 1410887680 file.avi + factory FTPEntry._parseListCommand(final String responseLine) { + if (regexpLIST.hasMatch(responseLine)) { + return FTPEntry._parseLIST(responseLine); + } else if (regexpLISTSiiServers.hasMatch(responseLine)) { + return FTPEntry._parseLISTiis(responseLine); + } else { + throw FTPConnectException( + 'Invalid format <$responseLine> for LIST command response !'); + } + } + + factory FTPEntry._parseLIST(final String responseLine) { + String _name = ""; + DateTime? _modifyTime; + String? _persmission; + FTPEntryType _type = FTPEntryType.UNKNOWN; + int _size = 0; + String? _unique; + String? _group; + int _gid = -1; + String? _mode; + String? _owner; + int _uid = -1; + + Iterable matches = regexpLIST.allMatches(responseLine); + for (Match match in matches) { + if (match.group(1) == "-") { + _type = FTPEntryType.FILE; + } else if (match.group(1) == "d") { + _type = FTPEntryType.DIR; + } else { + _type = FTPEntryType.LINK; + } + + //permission + _persmission = match.group(2); + //nb files + //var nbFiles = match.group(3); + //owner + _owner = match.group(4); + //group + _group = match.group(5); + //size + _size = int.tryParse(match.group(6)!) ?? 0; + //date + String date = (match.group(7)!.split(" ")..removeWhere((i) => i.isEmpty)) + .join(" "); //keep only one space + //insert year + if (date.contains(':')) date = '$date ${DateTime.now().year}'; + var format = date.contains(':') ? 'MMM dd hh:mm yyyy' : 'MMM dd yyyy'; + _modifyTime = DateFormat(format, 'en_US').parse(date); + //file/dir name + _name = match.group(8)!; + } + return FTPEntry._(_name, _modifyTime, _persmission, _type, _size, _unique, + _group, _gid, _mode, _owner, _uid, {}); + } + + factory FTPEntry._parseLISTiis(final String responseLine) { + String _name = ""; + DateTime? _modifyTime; + String? _persmission; + FTPEntryType _type = FTPEntryType.UNKNOWN; + int _size = 0; + String? _unique; + String? _group; + int _gid = -1; + String? _mode; + String? _owner; + 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'; + }); + _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")) { + _type = FTPEntryType.DIR; + } else { + _type = FTPEntryType.LINK; + } + //size + _size = int.tryParse(match.group(3)!) ?? 0; + + //file/dir name + _name = match.group(4)!; + } + return FTPEntry._(_name, _modifyTime, _persmission, _type, _size, _unique, + _group, _gid, _mode, _owner, _uid, {}); + } + + @override + String toString() => + 'name=$name;modify=$modifyTime;perm=$permission;type=${type.describeEnum.toLowerCase()};size=$size;unique=$unique;unix.group=$group;unix.mode=$mode;unix.owner=$owner;unix.uid=$uid;unix.gid=$gid'; +} + +enum FTPEntryType { FILE, DIR, LINK, UNKNOWN } + +extension FtpEntryTypeEnum on FTPEntryType { + String get describeEnum => + this.toString().substring(this.toString().indexOf('.') + 1); +} diff --git a/lib/core/utils/ftpconnect/src/ftp_exceptions.dart b/lib/core/utils/ftpconnect/src/ftp_exceptions.dart new file mode 100644 index 000000000..59368b2ba --- /dev/null +++ b/lib/core/utils/ftpconnect/src/ftp_exceptions.dart @@ -0,0 +1,11 @@ +class FTPConnectException implements Exception { + final String message; + final String? response; + + FTPConnectException(this.message, [this.response]); + + @override + String toString() { + return 'FTPConnectException: $message (Response: $response)'; + } +} diff --git a/lib/core/utils/ftpconnect/src/ftp_reply.dart b/lib/core/utils/ftpconnect/src/ftp_reply.dart new file mode 100644 index 000000000..c4538f176 --- /dev/null +++ b/lib/core/utils/ftpconnect/src/ftp_reply.dart @@ -0,0 +1,24 @@ +import 'dart:core'; + +class FTPReply { + final int _code; + final String _messages; + + FTPReply(this._code, this._messages); + + get code => this._code; + + get message => this._messages; + + bool isSuccessCode() { + int aux = this._code - 200; + return aux >= 0 && aux < 100; + } + + @override + String toString() { + StringBuffer buffer = new StringBuffer(); + buffer.write("FTPReply = [code= $_code, message= $_messages]"); + return buffer.toString(); + } +} diff --git a/lib/core/utils/ftpconnect/src/ftp_socket.dart b/lib/core/utils/ftpconnect/src/ftp_socket.dart new file mode 100644 index 000000000..cdb28c7be --- /dev/null +++ b/lib/core/utils/ftpconnect/src/ftp_socket.dart @@ -0,0 +1,221 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import '../ftpconnect.dart'; +import 'ftp_reply.dart'; + +class FTPSocket { + final String host; + final int port; + final Logger logger; + final int timeout; + final SecurityType securityType; + late RawSocket _socket; + TransferMode transferMode = TransferMode.passive; + TransferType _transferType = TransferType.auto; + ListCommand listCommand = ListCommand.MLSD; + bool supportIPV6 = false; + + FTPSocket(this.host, this.port, this.securityType, this.logger, this.timeout); + + /// Set current transfer type of socket + /// + /// Supported types are: [TransferType.auto], [TransferType.ascii], [TransferType.binary], + TransferType get transferType => _transferType; + + /// Read the FTP Server response from the Stream + /// + /// Blocks until data is received! + Future readResponse() async { + StringBuffer res = StringBuffer(); + await Future.doWhile(() async { + bool dataReceivedSuccessfully = false; + + //this is used to read all data for specific command line + while (_socket.available() > 0) { + res.write(String.fromCharCodes(_socket.read()!).trim()); + dataReceivedSuccessfully = true; + } + if (dataReceivedSuccessfully) return false; + + await Future.delayed(Duration(milliseconds: 300)); + return true; + }).timeout(Duration(seconds: timeout), onTimeout: () { + throw FTPConnectException('Timeout reached for Receiving response !'); + }); + + String r = res.toString(); + if (r.startsWith("\n")) r = r.replaceFirst("\n", ""); + + if (r.length < 3) throw FTPConnectException("Illegal Reply Exception", r); + + int? code; + List lines = r.split('\n'); + //get last code + String? line; + for (line in lines) { + if (line.length >= 3) code = int.tryParse(line.substring(0, 3)) ?? code; + } + //multiline response + if (line != null && line.length >= 4 && line[3] == '-') + return await readResponse(); + + if (code == null) throw FTPConnectException("Illegal Reply Exception", r); + + FTPReply reply = FTPReply(code, r); + logger.log('< ${reply.toString()}'); + return reply; + } + + /// Send a command [cmd] to the FTP Server + /// if [waitResponse] the function waits for the reply, other wise return '' + Future sendCommand(String cmd) { + logger.log('> $cmd'); + _socket.write(Utf8Codec().encode('$cmd\r\n')); + + return readResponse(); + } + + /// Send a command [cmd] to the FTP Server + /// if [waitResponse] the function waits for the reply, other wise return '' + void sendCommandWithoutWaitingResponse(String cmd) async { + logger.log('> $cmd'); + _socket.write(Utf8Codec().encode('$cmd\r\n')); + } + + /// Connect to the FTP Server and Login with [user] and [pass] + Future connect(String user, String pass, {String? account}) async { + logger.log('Connecting...'); + + final timeout = Duration(seconds: this.timeout); + + try { + // FTPS starts secure + if (securityType == SecurityType.FTPS) { + _socket = await RawSecureSocket.connect( + host, + port, + timeout: timeout, + onBadCertificate: (certificate) => true, + ); + } else { + _socket = await RawSocket.connect( + host, + port, + timeout: timeout, + ); + } + } catch (e) { + throw FTPConnectException( + 'Could not connect to $host ($port)', e.toString()); + } + + logger.log('Connection established, waiting for welcome message...'); + await readResponse(); + + // FTPES needs to be upgraded prior to getting a welcome + if (securityType == SecurityType.FTPES) { + FTPReply lResp = await sendCommand('AUTH TLS'); + if (!lResp.isSuccessCode()) { + lResp = await sendCommand('AUTH SSL'); + if (!lResp.isSuccessCode()) { + throw FTPConnectException( + 'FTPES cannot be applied: the server refused both AUTH TLS and AUTH SSL commands', + lResp.message); + } + } + + _socket = await RawSecureSocket.secure(_socket, + onBadCertificate: (certificate) => true); + } + + if ([SecurityType.FTPES, SecurityType.FTPS].contains(securityType)) { + await sendCommand('PBSZ 0'); + await sendCommand('PROT P'); + } + + // Send Username + FTPReply lResp = await sendCommand('USER $user'); + + //password required + if (lResp.code == 331) { + lResp = await sendCommand('PASS $pass'); + if (lResp.code == 332) { + if (account == null) throw FTPConnectException('Account required'); + lResp = await sendCommand('ACCT $account'); + if (!lResp.isSuccessCode()) { + throw FTPConnectException('Wrong Account', lResp.message); + } + } else if (!lResp.isSuccessCode()) { + throw FTPConnectException('Wrong Username/password', lResp.message); + } + //account required + } else if (lResp.code == 332) { + if (account == null) throw FTPConnectException('Account required'); + lResp = await sendCommand('ACCT $account'); + if (!lResp.isSuccessCode()) { + throw FTPConnectException('Wrong Account', lResp.message); + } + } else if (!lResp.isSuccessCode()) { + throw FTPConnectException('Wrong username $user', lResp.message); + } + + logger.log('Connected!'); + return true; + } + + Future openDataTransferChannel() async { + FTPReply res = FTPReply(200, ""); + if (transferMode == TransferMode.active) { + //todo later + } else { + res = await sendCommand(supportIPV6 ? 'EPSV' : 'PASV'); + if (!res.isSuccessCode()) { + throw FTPConnectException('Could not start Passive Mode', res.message); + } + } + + return res; + } + + /// Set the Transfer mode on [socket] to [mode] + Future setTransferType(TransferType pTransferType) async { + //if we already in the same transfer type we do nothing + if (_transferType == pTransferType) return; + switch (pTransferType) { + case TransferType.auto: + // Set to ASCII mode + await sendCommand('TYPE A'); + break; + case TransferType.ascii: + // Set to ASCII mode + await sendCommand('TYPE A'); + break; + case TransferType.binary: + // Set to BINARY mode + await sendCommand('TYPE I'); + break; + default: + break; + } + _transferType = pTransferType; + } + + // Disconnect from the FTP Server + Future disconnect() async { + logger.log('Disconnecting...'); + + try { + await sendCommand('QUIT'); + } catch (ignored) { + // Ignore + } finally { + await _socket.close(); + _socket.shutdown(SocketDirection.both); + } + + logger.log('Disconnected!'); + return true; + } +} diff --git a/lib/core/utils/ftpconnect/src/ftpconnect_base.dart b/lib/core/utils/ftpconnect/src/ftpconnect_base.dart new file mode 100644 index 000000000..f7e8f0085 --- /dev/null +++ b/lib/core/utils/ftpconnect/src/ftpconnect_base.dart @@ -0,0 +1,307 @@ +import 'dart:io'; + +import '../ftpconnect.dart'; +import 'package:path/path.dart'; + +import 'commands/directory.dart'; +import 'commands/file.dart'; +import 'ftp_reply.dart'; +import 'ftp_socket.dart'; +import 'utils.dart'; + +class FTPConnect { + final String _user; + final String _pass; + late FTPSocket _socket; + + /// Create a FTP Client instance + /// + /// [host]: Hostname or IP Address + /// [port]: Port number (Defaults to 21 for FTP and FTPES, 990 for FTPS) + /// [user]: Username (Defaults to anonymous) + /// [pass]: Password if not anonymous login + /// [debug]: Enable Debug Logging + /// [timeout]: Timeout in seconds to wait for responses + FTPConnect( + String host, { + int? port, + String user = 'anonymous', + String pass = '', + bool showLog = false, + SecurityType securityType = SecurityType.FTP, + Logger? logger, + int timeout = 30, + }) : _user = user, + _pass = pass { + port ??= securityType == SecurityType.FTPS ? 990 : 21; + _socket = FTPSocket( + host, + port, + securityType, + logger != null ? logger : Logger(isEnabled: showLog), + timeout, + ); + } + + set transferMode(TransferMode pTransferMode) { + _socket.transferMode = pTransferMode; + } + + set listCommand(ListCommand pListCommand) { + _socket.listCommand = pListCommand; + } + + set supportIPV6(bool pSupportIPV6) => _socket.supportIPV6 = pSupportIPV6; + + /// Set current transfer type of connection + /// + /// Supported types are: [TransferType.auto], [TransferType.ascii], [TransferType.binary], + Future setTransferType(TransferType pTransferType) async { + if (_socket.transferType == pTransferType) return; + await _socket.setTransferType(pTransferType); + } + + /// Connect to the FTP Server + /// return true if we are connected successfully + Future connect() => _socket.connect(_user, _pass); + + /// Disconnect from the FTP Server + /// return true if we are disconnected successfully + Future disconnect() => _socket.disconnect(); + + Future sendCustomCommand(String pCmd) => _socket.sendCommand(pCmd); + + /// Upload the File [fFile] to the current directory + Future uploadFile( + File fFile, { + String sRemoteName = '', + FileProgress? onProgress, + }) { + return FTPFile(_socket).upload( + fFile, + remoteName: sRemoteName, + onProgress: onProgress, + ); + } + + /// Download the Remote File [sRemoteName] to the local File [fFile] + Future downloadFile( + String? sRemoteName, + File fFile, { + FileProgress? onProgress, + }) { + return FTPFile(_socket) + .download(sRemoteName, fFile, onProgress: onProgress); + } + + /// Create a new Directory with the Name of [sDirectory] in the current directory. + /// + /// Returns `true` if the directory was created successfully + /// Returns `false` if the directory could not be created or already exists + Future makeDirectory(String sDirectory) { + return FTPDirectory(_socket).makeDirectory(sDirectory); + } + + /// Deletes the Directory with the Name of [sDirectory] in the current directory. + /// + /// Returns `true` if the directory was deleted successfully + /// Returns `false` if the directory could not be deleted or does not nexist + Future deleteEmptyDirectory(String? sDirectory) { + return FTPDirectory(_socket).deleteEmptyDirectory(sDirectory); + } + + /// Deletes the Directory with the Name of [sDirectory] in the current directory. + /// + /// Returns `true` if the directory was deleted successfully + /// Returns `false` if the directory could not be deleted or does not nexist + /// THIS USEFUL TO DELETE NON EMPTY DIRECTORY + Future deleteDirectory(String sDirectory) async { + String currentDir = await this.currentDirectory(); + if (!await this.changeDirectory(sDirectory)) { + throw FTPConnectException("Couldn't change directory to $sDirectory"); + } + List dirContent = await this.listDirectoryContent(); + await Future.forEach(dirContent, (FTPEntry entry) async { + if (entry.type == FTPEntryType.FILE) { + if (!await deleteFile(entry.name)) { + throw FTPConnectException("Couldn't delete file ${entry.name}"); + } + } else { + if (!await deleteDirectory(entry.name)) { + throw FTPConnectException("Couldn't delete folder ${entry.name}"); + } + } + }); + await this.changeDirectory(currentDir); + return await deleteEmptyDirectory(sDirectory); + } + + /// Change into the Directory with the Name of [sDirectory] within the current directory. + /// + /// Use `..` to navigate back + /// Returns `true` if the directory was changed successfully + /// Returns `false` if the directory could not be changed (does not exist, no permissions or another error) + Future changeDirectory(String? sDirectory) { + return FTPDirectory(_socket).changeDirectory(sDirectory); + } + + /// Returns the current directory + Future currentDirectory() { + return FTPDirectory(_socket).currentDirectory(); + } + + /// Returns the content of the current directory + /// [cmd] refer to the used command for the server, there is servers working + /// with MLSD and other with LIST + Future> listDirectoryContent() { + return FTPDirectory(_socket).directoryContent(); + } + + /// Returns the content names of the current directory + /// [cmd] refer to the used command for the server, there is servers working + /// with MLSD and other with LIST for detailed content + Future> listDirectoryContentOnlyNames() { + return FTPDirectory(_socket).directoryContentNames(); + } + + /// Rename a file (or directory) from [sOldName] to [sNewName] + Future rename(String sOldName, String sNewName) { + return FTPFile(_socket).rename(sOldName, sNewName); + } + + /// Delete the file [sFilename] from the server + Future deleteFile(String? sFilename) { + return FTPFile(_socket).delete(sFilename); + } + + /// check the existence of the file [sFilename] from the server + Future existFile(String sFilename) { + return FTPFile(_socket).exist(sFilename); + } + + /// returns the file [sFilename] size from server, + /// returns -1 if file does not exist + Future sizeFile(String sFilename) { + return FTPFile(_socket).size(sFilename); + } + + /// Upload the File [fileToUpload] to the current directory + /// if [pRemoteName] is not setted the remote file will take take the same local name + /// [pRetryCount] number of attempts + /// + /// this strategy can be used when we don't need to go step by step + /// (connect -> upload -> disconnect) or there is a need for a number of attemps + /// in case of a poor connexion for example + Future uploadFileWithRetry( + File fileToUpload, { + String pRemoteName = '', + int pRetryCount = 1, + FileProgress? onProgress, + }) { + Future uploadFileRetry() async { + bool res = await this.uploadFile( + fileToUpload, + sRemoteName: pRemoteName, + onProgress: onProgress, + ); + return res; + } + + return Utils.retryAction(() => uploadFileRetry(), pRetryCount); + } + + /// Download the Remote File [pRemoteName] to the local File [pLocalFile] + /// [pRetryCount] number of attempts + /// + /// this strategy can be used when we don't need to go step by step + /// (connect -> download -> disconnect) or there is a need for a number of attempts + /// in case of a poor connexion for example + Future downloadFileWithRetry( + String pRemoteName, + File pLocalFile, { + int pRetryCount = 1, + FileProgress? onProgress, + }) { + Future downloadFileRetry() async { + bool res = await this.downloadFile( + pRemoteName, + pLocalFile, + onProgress: onProgress, + ); + return res; + } + + return Utils.retryAction(() => downloadFileRetry(), pRetryCount); + } + + /// Download the Remote Directory [pRemoteDir] to the local File [pLocalDir] + /// [pRetryCount] number of attempts + Future downloadDirectory(String pRemoteDir, Directory pLocalDir, + {int pRetryCount = 1}) { + Future downloadDir(String? pRemoteDir, Directory pLocalDir) async { + await pLocalDir.create(recursive: true); + + //read remote directory content + if (!await this.changeDirectory(pRemoteDir)) { + throw FTPConnectException('Cannot download directory', + '$pRemoteDir not found or inaccessible !'); + } + List dirContent = await this.listDirectoryContent(); + await Future.forEach(dirContent, (FTPEntry entry) async { + if (entry.type == FTPEntryType.FILE) { + File localFile = File(join(pLocalDir.path, entry.name)); + await downloadFile(entry.name, localFile); + } else if (entry.type == FTPEntryType.DIR) { + //create a local directory + var localDir = await Directory(join(pLocalDir.path, entry.name)) + .create(recursive: true); + await downloadDir(entry.name, localDir); + //back to current folder + await this.changeDirectory('..'); + } + }); + return true; + } + + Future downloadDirRetry() async { + bool res = await downloadDir(pRemoteDir, pLocalDir); + return res; + } + + return Utils.retryAction(() => downloadDirRetry(), pRetryCount); + } + + /// check the existence of the Directory with the Name of [pDirectory]. + /// + /// Returns `true` if the directory was changed successfully + /// Returns `false` if the directory could not be changed (does not exist, no permissions or another error) + Future checkFolderExistence(String pDirectory) { + return this.changeDirectory(pDirectory); + } + + /// Create a new Directory with the Name of [pDirectory] in the current directory if it does not exist. + /// + /// Returns `true` if the directory exists or was created successfully + /// Returns `false` if the directory not found and could not be created + Future createFolderIfNotExist(String pDirectory) async { + if (!await checkFolderExistence(pDirectory)) { + return this.makeDirectory(pDirectory); + } + return true; + } +} + +///Note that [LIST] and [MLSD] return content detailed +///BUT [NLST] return only dir/file names inside the given directory +enum ListCommand { NLST, LIST, MLSD } + +enum TransferType { auto, ascii, binary } + +enum TransferMode { active, passive } + +enum SecurityType { FTP, FTPS, FTPES } + +extension CommandListTypeEnum on ListCommand { + String get describeEnum => + this.toString().substring(this.toString().indexOf('.') + 1); +} diff --git a/lib/core/utils/ftpconnect/src/logger.dart b/lib/core/utils/ftpconnect/src/logger.dart new file mode 100644 index 000000000..ce4a1ad6d --- /dev/null +++ b/lib/core/utils/ftpconnect/src/logger.dart @@ -0,0 +1,13 @@ +class Logger { + final bool isEnabled; + + Logger({this.isEnabled = false}); + + void log(String pMessage) { + if (isEnabled) _printLog(pMessage); + } + + void _printLog(String pMessage) { + print('[${DateTime.now().toString()}] $pMessage'); + } +} diff --git a/lib/core/utils/ftpconnect/src/utils.dart b/lib/core/utils/ftpconnect/src/utils.dart new file mode 100644 index 000000000..6fbd41baa --- /dev/null +++ b/lib/core/utils/ftpconnect/src/utils.dart @@ -0,0 +1,57 @@ +import 'dart:async'; + +class Utils { + Utils._(); + + static int parsePort(String response, bool isIPV6) { + return isIPV6 ? parsePortEPSV(response) : parsePortPASV(response); + } + + /// Parse the Passive Mode Port from the Servers [sResponse] + /// port format (|||xxxxx|) + static int parsePortEPSV(String sResponse) { + int iParOpen = sResponse.indexOf('('); + int iParClose = sResponse.indexOf(')'); + + if (iParClose > -1 && iParOpen > -1) { + sResponse = sResponse.substring(iParOpen + 4, iParClose - 1); + } + return int.parse(sResponse); + } + + /// Parse the Passive Mode Port from the Servers [sResponse] + /// format 227 Entering Passive Mode (192,168,8,36,8,75). + static int parsePortPASV(String sResponse) { + int iParOpen = sResponse.indexOf('('); + int iParClose = sResponse.indexOf(')'); + + String sParameters = sResponse.substring(iParOpen + 1, iParClose); + List lstParameters = sParameters.split(','); + + int iPort1 = int.parse(lstParameters[lstParameters.length - 2]); + int iPort2 = int.parse(lstParameters[lstParameters.length - 1]); + + return (iPort1 * 256) + iPort2; + } + + ///retry a function [retryCount] times, until exceed [retryCount] or execute the function successfully + ///Return true if the future executed successfully , false other wises + static Future retryAction(FutureOr action(), retryCount) async { + int lAttempts = 1; + bool result = true; + await Future.doWhile(() async { + try { + result = await action(); + //if there is no exception we exit the loop (return false to exit) + return false; + } catch (e) { + if (lAttempts++ >= retryCount) { + throw e; + } + } + //return true to loop again + return true; + }); + return result; + } +} From d51bc61c432354b978034f9887f66e2f03060865 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Sat, 5 Jul 2025 17:05:29 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=9F=B3=E4=B9=90?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E7=8A=B6=E6=80=81=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/services/music_manager.dart | 2 +- .../agent/controllers/agent_controller.dart | 53 +++++++++---------- lib/modules/agent/views/music_card.dart | 2 +- .../music/views/music_playlist_view.dart | 15 +++--- .../agent_service/AgentService.kt | 8 +-- .../music_service/MusicService.kt | 46 ++++++++-------- .../music_service/lib/music_service.dart | 7 +-- 7 files changed, 63 insertions(+), 70 deletions(-) diff --git a/lib/data/services/music_manager.dart b/lib/data/services/music_manager.dart index 621cd927c..51975424f 100644 --- a/lib/data/services/music_manager.dart +++ b/lib/data/services/music_manager.dart @@ -105,7 +105,7 @@ class MusicManager extends GetxService { required Function() onPrevious, required Function(int) onSeekTo, required Function() onStop, - required Function(String, int, int, double) onStop1, + required Function(int, int, int, double) onStop1, }) async { try { await _musicService.setMediaSessionCallbacks( diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 6c69ad897..8eec19a15 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -5,7 +5,6 @@ import 'dart:io'; import 'package:amap_service/amap_service.dart'; import 'package:deep_voice/core/utils/upload_oss.dart'; import 'package:deep_voice/data/models/user_Info.dart'; -import 'package:deep_voice/data/services/location_manager.dart'; import 'package:deep_voice/data/services/music_manager.dart'; import 'package:deep_voice/data/services/navigation_manager.dart'; import 'package:deep_voice/data/services/network/api.dart'; @@ -126,11 +125,11 @@ class AgentController extends GetxController { // 添加加载状态跟踪 final RxBool isLoadingMusic = false.obs; - final RxString loadingMusicId = ''.obs; + final RxInt loadingMusicId = 0.obs; // 设置加载状态的方法 - void setLoadingMusic(String id, bool loading) { - loadingMusicId.value = loading ? id : ''; + void setLoadingMusic(int id, bool loading) { + loadingMusicId.value = loading ? id : 0; isLoadingMusic.value = loading; } @@ -1333,7 +1332,7 @@ class AgentController extends GetxController { try { var status = await _musiceManager.getStatus(); isPlaying.value = status['isPlaying'] ?? false; - currentId.value = status['id'] ?? 0; + currentId.value = status['id'] as int; Logger.d('AgentController', '音乐状态更新: playing=${isPlaying.value}, url=${currentId.value}'); print("音乐状态更新: playing=${isPlaying.value}, url=${currentId.value}"); @@ -1351,16 +1350,14 @@ class AgentController extends GetxController { isPlaying.value = true; // 修复:确保currentId接收int类型 final songId = song['id']; - currentId.value = songId is int - ? songId - : (int.tryParse(songId?.toString() ?? '0') ?? 0); + currentId.value = songId ?? 0; // 重置进度状态 currentPosition.value = 0; duration.value = 0; progress.value = 0.0; - final id = song['id'] ?? ""; + final id = song['id'] ?? 0; final timestampMs = song['ts'] ?? 0; // 检查URL是否需要刷新(只有当有时间戳时才检查) @@ -1382,9 +1379,7 @@ class AgentController extends GetxController { song["url"] = urldata['url'] ?? ''; // 修复:确保currentId接收int类型 final newSongId = song['id']; - currentId.value = newSongId is int - ? newSongId - : (int.tryParse(newSongId?.toString() ?? '0') ?? 0); + currentId.value = newSongId ?? 0; Logger.d(TAG, '获取新URL成功'); } catch (e) { Logger.e(TAG, '获取新URL失败: $e'); @@ -1394,8 +1389,8 @@ class AgentController extends GetxController { } // 转换为MusicManager期望的格式 - final musicData = { - 'id': song['id']?.toString() ?? '', + final musicData = { + 'id': song['id'], 'url': song['url']?.toString() ?? '', 'title': song['title']?.toString() ?? song['name']?.toString() ?? '', 'artist': @@ -1521,10 +1516,10 @@ class AgentController extends GetxController { } // 获取播放进度 - Future updateMusicProgress1(String currentIdInternal, int newPosition, + Future updateMusicProgress1(int currentIdInternal, int newPosition, int newDuration, double newProgress) async { try { - if (int.parse(currentIdInternal) != 0) { + if (currentIdInternal != 0) { isPlaying.value = true; // return; // 如果没有在播放,直接返回 } @@ -1533,11 +1528,11 @@ class AgentController extends GetxController { // final newPosition = progressInfo['currentPosition'] ?? 0; // final newDuration = progressInfo['duration'] ?? 0; // final newProgress = progressInfo['progress'] ?? 0.0; - print( - "updateMusicProgress1:currentIdInternal:$currentIdInternal newPosition:$newPosition newDuration:$newDuration newProgress:$newProgress"); + // print( + // "updateMusicProgress1:currentIdInternal:$currentIdInternal newPosition:$newPosition newDuration:$newDuration newProgress:$newProgress"); bool hasUpdate = false; - if (currentId.value != int.parse(currentIdInternal)) { - currentId.value = int.parse(currentIdInternal); + if (currentId.value != currentIdInternal) { + currentId.value = currentIdInternal; hasUpdate = true; } // 只有当值发生变化时才更新,避免不必要的UI刷新 @@ -1610,7 +1605,7 @@ class AgentController extends GetxController { } // 跳转到指定位置 - Future seekToPosition1(String currentIdInternal, int newPosition, + Future seekToPosition1(int currentIdInternal, int newPosition, int newDuration, double newProgress) async { try { //await _musiceManager.seekTo(positionMs); @@ -1676,7 +1671,7 @@ class AgentController extends GetxController { // 设置加载状态 isLoadingMusic.value = true; - loadingMusicId.value = playlist[0]['id']?.toString() ?? ''; + loadingMusicId.value = playlist[0]['id']; // 重置播放状态 isPlaying.value = true; @@ -1687,7 +1682,7 @@ class AgentController extends GetxController { // 格式化歌曲列表数据 final formattedPlaylist = playlist.map((song) { return { - 'id': song['id']?.toString() ?? '', + 'id': song['id'], 'url': song['url']?.toString() ?? '', 'title': song['title']?.toString() ?? song['name']?.toString() ?? '', 'artist': @@ -1702,7 +1697,7 @@ class AgentController extends GetxController { // 更新当前播放歌曲ID if (formattedPlaylist.isNotEmpty) { - currentId.value = int.tryParse(formattedPlaylist[0]['id'] ?? '0') ?? 0; + currentId.value = formattedPlaylist[0]['id'] ?? 0; } // 启动进度定时器 @@ -1726,7 +1721,7 @@ class AgentController extends GetxController { } finally { // 清除加载状态 isLoadingMusic.value = false; - loadingMusicId.value = ''; + loadingMusicId.value = 0; } } @@ -1778,7 +1773,7 @@ class AgentController extends GetxController { // 查找当前歌曲在列表中的索引 int index = -1; for (int i = 0; i < playlist.length; i++) { - if (int.tryParse(playlist[i]['id']?.toString() ?? '0') == songId) { + if (playlist[i]['id'] == songId) { index = i; break; } @@ -1837,10 +1832,10 @@ class AgentController extends GetxController { } /// 处理来自系统通知栏的跳转操作 - Future onMediaSessionSeekTo1(String currentIdInternal, int newPosition, + Future onMediaSessionSeekTo1(int currentIdInternal, int newPosition, int newDuration, double newProgress) async { - print( - "onMediaSessionSeekTo1 currentIdInternal :$currentIdInternal ,newPosition:$newPosition newDuration:$newDuration newProgress:$newProgress"); + // print( + // "onMediaSessionSeekTo1 currentIdInternal :$currentIdInternal ,newPosition:$newPosition newDuration:$newDuration newProgress:$newProgress"); await seekToPosition1( currentIdInternal, newPosition, newDuration, newProgress); } diff --git a/lib/modules/agent/views/music_card.dart b/lib/modules/agent/views/music_card.dart index f4af0c4fb..e00cb158a 100644 --- a/lib/modules/agent/views/music_card.dart +++ b/lib/modules/agent/views/music_card.dart @@ -200,7 +200,7 @@ Widget buildMusicContent( ...playlist.asMap().entries.take(4).map((entry) { final index = entry.key; final item = entry.value; - final id = item["id"] ?? 0; + final id = item["id"]; final name = item["name"] ?? "未知歌曲"; final url = item["url"] ?? ""; final singer = item["sgener"] ?? "未知歌手"; diff --git a/lib/modules/music/views/music_playlist_view.dart b/lib/modules/music/views/music_playlist_view.dart index 69fa15a85..ec63a75c2 100644 --- a/lib/modules/music/views/music_playlist_view.dart +++ b/lib/modules/music/views/music_playlist_view.dart @@ -1,8 +1,6 @@ import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:flutter/services.dart'; - import '../../../core/utils/logger.dart'; import '../../agent/controllers/agent_controller.dart'; import '../../agent/views/circular_progress_button.dart'; @@ -80,12 +78,11 @@ class MusicPlaylistView extends StatelessWidget { if (controller.currentId.value != 0) { try { currentSong = playlist.firstWhere( - (song) => - int.tryParse(song['id']?.toString() ?? '0') == - controller.currentId.value, + (song) => song['id'] == controller.currentId.value, ); } catch (e) { // 如果没找到,使用第一首歌曲 + currentSong = playlist.isNotEmpty ? playlist.first : null; } } else { @@ -192,7 +189,7 @@ class MusicPlaylistView extends StatelessWidget { itemCount: playlist.length, itemBuilder: (context, index) { final item = playlist[index]; - final id = item["id"]?.toString() ?? ""; + final id = item["id"] ?? 0; final name = item["name"] ?? "未知歌曲"; final url = item["url"] ?? ""; final singer = item["singer"] ?? item["sgener"] ?? "未知歌手"; @@ -215,7 +212,7 @@ class MusicPlaylistView extends StatelessWidget { Widget _buildMusicItem( BuildContext context, AgentController controller, - String id, + int id, String name, String url, String singer, @@ -323,7 +320,7 @@ class MusicPlaylistView extends StatelessWidget { SizedBox(width: 14.w), // 播放按钮 Obx(() { - final currentIdInt = int.tryParse(id) ?? 0; + final currentIdInt = id; final isThisPlaying = controller.isPlaying.value && controller.currentId.value == currentIdInt; final currentProgress = @@ -601,7 +598,7 @@ class MusicPlaylistView extends StatelessWidget { // 播放选中的歌曲 void _playSelectedSong(AgentController controller, Map song, List> playlist) { - final songId = int.tryParse(song['id']?.toString() ?? '0') ?? 0; + final songId = song['id'] ?? 0; controller.updateCurrentPlaylistContext(playlist, songId); controller.musicPlay(song); } diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 132b8efa1..b51f5aa2b 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -1447,12 +1447,12 @@ object AgentService : CoroutineScope { // Log.d(TAG, "mcp调用结果: $metaStr") if (meta.has("card_music")) { //音乐卡片 val cardMusic = meta.getJSONObject("card_music") - val id = cardMusic.optInt("id", 0) + val id = cardMusic.optLong("id", 0) val url = cardMusic.optString("url", "") val name = cardMusic.optString("name", "") val sgener = cardMusic.optString("sgener", "") val image = cardMusic.optString("image", "") - val ts = cardMusic.optInt("ts", 0) + val ts = cardMusic.optLong("ts", 0) processMusicPlay( mapOf( "id" to id, @@ -1470,12 +1470,12 @@ object AgentService : CoroutineScope { val playlist = mutableListOf>() for (i in 0 until musics.length()) { val item = musics.optJSONObject(i) ?: continue - val id = item.optInt("id", 0) + val id = item.optLong("id", 0) val url = item.optString("url", "") val name = item.optString("name", "") val sgener = item.optString("sgener", "") val image = item.optString("image", "") - val ts = item.optInt("ts", 0) + val ts = item.optLong("ts", 0) playlist.add( mapOf( "id" to id, diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt index 2e11c4301..0956f50d8 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt @@ -38,7 +38,7 @@ class MusicService : MediaBrowserServiceCompat() { private const val DELETE_ACTION = "com.yunqiinnovation.music_service.DELETE_NOTIFICATION" private val isRunning = AtomicBoolean(false) - private var currentIdInternal: String? = null + private var currentIdInternal: Long? = null private var currentStatus = false private var playlist = mutableListOf>() private var currentIndex = 0 @@ -48,7 +48,7 @@ class MusicService : MediaBrowserServiceCompat() { fun isServiceRunning() = isRunning.get() fun isPlaying() = currentStatus - fun getCurrentId(): String? = currentIdInternal + fun getCurrentId(): Long? = currentIdInternal // // 获取播放进度信息 // fun getProgress(): Map { @@ -118,11 +118,11 @@ class MusicService : MediaBrowserServiceCompat() { fun updateMediaSessionMetadata(song: Map) { instance?.let { service -> try { - val title = song["title"] as? String ?: song["name"] as? String ?: "未知标题" - val artist = song["artist"] as? String ?: song["sgener"] as? String ?: "未知歌手" - val coverUrl = song["coverUrl"] as? String ?: song["image"] as? String ?: "" -currentIdInternal = song["id"] as? String ?: song["id"] as? String ?: "0" - Log.i(TAG, "updateMediaSessionMetadata currentIdInternal: $currentIdInternal") + val title = song["title"] as? String ??: "未知标题" + val artist = song["artist"] as? String ??: "未知歌手" + val coverUrl = song["coverUrl"] as? String ??: "" + currentIdInternal = (song["id"] as? Number)?.toLong() ?: 0L + Log.i(TAG, "updateMediaSessionMetadata currentIdInternal: $currentIdInternal") val metadata = android.support.v4.media.MediaMetadataCompat.Builder() .putString(android.support.v4.media.MediaMetadataCompat.METADATA_KEY_TITLE, title) .putString(android.support.v4.media.MediaMetadataCompat.METADATA_KEY_ARTIST, artist) @@ -216,13 +216,12 @@ private fun updateProgress() { // 更新媒体会话进度 // 通知Flutter层 - flutterChannel?.invokeMethod("onMediaSessionSeekTo1", - mapOf( + flutterChannel?.invokeMethod("onMediaSessionSeekTo1", mapOf( "currentIdInternal" to currentIdInternal, - "currentPosition" to currentPosition, - "duration" to duration, - "progress" to progress - )) + "currentPosition" to currentPosition, + "duration" to duration, + "progress" to progress + )) } } } @@ -238,19 +237,20 @@ private fun updateProgress() { when (command) { "play" -> intent.getSerializableExtra("song")?.let { url -> val songMap = intent.getSerializableExtra("song") as? Map + Log.i(TAG, "MusicePlay onStartCommand: $songMap") songMap?.let { song -> - val songId = song["id"]as? String ?:"0" - Log.i(TAG, "command songId: $songId") + val songId = (song["id"] as? Number)?.toLong() ?: 0L + currentIdInternal = songId - val existingIndex = playlist.indexOfFirst { it["id"] == songId } + val existingIndex = playlist.indexOfFirst { it["id"] == songId} if (existingIndex == -1) { playlist.add(song) currentIndex = playlist.size - 1 } else { currentIndex = existingIndex } - + Log.i(TAG, "MusicePlay songId: $songId currentIndex:$currentIndex" ) play(song) } } @@ -546,8 +546,8 @@ private fun updateProgress() { private fun play(song: Map) { val url = song["url"] as? String ?: return Log.i(TAG, "开始播放: $url") - val id = song["id"] as? String ?: "0" - Log.i(TAG, "开始播放play id: $id") + val id = (song["id"] as? Number)?.toLong() ?: 0L + Log.i(TAG, "开始播放play id: $id") val title = song["title"] as? String ?: "未知标题" val artist = song["artist"] as? String ?: "未知歌手" val coverUrl = song["coverUrl"] as String @@ -586,8 +586,8 @@ private fun updateProgress() { val title = song["title"] as? String ?: "未知标题" val artist = song["artist"] as? String ?: "未知歌手" val coverUrl = song["coverUrl"] as String - val id = song["id"] as? String ?: "0" - Log.i(TAG, "pause id: $id") + val id =(song["id"] as? Number)?.toLong() ?: 0L + Log.i(TAG, "pause id: $id") currentIdInternal = id updateNotification(title,artist,coverUrl) setPlaybackState(PlaybackStateCompat.STATE_PAUSED); @@ -607,8 +607,8 @@ private fun updateProgress() { val title = song["title"] as? String ?: "未知标题" val artist = song["artist"] as? String ?: "未知歌手" val coverUrl = song["coverUrl"] as String - val id = song["id"] as? String ?: "0" - Log.i(TAG, "resume id: $id") + val id = (song["id"] as? Number)?.toLong() ?: 0L + Log.i(TAG, "resume id: $id") currentIdInternal = id updateNotification(title,artist,coverUrl) setPlaybackState(PlaybackStateCompat.STATE_PLAYING); diff --git a/local_plugins/music_service/lib/music_service.dart b/local_plugins/music_service/lib/music_service.dart index 8be33fe68..b6401acd1 100644 --- a/local_plugins/music_service/lib/music_service.dart +++ b/local_plugins/music_service/lib/music_service.dart @@ -1,3 +1,5 @@ +import 'dart:ffi'; + import 'package:flutter/services.dart'; class MusicService { @@ -101,7 +103,7 @@ class MusicService { required Function() onPrevious, required Function(int) onSeekTo, required Function() onStop, - required Function(String, int, int, double) onStop1, + required Function(int, int, int, double) onStop1, }) async { // 设置方法调用处理器来处理来自原生平台的回调 _methodChannel.setMethodCallHandler((call) async { @@ -126,8 +128,7 @@ class MusicService { onStop(); break; case 'onMediaSessionSeekTo1': - final currentIdInternal = - call.arguments['currentIdInternal'] as String; + final currentIdInternal = call.arguments['currentIdInternal'] as int; final currentPosition = call.arguments['currentPosition'] as int; final duration = call.arguments['duration'] as int; final progress = call.arguments['progress'] as double;