From 25d182e059a4d31ae1726b2abe081e2dd514a69d Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sat, 5 Jul 2025 15:34:43 +0800 Subject: [PATCH 01/15] =?UTF-8?q?fix(agent):=20=E4=BF=AE=E5=A4=8D=E5=A3=B0?= =?UTF-8?q?=E9=9F=B3=E6=9D=A1=E5=8A=A8=E7=94=BB=E6=98=BE=E7=A4=BA=E6=9D=A1?= =?UTF-8?q?=E4=BB=B6=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加shouldShowAnimation变量来同时判断isListening和isPushToTalkActive状态,确保在两种情况下都能正确显示动画 --- lib/modules/agent/widgets/sound_bar_widget.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/modules/agent/widgets/sound_bar_widget.dart b/lib/modules/agent/widgets/sound_bar_widget.dart index e5c483b36..c108bcca2 100644 --- a/lib/modules/agent/widgets/sound_bar_widget.dart +++ b/lib/modules/agent/widgets/sound_bar_widget.dart @@ -134,8 +134,10 @@ class AgentSoundBarsWidget extends StatelessWidget { Widget build(BuildContext context) { return Obx(() { final controller = Get.find(); + final shouldShowAnimation = + controller.isListening.value || controller.isPushToTalkActive.value; return SoundBarsWidget( - isActive: controller.isListening.value, + isActive: shouldShowAnimation, barCount: barCount, ); }); From b7248e2265f0f6552d8b9300dd5839921e042322 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sat, 5 Jul 2025 16:50:52 +0800 Subject: [PATCH 02/15] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0FTP=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E5=B7=A5=E5=85=B7=E5=BA=93=E5=AE=9E=E7=8E=B0=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E4=B8=8A=E4=BC=A0=E4=B8=8B=E8=BD=BD=E7=AD=89=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= 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 03/15] =?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; From e7c04c6dcad79fe37f4e01ee15c6fcd7eb606d98 Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Sat, 5 Jul 2025 18:09:16 +0800 Subject: [PATCH 04/15] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8C=89=E4=BD=8F?= =?UTF-8?q?=E8=AF=B4=E8=AF=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/services/music_manager.dart | 4 +- .../agent/controllers/agent_controller.dart | 88 ++----------------- .../agent_service/AgentService.kt | 12 +-- .../azure_speech/AzureAsrHelper.kt | 18 +++- .../music_service/MusicService.kt | 2 +- .../music_service/lib/music_service.dart | 6 +- 6 files changed, 33 insertions(+), 97 deletions(-) diff --git a/lib/data/services/music_manager.dart b/lib/data/services/music_manager.dart index 51975424f..87a05df40 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(int, int, int, double) onStop1, + required Function(int, int, int, double) onProgress, }) async { try { await _musicService.setMediaSessionCallbacks( @@ -115,7 +115,7 @@ class MusicManager extends GetxService { onPrevious: onPrevious, onSeekTo: onSeekTo, onStop: onStop, - onStop1: onStop1, + onProgress: onProgress, ); } catch (e) { Logger.d('MusicManager', '设置媒体会话回调失败: $e'); diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 8eec19a15..455e3aeaa 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -165,7 +165,7 @@ class AgentController extends GetxController { onPrevious: onMediaSessionPrevious, onSeekTo: onMediaSessionSeekTo, onStop: onMediaSessionStop, - onStop1: onMediaSessionSeekTo1, + onProgress: onMediaSessionProgress, ); Logger.d('AgentController', '媒体会话回调设置完成'); } catch (e) { @@ -1307,7 +1307,6 @@ class AgentController extends GetxController { // 设置按住说话状态 isPushToTalkActive.value = true; - print("开始按住说话,状态已设置为true"); // 开始语音识别 await AgentService.audioContinue(); // await startVoiceInput(); @@ -1406,9 +1405,6 @@ class AgentController extends GetxController { await _updateMediaSessionMetadata(musicData); await _updateMediaSessionPlaybackState(true); - // 启动进度定时器 - _startProgressTimer(); - Logger.d(TAG, '音乐播放启动成功'); } catch (e) { Logger.e(TAG, '播放音乐失败: $e'); @@ -1439,7 +1435,6 @@ class AgentController extends GetxController { Logger.d('AgentController', '恢复播放音乐'); await _musiceManager.resume(); isPlaying.value = true; - _startProgressTimer(); // 更新媒体会话播放状态 await _updateMediaSessionPlaybackState(true); @@ -1451,23 +1446,6 @@ class AgentController extends GetxController { } } - // 启动进度更新定时器 - void _startProgressTimer() { - _stopProgressTimer(); // 先停止现有定时器 - - Logger.d('AgentController', '启动播放进度定时器'); - _progressTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) { - if (isPlaying.value && currentId.value != 0) { - updateMusicProgress(); - } else if (!isPlaying.value) { - // 如果不在播放状态,停止定时器 - timer.cancel(); - _progressTimer = null; - Logger.d('AgentController', '播放已停止,取消进度定时器'); - } - }); - } - // 停止进度更新定时器 void _stopProgressTimer() { if (_progressTimer != null) { @@ -1478,45 +1456,7 @@ class AgentController extends GetxController { } // 获取播放进度 - Future updateMusicProgress() async { - try { - if (!isPlaying.value || currentId.value == 0) { - return; // 如果没有在播放,直接返回 - } - - // var progressInfo = await _musiceManager.getProgress(); - // final newPosition = progressInfo['currentPosition'] ?? 0; - // final newDuration = progressInfo['duration'] ?? 0; - // final newProgress = progressInfo['progress'] ?? 0.0; - - // bool hasUpdate = false; - - // // 只有当值发生变化时才更新,避免不必要的UI刷新 - // if (currentPosition.value != newPosition) { - // currentPosition.value = newPosition; - // hasUpdate = true; - // } - // if (duration.value != newDuration) { - // duration.value = newDuration; - // hasUpdate = true; - // } - // if ((progress.value - newProgress).abs() > 0.001) { - // // 避免浮点数精度问题 - // progress.value = newProgress; - // hasUpdate = true; - // } - - // 如果有进度更新,通知系统媒体会话更新进度信息 - // if (hasUpdate) { - // await _updateMediaSessionProgress(); - // } - } catch (e) { - Logger.e('AgentController', '获取播放进度失败: $e'); - } - } - -// 获取播放进度 - Future updateMusicProgress1(int currentIdInternal, int newPosition, + Future updateMusicProgress(int currentIdInternal, int newPosition, int newDuration, double newProgress) async { try { if (currentIdInternal != 0) { @@ -1524,12 +1464,6 @@ class AgentController extends GetxController { // return; // 如果没有在播放,直接返回 } - // // var progressInfo = await _musiceManager.getProgress(); - // 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"); bool hasUpdate = false; if (currentId.value != currentIdInternal) { currentId.value = currentIdInternal; @@ -1597,20 +1531,17 @@ class AgentController extends GetxController { Future seekToPosition(int positionMs) async { try { await _musiceManager.seekTo(positionMs); - // 立即更新进度状态 - // await updateMusicProgress(); } catch (e) { Logger.e('AgentController', '跳转播放位置失败: $e'); } } // 跳转到指定位置 - Future seekToPosition1(int currentIdInternal, int newPosition, + Future seekToProgress(int currentIdInternal, int newPosition, int newDuration, double newProgress) async { try { - //await _musiceManager.seekTo(positionMs); // 立即更新进度状态 - await updateMusicProgress1( + await updateMusicProgress( currentIdInternal, newPosition, newDuration, newProgress); } catch (e) { Logger.e('AgentController', '跳转播放位置失败: $e'); @@ -1700,9 +1631,6 @@ class AgentController extends GetxController { currentId.value = formattedPlaylist[0]['id'] ?? 0; } - // 启动进度定时器 - _startProgressTimer(); - Logger.d(TAG, '音乐列表播放启动成功'); } catch (e) { Logger.e(TAG, '播放音乐列表失败: $e'); @@ -1831,12 +1759,12 @@ class AgentController extends GetxController { _stopProgressTimer(); } - /// 处理来自系统通知栏的跳转操作 - Future onMediaSessionSeekTo1(int currentIdInternal, int newPosition, + /// 处理来自系统进度条 + Future onMediaSessionProgress(int currentIdInternal, int newPosition, int newDuration, double newProgress) async { // print( - // "onMediaSessionSeekTo1 currentIdInternal :$currentIdInternal ,newPosition:$newPosition newDuration:$newDuration newProgress:$newProgress"); - await seekToPosition1( + // "onMediaSessionProgress currentIdInternal :$currentIdInternal ,newPosition:$newPosition newDuration:$newDuration newProgress:$newProgress"); + await seekToProgress( currentIdInternal, newPosition, newDuration, newProgress); } } 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 b51f5aa2b..01b42c684 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 @@ -352,7 +352,6 @@ object AgentService : CoroutineScope { */ private fun startIdleCheck() { stopIdleCheck() // 先停止现有的检查 - Log.d(TAG, "启动空闲检测: ${isRecognitionActive}") if (!isRecognitionActive) return idleCheckJob = launch { @@ -360,7 +359,6 @@ object AgentService : CoroutineScope { delay(maxIdleSeconds * 1000L) if (isActive && isRecognitionActive && !hasSpeechDetected && !isTtsSpeaking && !isAiStreaming) { - Log.d(TAG, "启动空闲检测: stopRecognition") //stopRecognition() sendEvent("auto_stop", mapOf("reason" to "idle_timeout", "seconds" to maxIdleSeconds)) } @@ -398,14 +396,12 @@ object AgentService : CoroutineScope { Log.d(TAG, "服务未初始化") return false } - Log.d(TAG, "麦克风音频未暂停,无法设置${azureAsrHelper!!.isPaused},$isExternalActive") if(!(azureAsrHelper!!.isPaused)&&isExternalActive) { Log.d(TAG, "麦克风音频未暂停,无法设置") BleService.closeCodec() return false } - Log.d(TAG, "开始语音识别") if (isRecognitionActive) return true // 检查录音权限 @@ -499,14 +495,13 @@ object AgentService : CoroutineScope { _isRecognitionActive.set(false) stopIdleCheck() - Log.d(TAG, "onSessionStopped") audioPlayer?.playAudio(R.raw.stop) } override fun onCanceled(reason: String, errorDetails: String) { _isRecognitionActive.set(false) stopIdleCheck() - Log.d(TAG, "recognition_canceled") + BleService.closeCodec() sendEvent("recognition_canceled", mapOf( "reason" to reason, @@ -517,7 +512,7 @@ object AgentService : CoroutineScope { override fun onError(error: String) { _isRecognitionActive.set(false) stopIdleCheck() - Log.d(TAG, "error") + BleService.closeCodec() Log.e(TAG, "语音识别错误: $error") sendEvent("error", mapOf( @@ -549,7 +544,6 @@ object AgentService : CoroutineScope { try { azureAsrHelper?.stopContinuousRecognition() - Log.d(TAG, "停止语音识别") BleService.closeCodec() _isRecognitionActive.set(false) stopIdleCheck() @@ -569,7 +563,7 @@ object AgentService : CoroutineScope { try { if(audioSourceType== AzureAsrHelper.AudioSourceType.EXTERNAL) { - Log.d(TAG, "当前是什么输入:$audioSourceType") + BleService.closeCodec() stopRecognition() startRecognition(false) diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt index aaa4887c5..661e51ec8 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt @@ -458,9 +458,12 @@ class AzureAsrHelper(private val context: Context) { EventHandler { _, _ -> // 直接在当前线程调用回调 Log.d(tag, "会话结束事件") + if (audioSourceType == AudioSourceType.EXTERNAL) { + callback.onSessionStopped() isContinuousRecognitionActive = false stopAudioProcessing() + } } ) @@ -633,7 +636,12 @@ class AzureAsrHelper(private val context: Context) { Log.d(tag, "关闭麦克风stopAudioPause: ") // 清理旧的识别器 isPaused = true - + // 如果正在进行连续识别,先停止 + if (isContinuousRecognitionActive) { + // 直接停止,不等待结果 + recognizer?.stopContinuousRecognitionAsync() + isContinuousRecognitionActive = false + } } @@ -642,9 +650,15 @@ class AzureAsrHelper(private val context: Context) { */ fun audioContinue() { Log.d(tag, "继续麦克风stopAudioPause: ") - +// 如果正在进行连续识别,先停止 + if (!isContinuousRecognitionActive) { + // 直接停止,不等待结果 + recognizer?.startContinuousRecognitionAsync() + isContinuousRecognitionActive = true + } // 开始连续识别 isPaused = false + } /** 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 0956f50d8..be1cabf4d 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 @@ -216,7 +216,7 @@ private fun updateProgress() { // 更新媒体会话进度 // 通知Flutter层 - flutterChannel?.invokeMethod("onMediaSessionSeekTo1", mapOf( + flutterChannel?.invokeMethod("onMediaSessionProgress", mapOf( "currentIdInternal" to currentIdInternal, "currentPosition" to currentPosition, "duration" to duration, diff --git a/local_plugins/music_service/lib/music_service.dart b/local_plugins/music_service/lib/music_service.dart index b6401acd1..b7007500c 100644 --- a/local_plugins/music_service/lib/music_service.dart +++ b/local_plugins/music_service/lib/music_service.dart @@ -103,7 +103,7 @@ class MusicService { required Function() onPrevious, required Function(int) onSeekTo, required Function() onStop, - required Function(int, int, int, double) onStop1, + required Function(int, int, int, double) onProgress, }) async { // 设置方法调用处理器来处理来自原生平台的回调 _methodChannel.setMethodCallHandler((call) async { @@ -127,12 +127,12 @@ class MusicService { case 'onMediaSessionStop': onStop(); break; - case 'onMediaSessionSeekTo1': + case 'onMediaSessionProgress': 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; - onStop1(currentIdInternal, currentPosition, duration, progress); + onProgress(currentIdInternal, currentPosition, duration, progress); break; } }); From d3921231b3c4e69313ab5dc49655eda18675806c Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sun, 6 Jul 2025 12:58:02 +0800 Subject: [PATCH 05/15] =?UTF-8?q?feat(translation):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E7=BF=BB=E8=AF=91=E5=8E=86=E5=8F=B2=E6=8C=89=E9=92=AE=E5=B9=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=AF=A6=E6=83=85=E9=A1=B5=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在翻译页面添加历史记录按钮,支持查看翻译历史 - 重构翻译详情页的会话项样式,改进视觉层次和交互 - 启用火山AI服务依赖注入 - 更新测试标记日期为0707 - 优化超时错误提示信息 --- lib/core/bindings/initial_binding.dart | 3 +- lib/modules/settings/views/settings_view.dart | 2 +- .../translation_detail_controller.dart | 8 +- .../views/translation_detail_view.dart | 122 +++++++++--------- .../translation/views/translation_view.dart | 13 ++ 5 files changed, 79 insertions(+), 69 deletions(-) diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index b916bc88d..d532f449e 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -4,6 +4,7 @@ import 'package:deep_voice/data/services/navigation_manager.dart'; import 'package:get/get.dart'; import '../../data/services/speech_factory.dart'; import '../../data/services/volcano_translation_service.dart'; +import '../../data/services/volcano_ai_service.dart'; import '../../data/services/language_manager.dart'; import '../../core/utils/logger.dart'; import '../../data/services/bluetooth_manager.dart'; @@ -40,7 +41,7 @@ class InitialBinding extends Bindings { Get.lazyPut(() => NavigationManager(), fenix: true); Get.lazyPut(() => LocationManager(), fenix: true); // 火山AI服务 - // Get.lazyPut(() => VolcanoAIService(), fenix: true); + Get.lazyPut(() => VolcanoAIService(), fenix: true); // 账号管理服务 Get.lazyPut(() => AccountService(), fenix: true); diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart index 7c69a9216..d8c0976b2 100644 --- a/lib/modules/settings/views/settings_view.dart +++ b/lib/modules/settings/views/settings_view.dart @@ -841,7 +841,7 @@ class SettingsView extends GetView { //------------测试使用标记 start---------------// SizedBox(height: 4.h), Text( - '${Platform.operatingSystem}-test-0626', + '${Platform.operatingSystem}-test-0707', style: TextStyle( fontSize: 12.sp, color: const Color.fromARGB(255, 143, 94, 94), diff --git a/lib/modules/translation/controllers/translation_detail_controller.dart b/lib/modules/translation/controllers/translation_detail_controller.dart index 78f284cee..f2f469041 100644 --- a/lib/modules/translation/controllers/translation_detail_controller.dart +++ b/lib/modules/translation/controllers/translation_detail_controller.dart @@ -50,7 +50,7 @@ class TranslationDetailController extends GetxController { // 异步初始化方法 Future _asyncInitialize() async { isLoading.value = true; - + try { // 使用 Future.microtask 将操作放到后台线程 await Future.microtask(() { @@ -116,13 +116,13 @@ class TranslationDetailController extends GetxController { Future extractSummary(String text) async { return _extractSummaryInternal(text, _defaultTimeoutDuration); } - + // 内部摘要提取方法,避免代码重复 Future _extractSummaryInternal(String text, int timeoutDuration) async { if (text.isEmpty) { return '无法生成摘要:文本为空'; } - + isGeneratingSummary.value = true; try { @@ -155,7 +155,7 @@ class TranslationDetailController extends GetxController { messages: messages, systemPrompt: systemPrompt, ).timeout(Duration(seconds: timeoutDuration), onTimeout: () { - throw TimeoutException('请求超时,请稍后重试'); + throw TimeoutException('请求超时', Duration(seconds: timeoutDuration)); }); } on TimeoutException catch (_) { final timeoutMessage = timeoutDuration == _defaultTimeoutDuration diff --git a/lib/modules/translation/views/translation_detail_view.dart b/lib/modules/translation/views/translation_detail_view.dart index 9e94ecbdf..5b988b5c5 100644 --- a/lib/modules/translation/views/translation_detail_view.dart +++ b/lib/modules/translation/views/translation_detail_view.dart @@ -210,85 +210,81 @@ class TranslationDetailView extends GetView { // 构建会话详情中的翻译项 Widget _buildSessionItemWidget(TranslationItem item, bool isDarkMode) { + final context = Get.context!; return Container( - width: double.infinity, - margin: EdgeInsets.only(bottom: 16.h), + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: isDarkMode ? AppColors.cardBackground : Colors.white, - borderRadius: BorderRadius.circular(16.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withAlpha(isDarkMode ? 30 : 8), - blurRadius: 8, - offset: Offset(0, 2), - ), - ], + color: item.isIntermediate + ? Colors.grey.withOpacity(0.1) // 中间结果使用浅灰色底色 + : (item.sourceLanguageCode == controller.currentItem.sourceLanguageCode + ? Theme.of(context) + .colorScheme + .primary + .withOpacity(0.08) // 源语言匹配时使用主题色底色 + : Theme.of(context) + .colorScheme + .secondary + .withOpacity(0.08)), // 否则使用次要色底色 + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: item.isIntermediate + ? Colors.grey.withOpacity(0.2) + : Colors.transparent, + width: 1, + ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 源文本部分 - Padding( - padding: EdgeInsets.fromLTRB(20.r, 20.r, 20.r, 10.r), - child: Text( - item.sourceText, + // 源文本 + Text( + item.sourceText, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).textTheme.bodyLarge?.color, + ), + ), + if (item.translatedText.isNotEmpty) ...[ + const SizedBox(height: 8), + // 翻译文本 + Text( + item.translatedText, style: TextStyle( - fontSize: 15.sp, - color: isDarkMode ? Colors.white : Colors.black, + fontSize: 16, + color: Theme.of(context).textTheme.bodyMedium?.color, fontWeight: FontWeight.w500, ), ), - ), - // 翻译文本部分 - Padding( - padding: EdgeInsets.fromLTRB(20.r, 10.r, 20.r, 16.r), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Expanded( - child: Text( - item.translatedText, - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[700], - fontWeight: FontWeight.normal, - ), + ], + // 语言信息和播放按钮 + const SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${item.sourceLanguageName.tr} → ${item.targetLanguageName.tr}', + style: TextStyle( + fontSize: 12, + color: Theme.of(context).textTheme.bodySmall?.color, + ), + ), + if (!item.isIntermediate) + GestureDetector( + onTap: () => controller.playTranslationItem(item), + child: Icon( + Icons.volume_up, + size: 20, + color: Theme.of(context).colorScheme.primary, ), ), - SizedBox(width: 12.w), - _buildPlayButton(() => controller.playTranslationItem(item), isDarkMode), - ], - ), + ], ), ], ), ); } - - // 构建播放按钮 - Widget _buildPlayButton(VoidCallback onTap, bool isDarkMode) { - return Material( - color: Colors.transparent, - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(20.r), - splashColor: isDarkMode ? AppColors.primary.withOpacity(0.2) : Colors.blue.withOpacity(0.2), - child: Container( - width: 28.w, - height: 28.h, - decoration: BoxDecoration( - color: isDarkMode ? AppColors.primary.withAlpha(10) : Colors.blue.withAlpha(10), - shape: BoxShape.circle, - ), - child: Icon( - Icons.volume_up, - color: Colors.white, - size: 16.sp, - ), - ), - ), - ); - } + // 构建总结视图 Widget _buildSummaryWidget(bool isDarkMode) { @@ -598,7 +594,7 @@ class TranslationDetailView extends GetView { // 分享内容方法 void _shareContent() { String shareText = ''; - + if (controller.selectedTabIndex.value == 0) { // 分享原始记录 final String sourceText = _getCombinedSourceText(); diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index e529bd98d..239f5d823 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/lib/modules/translation/views/translation_view.dart @@ -169,6 +169,19 @@ class TranslationView extends GetView { }, tooltip: '查看录音文件', ), + // 添加历史记录按钮 + IconButton( + icon: Icon( + Icons.history, + color: isDarkMode ? Colors.white : Colors.black, + size: 24.sp, + ), + onPressed: () async { + await controller.stopAll(); + Get.toNamed(Routes.translationHistory); + }, + tooltip: '翻译历史', + ), ], ); } From dd03ba86bd2e0a117cebcfc5994f5a58d194b354 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sun, 6 Jul 2025 15:41:18 +0800 Subject: [PATCH 06/15] =?UTF-8?q?feat(=E7=BF=BB=E8=AF=91=E5=8E=86=E5=8F=B2?= =?UTF-8?q?):=20=E6=B7=BB=E5=8A=A0=E6=97=B6=E9=97=B4=E5=92=8C=E8=AF=AD?= =?UTF-8?q?=E7=A7=8D=E6=98=BE=E7=A4=BA=E5=B9=B6=E4=BC=98=E5=8C=96=E5=B8=83?= =?UTF-8?q?=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在翻译历史列表中新增时间和语种信息的显示,包括: 1. 格式化显示时间(今天显示时间,其他日期显示月日时间) 2. 添加源语言和目标语言的显示(使用双向箭头符号) 3. 调整列表项布局为顶部对齐 4. 移除翻译详情页中暂时不需要的"记录摘要"标签页 --- .../views/translation_detail_view.dart | 35 +++++------ .../views/translation_history_view.dart | 60 ++++++++++++++++++- 2 files changed, 76 insertions(+), 19 deletions(-) diff --git a/lib/modules/translation/views/translation_detail_view.dart b/lib/modules/translation/views/translation_detail_view.dart index 5b988b5c5..c31db5552 100644 --- a/lib/modules/translation/views/translation_detail_view.dart +++ b/lib/modules/translation/views/translation_detail_view.dart @@ -72,14 +72,14 @@ class TranslationDetailView extends GetView { ), onPressed: () => Get.back(), ), - title: Text( - 'Detail', - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black, - fontSize: 24.sp, - fontWeight: FontWeight.bold, - ), - ), + // title: Text( + // 'Detail', + // style: TextStyle( + // color: isDarkMode ? Colors.white : Colors.black, + // fontSize: 24.sp, + // fontWeight: FontWeight.bold, + // ), + // ), centerTitle: true, actions: [ IconButton( @@ -108,15 +108,16 @@ class TranslationDetailView extends GetView { isDarkMode: isDarkMode, )), ), - SizedBox(width: 12.w), - Expanded( - child: Obx(() => _buildTabButton( - title: '记录摘要', - isSelected: controller.selectedTabIndex.value == 1, - onTap: () => controller.switchTab(1), - isDarkMode: isDarkMode, - )), - ), + //屏蔽“记录摘要”,需要是开启 + // SizedBox(width: 12.w), + // Expanded( + // child: Obx(() => _buildTabButton( + // title: '记录摘要', + // isSelected: controller.selectedTabIndex.value == 1, + // onTap: () => controller.switchTab(1), + // isDarkMode: isDarkMode, + // )), + // ), ], ), ); diff --git a/lib/modules/translation/views/translation_history_view.dart b/lib/modules/translation/views/translation_history_view.dart index 1283a4c27..4aeb6ef38 100644 --- a/lib/modules/translation/views/translation_history_view.dart +++ b/lib/modules/translation/views/translation_history_view.dart @@ -1,11 +1,13 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:intl/intl.dart'; import '../controllers/translation_controller.dart'; import '../controllers/translation_history_controller.dart'; import '../bindings/translation_detail_binding.dart'; import 'translation_detail_view.dart'; import '../../../core/theme/app_colors.dart'; +import '../../../data/services/language_manager.dart'; class TranslationHistoryView extends GetView { const TranslationHistoryView({Key? key}) : super(key: key); @@ -116,7 +118,7 @@ class TranslationHistoryView extends GetView { child: Padding( padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h), child: Row( - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( @@ -144,9 +146,36 @@ class TranslationHistoryView extends GetView { maxLines: 2, overflow: TextOverflow.ellipsis, ), + SizedBox(height: 8.h), + // 时间和语种信息行 + Row( + children: [ + // 时间信息 + Text( + _formatSessionTime(item.timestamp), + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.grey[500] : Colors.grey[600], + ), + ), + SizedBox(width: 12.w), + // 语种信息 + Expanded( + child: Text( + _formatLanguagePair(item), + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.grey[500] : Colors.grey[600], + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), ], ), ), + SizedBox(width: 12.w), Icon( Icons.chevron_right, color: isDarkMode ? Colors.grey[600] : Colors.grey[350], @@ -161,7 +190,7 @@ class TranslationHistoryView extends GetView { void _showTranslationDetail(TranslationItem item) { // 获取该会话的所有翻译项 final List sessionItems = controller.getSessionItems(item.sessionId); - + Get.to( () => TranslationDetailView(), arguments: { @@ -173,6 +202,33 @@ class TranslationHistoryView extends GetView { ); } + // 格式化会话时间 + String _formatSessionTime(DateTime timestamp) { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final itemDate = DateTime(timestamp.year, timestamp.month, timestamp.day); + + if (itemDate == today) { + // 今天:只显示时间 HH:mm + return DateFormat('HH:mm').format(timestamp); + } else { + // 其他日期:显示月日和时间 MM-dd HH:mm + return DateFormat('MM-dd HH:mm').format(timestamp); + } + } + + // 格式化语种对信息 + String _formatLanguagePair(TranslationItem item) { + final languageManager = Get.find(); + + // 获取源语言和目标语言的中文名称 + final sourceName = languageManager.getChineseNameByAsrCode(item.sourceLanguageCode) ?? '未知语言'; + final targetName = languageManager.getChineseNameByAsrCode(item.targetLanguageCode) ?? '未知语言'; + + // 使用双向箭头符号表示翻译方向 + return '$sourceName ⇄ $targetName'; + } + void _showClearHistoryConfirmDialog() { final isDarkMode = Get.isDarkMode; From 38f1bb34564c7c9d9e49a6d250a5213e6515e666 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sun, 6 Jul 2025 16:42:40 +0800 Subject: [PATCH 07/15] =?UTF-8?q?feat(translation):=20=E6=94=B9=E8=BF=9B?= =?UTF-8?q?=E7=BF=BB=E8=AF=91=E5=8E=86=E5=8F=B2=E5=AD=98=E5=82=A8=E5=92=8C?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 分离存储限制和显示限制,存储限制增加到1000条而显示限制为20条 新增getAllHistory方法获取完整历史记录 优化保存逻辑,仅保存完成态记录并减少频繁保存 --- .../controllers/translation_controller.dart | 107 ++++++++++++++---- .../translation_history_controller.dart | 16 ++- 2 files changed, 97 insertions(+), 26 deletions(-) diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 924bfa972..a964585fc 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -45,11 +45,16 @@ class TranslationController extends GetxController { Timer? _recordTimer; //秒 int _seconds = 0; - // 存储相关 + // 存储相关配置 static const String _historyKey = 'translation_history'; static const String _sourceLanguageKey = 'translation_source_language'; static const String _targetLanguageKey = 'translation_target_language'; - static const int _maxHistoryItems = 10; + + // 【重要改进】区分存储限制和显示限制 + // 存储限制:大幅增加到1000条,充分利用本地存储能力,几乎无限制存储 + static const int _maxStorageItems = 1000; + // 主页面显示限制:只显示最近20条,保证界面性能和用户体验 + static const int _maxDisplayItems = 20; RxInt activeSpeaker = 0.obs; // 0=无激活, 1=按钮1, 2=按钮2 RxInt lastActiveSpeaker = 0.obs; // 上一次状态 @@ -559,8 +564,8 @@ class TranslationController extends GetxController { String? translationResult; try { - var detectedSourceLanguageCode; - var detectedTargetLanguageCode; + String detectedSourceLanguageCode; + String detectedTargetLanguageCode; if ((currentMode.value == 'faceToFace' && activeSpeaker.value == 1) || (currentMode.value == 'faceToFace' && activeSpeaker.value == 0 && @@ -608,7 +613,12 @@ class TranslationController extends GetxController { translationHistory[index].targetLanguageCode = detectedTargetLanguageCode; translationHistory.refresh(); - saveTranslationHistory(); + + // 【改进】只有在最终结果时才保存,避免中间态时的频繁保存 + if (isFinal) { + saveTranslationHistory(); + } + // 更新后滚动到底部 _scrollToBottom(); // 播放TTS - 只在最终结果时播放 @@ -630,7 +640,7 @@ class TranslationController extends GetxController { } } else { Logger.info( - '未找到匹配的历史项 ${sourceText} ${timestamp.toIso8601String() ?? "无时间戳"}'); + '未找到匹配的历史项 $sourceText ${timestamp.toIso8601String()}'); } } } catch (e) { @@ -836,10 +846,13 @@ class TranslationController extends GetxController { final List allHistory = historyList.map((item) => TranslationItem.fromJson(item)).toList(); - // 只保留最新的10条记录 - if (allHistory.length > _maxHistoryItems) { + // 【注意】存储中的记录都是完成态记录(isIntermediate = false) + // 因为保存时已经过滤掉了中间态记录,确保数据质量 + + // 主页面只显示最近的20条完成态记录,保证界面性能 + if (allHistory.length > _maxDisplayItems) { translationHistory.value = - allHistory.sublist(allHistory.length - _maxHistoryItems); + allHistory.sublist(allHistory.length - _maxDisplayItems); } else { translationHistory.value = allHistory; } @@ -855,22 +868,76 @@ class TranslationController extends GetxController { } } - // 保存翻译历史 + // 保存翻译历史 - 回归原始简单实现 + // 【修复】完全回归原始保存逻辑,只保存当前内存中的完成态记录 void saveTranslationHistory() { - if (translationHistory.length > _maxHistoryItems) { - translationHistory.removeRange( - 0, translationHistory.length - _maxHistoryItems); - } - try { + // 只保存当前内存中的完成态记录,过滤掉中间态记录 + final List finalRecords = translationHistory + .where((item) => !item.isIntermediate) + .toList(); + + // 应用存储限制,保留最新的1000条完成态记录 + List historyToSave = finalRecords; + if (finalRecords.length > _maxStorageItems) { + historyToSave = finalRecords.sublist(finalRecords.length - _maxStorageItems); + } + + // 保存到本地存储 final List> historyJson = - translationHistory.map((item) => item.toJson()).toList(); + historyToSave.map((item) => item.toJson()).toList(); _storage.write(_historyKey, jsonEncode(historyJson)); + + Logger.info('翻译历史保存成功: 存储${historyToSave.length}条记录'); } catch (e) { Logger.error('保存翻译历史失败: ${e.toString()}'); } } + // 获取所有完成态历史记录(供历史页面使用) + // 【修复】简化实现,合并存储和内存中的完成态记录 + List getAllHistory() { + try { + final List result = []; + final Set existingIds = {}; + + // 添加存储的历史记录(存储中的记录都是完成态) + final historyJson = _storage.read(_historyKey); + if (historyJson != null) { + final List historyList = jsonDecode(historyJson); + final List storedHistory = + historyList.map((item) => TranslationItem.fromJson(item)).toList(); + + for (final item in storedHistory) { + final id = '${item.sessionId}_${item.timestamp.millisecondsSinceEpoch}'; + if (!existingIds.contains(id)) { + existingIds.add(id); + result.add(item); + } + } + } + + // 添加当前内存中的完成态记录(排除中间态记录) + for (final item in translationHistory) { + if (!item.isIntermediate) { + final id = '${item.sessionId}_${item.timestamp.millisecondsSinceEpoch}'; + if (!existingIds.contains(id)) { + existingIds.add(id); + result.add(item); + } + } + } + + // 按时间排序 + result.sort((a, b) => a.timestamp.compareTo(b.timestamp)); + return result; + } catch (e) { + Logger.error('获取完整历史记录失败: ${e.toString()}'); + // 发生错误时,返回当前内存中的完成态记录 + return translationHistory.where((item) => !item.isIntermediate).toList(); + } + } + // 清除翻译历史 void clearTranslationHistory() { translationHistory.clear(); @@ -882,11 +949,9 @@ class TranslationController extends GetxController { _scrollToBottom(); } - /// 处理原生语音交互开始事件 - void _handleVoiceInteractionStarted() { - // 收到recognitionStarted事件时,停止当前的AI交互、录音和TTS - stopAll(); - } + + + } // 翻译项模型 diff --git a/lib/modules/translation/controllers/translation_history_controller.dart b/lib/modules/translation/controllers/translation_history_controller.dart index f5679c286..04fb34c50 100644 --- a/lib/modules/translation/controllers/translation_history_controller.dart +++ b/lib/modules/translation/controllers/translation_history_controller.dart @@ -44,9 +44,12 @@ class TranslationHistoryController extends GetxController { // 按日期对历史记录进行分组 void _groupHistoryByDate() { final Map> newGroupedHistory = {}; - + + // 获取完整的历史记录,而不仅仅是当前显示的记录 + final List allHistory = translationController.getAllHistory(); + // 筛选出每个会话的第一条记录 - final List firstItemsOfSessions = translationController.translationHistory + final List firstItemsOfSessions = allHistory .where((item) => item.isFirstInSession) .toList(); @@ -98,12 +101,15 @@ class TranslationHistoryController extends GetxController { // 按会话ID对历史记录进行分组 void _groupHistoryBySession() { final Map> newSessionMap = {}; - - for (var item in translationController.translationHistory) { + + // 获取完整的历史记录进行会话分组 + final List allHistory = translationController.getAllHistory(); + + for (var item in allHistory) { if (!newSessionMap.containsKey(item.sessionId)) { newSessionMap[item.sessionId] = []; } - + newSessionMap[item.sessionId]!.add(item); } From c01bfbee38eb83b215ad727119c4c07e55192ae2 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sun, 6 Jul 2025 20:33:18 +0800 Subject: [PATCH 08/15] =?UTF-8?q?feat(translation):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E7=BF=BB=E8=AF=91=E8=AE=B0=E5=BD=95=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在历史记录控制器中添加删除会话历史方法 - 在详情控制器中添加删除单条记录方法 - 在视图中添加删除按钮和确认对话框 - 支持从内存和本地存储中同步删除记录 --- .../controllers/translation_controller.dart | 78 ++++++++++ .../translation_detail_controller.dart | 56 ++++++- .../translation_history_controller.dart | 34 ++++- .../views/translation_detail_view.dart | 140 ++++++++++++++++-- .../views/translation_history_view.dart | 74 ++++++++- 5 files changed, 369 insertions(+), 13 deletions(-) diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index a964585fc..cff3a3247 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -944,6 +944,84 @@ class TranslationController extends GetxController { _storage.remove(_historyKey); } + // 删除指定会话的所有翻译记录 + void deleteSessionHistory(String sessionId) { + try { + // 1. 从内存中删除该会话的所有记录 + final originalCount = translationHistory.length; + translationHistory.removeWhere((item) => item.sessionId == sessionId); + final removedFromMemory = originalCount - translationHistory.length; + + // 2. 从本地存储中删除该会话的记录 + final historyJson = _storage.read(_historyKey); + int removedFromStorage = 0; + + if (historyJson != null) { + final List historyList = jsonDecode(historyJson); + final List storedHistory = + historyList.map((item) => TranslationItem.fromJson(item)).toList(); + + final originalStoredCount = storedHistory.length; + storedHistory.removeWhere((item) => item.sessionId == sessionId); + removedFromStorage = originalStoredCount - storedHistory.length; + + // 重新保存更新后的存储数据 + final List> updatedHistoryJson = + storedHistory.map((item) => item.toJson()).toList(); + _storage.write(_historyKey, jsonEncode(updatedHistoryJson)); + } + + // 3. 刷新UI显示 + translationHistory.refresh(); + + Logger.info('删除会话历史成功: sessionId=$sessionId, 内存删除${removedFromMemory}条, 存储删除${removedFromStorage}条'); + } catch (e) { + Logger.error('删除会话历史失败: ${e.toString()}'); + rethrow; // 重新抛出异常,让调用方处理 + } + } + + // 删除指定的单条翻译记录 + void deleteTranslationItem(String sessionId, DateTime timestamp) { + try { + // 1. 从内存中删除指定的翻译记录 + final originalCount = translationHistory.length; + translationHistory.removeWhere((item) => + item.sessionId == sessionId && + item.timestamp.millisecondsSinceEpoch == timestamp.millisecondsSinceEpoch); + final removedFromMemory = originalCount - translationHistory.length; + + // 2. 从本地存储中删除指定的翻译记录 + final historyJson = _storage.read(_historyKey); + int removedFromStorage = 0; + + if (historyJson != null) { + final List historyList = jsonDecode(historyJson); + final List storedHistory = + historyList.map((item) => TranslationItem.fromJson(item)).toList(); + + final originalStoredCount = storedHistory.length; + storedHistory.removeWhere((item) => + item.sessionId == sessionId && + item.timestamp.millisecondsSinceEpoch == timestamp.millisecondsSinceEpoch); + removedFromStorage = originalStoredCount - storedHistory.length; + + // 重新保存更新后的存储数据 + final List> updatedHistoryJson = + storedHistory.map((item) => item.toJson()).toList(); + _storage.write(_historyKey, jsonEncode(updatedHistoryJson)); + } + + // 3. 刷新UI显示 + translationHistory.refresh(); + + Logger.info('删除单条翻译记录成功: sessionId=$sessionId, timestamp=${timestamp.toIso8601String()}, 内存删除${removedFromMemory}条, 存储删除${removedFromStorage}条'); + } catch (e) { + Logger.error('删除单条翻译记录失败: ${e.toString()}'); + rethrow; // 重新抛出异常,让调用方处理 + } + } + // 滚动到底部 (公共方法,用于向后兼容) void scrollToBottom({bool animate = true}) { _scrollToBottom(); diff --git a/lib/modules/translation/controllers/translation_detail_controller.dart b/lib/modules/translation/controllers/translation_detail_controller.dart index f2f469041..a33bf50ac 100644 --- a/lib/modules/translation/controllers/translation_detail_controller.dart +++ b/lib/modules/translation/controllers/translation_detail_controller.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../../../data/services/volcano_ai_service.dart'; import '../controllers/translation_controller.dart'; @@ -182,10 +183,63 @@ class TranslationDetailController extends GetxController { } } + // 删除单条翻译记录 + Future deleteTranslationItem(TranslationItem item) async { + try { + // 调用主控制器的删除方法 + historyController.translationController.deleteTranslationItem( + item.sessionId, + item.timestamp + ); + + // 从当前会话项目列表中移除该项目 + sessionItems.removeWhere((sessionItem) => + sessionItem.sessionId == item.sessionId && + sessionItem.timestamp.millisecondsSinceEpoch == item.timestamp.millisecondsSinceEpoch); + + // 重新加载历史控制器的数据 + await historyController.loadHistoryData(); + + // 检查是否删除了会话中的最后一条记录 + if (sessionItems.isEmpty) { + // 如果会话为空,返回上级页面 + Get.back(); + Get.snackbar( + '删除成功', + '已删除该翻译记录,会话已清空', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200], + colorText: Get.isDarkMode ? Colors.white : Colors.black, + duration: const Duration(seconds: 2), + ); + } else { + // 显示删除成功提示 + Get.snackbar( + '删除成功', + '已删除该翻译记录', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200], + colorText: Get.isDarkMode ? Colors.white : Colors.black, + duration: const Duration(seconds: 2), + ); + } + } catch (e) { + // 显示删除失败提示 + Get.snackbar( + '删除失败', + '删除翻译记录时发生错误: ${e.toString()}', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red[400], + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + } + } + @override void onClose() { // 确保资源被正确释放 historyController.translationController.stopAll(); super.onClose(); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/lib/modules/translation/controllers/translation_history_controller.dart b/lib/modules/translation/controllers/translation_history_controller.dart index 04fb34c50..3c2184c43 100644 --- a/lib/modules/translation/controllers/translation_history_controller.dart +++ b/lib/modules/translation/controllers/translation_history_controller.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import '../controllers/translation_controller.dart'; @@ -139,4 +140,35 @@ class TranslationHistoryController extends GetxController { // 检查是否有历史记录 bool get hasHistory => translationController.translationHistory.isNotEmpty; -} \ No newline at end of file + + // 删除指定会话的历史记录 + Future deleteSessionHistory(String sessionId) async { + try { + // 调用主控制器的删除方法 + translationController.deleteSessionHistory(sessionId); + + // 重新加载和分组历史数据 + await loadHistoryData(); + + // 显示删除成功提示 + Get.snackbar( + '删除成功', + '已删除该会话的所有翻译记录', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200], + colorText: Get.isDarkMode ? Colors.white : Colors.black, + duration: const Duration(seconds: 2), + ); + } catch (e) { + // 显示删除失败提示 + Get.snackbar( + '删除失败', + '删除会话记录时发生错误: ${e.toString()}', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red[400], + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + } + } +} \ No newline at end of file diff --git a/lib/modules/translation/views/translation_detail_view.dart b/lib/modules/translation/views/translation_detail_view.dart index c31db5552..52ad53397 100644 --- a/lib/modules/translation/views/translation_detail_view.dart +++ b/lib/modules/translation/views/translation_detail_view.dart @@ -258,7 +258,7 @@ class TranslationDetailView extends GetView { ), ), ], - // 语言信息和播放按钮 + // 语言信息和操作按钮 const SizedBox(height: 4), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -270,15 +270,39 @@ class TranslationDetailView extends GetView { color: Theme.of(context).textTheme.bodySmall?.color, ), ), - if (!item.isIntermediate) - GestureDetector( - onTap: () => controller.playTranslationItem(item), - child: Icon( - Icons.volume_up, - size: 20, - color: Theme.of(context).colorScheme.primary, - ), - ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + // 删除按钮 + if (!item.isIntermediate) + InkWell( + onTap: () => _showDeleteItemConfirmDialog(item), + borderRadius: BorderRadius.circular(6.r), + splashColor: Colors.red.withValues(alpha: 0.1), + highlightColor: Colors.red.withValues(alpha: 0.05), + child: Container( + padding: EdgeInsets.all(6.w), + child: Icon( + Icons.delete_outline, + size: 16.sp, + color: isDarkMode ? Colors.grey[500] : Colors.grey[400], + ), + ), + ), + // 播放按钮 + if (!item.isIntermediate) ...[ + SizedBox(width: 8.w), + GestureDetector( + onTap: () => controller.playTranslationItem(item), + child: Icon( + Icons.volume_up, + size: 20.sp, + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + ], + ), ], ), ], @@ -592,6 +616,102 @@ class TranslationDetailView extends GetView { ); } + // 显示删除单条记录确认对话框 + void _showDeleteItemConfirmDialog(TranslationItem item) { + final isDarkMode = Get.isDarkMode; + + Get.dialog( + AlertDialog( + backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white, + title: Text( + '删除翻译记录', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.bold, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '确定要删除这条翻译记录吗?此操作无法撤销。', + style: TextStyle( + fontSize: 16.sp, + color: isDarkMode ? Colors.grey[300] : Colors.black87, + ), + ), + SizedBox(height: 12.h), + Container( + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[800] : Colors.grey[100], + borderRadius: BorderRadius.circular(8.r), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.sourceText, + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.grey[300] : Colors.black87, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + if (item.translatedText.isNotEmpty) ...[ + SizedBox(height: 4.h), + Text( + item.translatedText, + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.grey[400] : Colors.black54, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text( + 'cancel'.tr, + style: TextStyle( + fontSize: 16.sp, + color: isDarkMode ? Colors.grey[400] : Colors.grey[700], + ), + ), + ), + TextButton( + onPressed: () { + Get.back(); + // 删除指定的翻译记录 + controller.deleteTranslationItem(item); + }, + child: Text( + 'confirm'.tr, + style: TextStyle( + fontSize: 16.sp, + color: Colors.red, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + ), + ); + } + // 分享内容方法 void _shareContent() { String shareText = ''; diff --git a/lib/modules/translation/views/translation_history_view.dart b/lib/modules/translation/views/translation_history_view.dart index 4aeb6ef38..44733c576 100644 --- a/lib/modules/translation/views/translation_history_view.dart +++ b/lib/modules/translation/views/translation_history_view.dart @@ -176,6 +176,22 @@ class TranslationHistoryView extends GetView { ), ), SizedBox(width: 12.w), + // 删除会话按钮 + InkWell( + onTap: () => _showDeleteSessionConfirmDialog(item.sessionId), + borderRadius: BorderRadius.circular(6.r), + splashColor: Colors.red.withOpacity(0.1), + highlightColor: Colors.red.withOpacity(0.05), + child: Container( + padding: EdgeInsets.all(8.w), + child: Icon( + Icons.delete_outline, + color: isDarkMode ? Colors.grey[500] : Colors.grey[400], + size: 18.sp, + ), + ), + ), + SizedBox(width: 8.w), Icon( Icons.chevron_right, color: isDarkMode ? Colors.grey[600] : Colors.grey[350], @@ -229,9 +245,65 @@ class TranslationHistoryView extends GetView { return '$sourceName ⇄ $targetName'; } + // 显示删除会话确认对话框 + void _showDeleteSessionConfirmDialog(String sessionId) { + final isDarkMode = Get.isDarkMode; + + Get.dialog( + AlertDialog( + backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white, + title: Text( + '删除会话记录', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.bold, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + content: Text( + '确定要删除该会话的所有翻译记录吗?此操作无法撤销。', + style: TextStyle( + fontSize: 16.sp, + color: isDarkMode ? Colors.grey[300] : Colors.black87, + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text( + 'cancel'.tr, + style: TextStyle( + fontSize: 16.sp, + color: isDarkMode ? Colors.grey[400] : Colors.grey[700], + ), + ), + ), + TextButton( + onPressed: () { + Get.back(); + // 删除指定会话的历史记录 + controller.deleteSessionHistory(sessionId); + }, + child: Text( + 'confirm'.tr, + style: TextStyle( + fontSize: 16.sp, + color: Colors.red, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + ), + ); + } + void _showClearHistoryConfirmDialog() { final isDarkMode = Get.isDarkMode; - + Get.dialog( AlertDialog( backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white, From 810cbb7d3dd2823bdb8ce2eb9aaf96a6bbe942ad Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sun, 6 Jul 2025 22:32:44 +0800 Subject: [PATCH 09/15] =?UTF-8?q?feat(=E8=81=8A=E5=A4=A9=E8=AE=B0=E5=BD=95?= =?UTF-8?q?):=20=E6=B7=BB=E5=8A=A0=E5=88=86=E9=A1=B5=E5=8A=A0=E8=BD=BD?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E6=B6=88=E6=81=AF=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现聊天记录的分页加载功能,当用户滚动到顶部时自动加载更多历史消息。新增加载状态指示器,包括加载中、加载失败和默认提示三种状态。优化消息列表性能,避免一次性加载所有历史记录。 --- .../agent/controllers/agent_controller.dart | 189 ++++++++++++++---- lib/modules/agent/views/agent_view.dart | 83 +++++++- 2 files changed, 231 insertions(+), 41 deletions(-) diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 455e3aeaa..ab76f6703 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -120,6 +120,13 @@ class AgentController extends GetxController { // 会话ID final String sessionId = 'default_agent'; + // 分页加载相关状态 + final isLoadingMore = false.obs; // 是否正在加载更多消息 + final hasMoreMessages = true.obs; // 是否还有更多消息 + final currentPage = 1.obs; // 当前页码 + final pageSize = 20; // 每页消息数量 + final isLoadingMoreError = false.obs; // 加载更多时是否出错 + // 是否已加载聊天记录 final isHistoryLoaded = false.obs; @@ -137,6 +144,7 @@ class AgentController extends GetxController { void onInit() { super.onInit(); _loadTtsPreference(); //加载TTS偏好设置 + _setupScrollListener(); //设置滚动监听器 _loadChatHistory(); //加载聊天历史记录 _subscribeToEvents(); //订阅代理服务事件 _initMusicStatus(); //初始化音乐状态 @@ -270,63 +278,33 @@ class AgentController extends GetxController { final result = await ChatStorage.getMessages( sessionId: sessionId, //会话ID page: 1, //从第一页开始 - pageSize: 20, //每页20条记录 + pageSize: pageSize, //每页20条记录 ); if (result.messages.isNotEmpty) { // 转换并添加消息到列表 - final historyMessages = result.messages.map((msg) { - final meta = msg.metadata != null && msg.metadata != "" - ? jsonDecode(msg.metadata!) - : null; // 如果为空,使用空对象 - - // 从元数据中提取图片信息 - final hasImage = meta != null && meta.containsKey('hasImage') - ? meta['hasImage'] as bool - : false; - final imagePath = meta != null && meta.containsKey('imagePath') - ? meta['imagePath'] as String? - : null; - - return Message( - isUser: msg.sender == 'user', - text: msg.message, - meta: meta, - timestamp: DateTime.parse(msg.timestamp), - hasImage: hasImage, - imagePath: imagePath, - ); - }).toList(); + final historyMessages = _convertChatMessagesToMessages(result.messages); // 消息按时间排序,最新的消息在底部 historyMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); messages.assignAll(historyMessages); - Logger.i(TAG, '成功加载 ${historyMessages.length} 条聊天记录'); - // 详细打印每条消息的内容和属性,便于调试 - Logger.d(TAG, '===== 历史消息详情开始 ====='); - for (int i = 0; i < historyMessages.length; i++) { - final msg = historyMessages[i]; - Logger.d( - TAG, - '消息[$i]: {' - 'isUser: ${msg.isUser}, ' - 'text: "${msg.text.length > 50 ? msg.text.substring(0, 50) + "..." : msg.text}", ' - 'time: ${msg.timestamp}, ' - 'hasImage: ${msg.hasImage}, ' - 'imagePath: ${msg.imagePath ?? "无"}, ' - 'meta: ${msg.meta != null ? "有" : "无"}' - '}'); - } - Logger.d(TAG, '===== 历史消息详情结束 ====='); + // 更新分页状态 + currentPage.value = 1; + hasMoreMessages.value = result.totalPages > 1; + + Logger.i(TAG, '成功加载 ${historyMessages.length} 条聊天记录,总页数: ${result.totalPages}'); + _logMessageDetails(historyMessages); } else { // 没有历史记录时添加欢迎消息 _addWelcomeMessage(); + hasMoreMessages.value = false; Logger.i(TAG, '没有历史记录,添加欢迎消息'); } } catch (e) { Logger.e(TAG, '加载聊天历史失败: $e'); _addWelcomeMessage(); + hasMoreMessages.value = false; } finally { isHistoryLoaded.value = true; messages.refresh(); @@ -334,6 +312,137 @@ class AgentController extends GetxController { } } + // 加载更多历史消息(分页加载) + Future loadMoreMessages() async { + // 防止重复加载 + if (isLoadingMore.value || !hasMoreMessages.value) { + Logger.d(TAG, '跳过加载更多:isLoadingMore=${isLoadingMore.value}, hasMoreMessages=${hasMoreMessages.value}'); + return; + } + + try { + isLoadingMore.value = true; + isLoadingMoreError.value = false; + + final nextPage = currentPage.value + 1; + Logger.i(TAG, '正在加载第 $nextPage 页历史消息...'); + + final result = await ChatStorage.getMessages( + sessionId: sessionId, + page: nextPage, + pageSize: pageSize, + ); + + if (result.messages.isNotEmpty) { + // 转换新消息 + final newMessages = _convertChatMessagesToMessages(result.messages); + + // 按时间排序新消息 + newMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); + + // 添加短暂延迟,让用户感知到加载过程 + await Future.delayed(const Duration(milliseconds: 300)); + + // 将新消息插入到现有消息列表的开头(因为是历史消息) + final updatedMessages = [...newMessages, ...messages]; + messages.assignAll(updatedMessages); + + // 更新分页状态 + currentPage.value = nextPage; + hasMoreMessages.value = nextPage < result.totalPages; + + Logger.i(TAG, '成功加载 ${newMessages.length} 条历史消息,当前页: $nextPage/${result.totalPages}'); + } else { + // 没有更多消息 + hasMoreMessages.value = false; + Logger.i(TAG, '没有更多历史消息'); + } + } catch (e) { + Logger.e(TAG, '加载更多消息失败: $e'); + isLoadingMoreError.value = true; + } finally { + isLoadingMore.value = false; + } + } + + // 转换ChatMessage到Message的通用方法 + List _convertChatMessagesToMessages(List chatMessages) { + return chatMessages.map((msg) { + final meta = msg.metadata != null && msg.metadata != "" + ? jsonDecode(msg.metadata!) + : null; // 如果为空,使用空对象 + + // 从元数据中提取图片信息 + final hasImage = meta != null && meta.containsKey('hasImage') + ? meta['hasImage'] as bool + : false; + final imagePath = meta != null && meta.containsKey('imagePath') + ? meta['imagePath'] as String? + : null; + + return Message( + isUser: msg.sender == 'user', + text: msg.message, + meta: meta, + timestamp: DateTime.parse(msg.timestamp), + hasImage: hasImage, + imagePath: imagePath, + ); + }).toList(); + } + + // 打印消息详情的通用方法 + void _logMessageDetails(List messageList) { + Logger.d(TAG, '===== 历史消息详情开始 ====='); + for (int i = 0; i < messageList.length; i++) { + final msg = messageList[i]; + Logger.d( + TAG, + '消息[$i]: {' + 'isUser: ${msg.isUser}, ' + 'text: "${msg.text.length > 50 ? msg.text.substring(0, 50) + "..." : msg.text}", ' + 'time: ${msg.timestamp}, ' + 'hasImage: ${msg.hasImage}, ' + 'imagePath: ${msg.imagePath ?? "无"}, ' + 'meta: ${msg.meta != null ? "有" : "无"}' + '}'); + } + Logger.d(TAG, '===== 历史消息详情结束 ====='); + } + + // 设置滚动监听器 + void _setupScrollListener() { + scrollController.addListener(() { + // 检查是否滚动到顶部(因为使用了reverse: true,顶部实际是最大滚动位置) + if (scrollController.position.pixels >= scrollController.position.maxScrollExtent - 100) { + // 当距离顶部100像素时触发加载更多 + if (hasMoreMessages.value && !isLoadingMore.value) { + Logger.d(TAG, '触发加载更多历史消息'); + loadMoreMessages(); + } + } + }); + } + + // 手动触发加载更多(用于测试或手动调用) + void triggerLoadMore() { + if (hasMoreMessages.value && !isLoadingMore.value) { + Logger.i(TAG, '手动触发加载更多历史消息'); + loadMoreMessages(); + } else { + Logger.w(TAG, '无法加载更多:hasMoreMessages=${hasMoreMessages.value}, isLoadingMore=${isLoadingMore.value}'); + } + } + + // 重置分页状态(用于刷新聊天记录) + void resetPagination() { + currentPage.value = 1; + hasMoreMessages.value = true; + isLoadingMore.value = false; + isLoadingMoreError.value = false; + Logger.i(TAG, '分页状态已重置'); + } + // 添加初始欢迎消息 void _addWelcomeMessage() { messages.add(Message( diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index 013166031..703aded40 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -200,8 +200,13 @@ class _AgentViewState extends State with WidgetsBindingObserver { reverse: true, padding: const EdgeInsets.symmetric( vertical: 16, horizontal: 16), - itemCount: controller.messages.length, + itemCount: controller.messages.length + (controller.hasMoreMessages.value ? 1 : 0), itemBuilder: (context, index) { + // 如果是最后一项且有更多消息,显示加载指示器 + if (index == controller.messages.length && controller.hasMoreMessages.value) { + return _buildLoadMoreIndicator(); + } + final message = controller .messages[controller.messages.length - 1 - index]; return Padding( @@ -1122,4 +1127,80 @@ class _AgentViewState extends State with WidgetsBindingObserver { ), ); } + + // 构建加载更多指示器 + Widget _buildLoadMoreIndicator() { + return Obx(() { + if (controller.isLoadingMore.value) { + // 正在加载状态 + return Container( + padding: const EdgeInsets.symmetric(vertical: 16), + child: const Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ), + SizedBox(width: 8), + Text( + '加载更多消息...', + style: TextStyle( + fontSize: 14, + color: Colors.grey, + ), + ), + ], + ), + ), + ); + } else if (controller.isLoadingMoreError.value) { + // 加载失败状态 + return Container( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Center( + child: GestureDetector( + onTap: () => controller.loadMoreMessages(), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.refresh, + size: 16, + color: Colors.grey, + ), + SizedBox(width: 8), + Text( + '加载失败,点击重试', + style: TextStyle( + fontSize: 14, + color: Colors.grey, + ), + ), + ], + ), + ), + ), + ); + } else { + // 默认状态(有更多消息但未加载) + return Container( + padding: const EdgeInsets.symmetric(vertical: 8), + child: const Center( + child: Text( + '向上滑动查看更多', + style: TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + ), + ); + } + }); + } } From 73a8aa8ccc7a73d827d0751758ae80376ac2fac5 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sun, 6 Jul 2025 23:31:41 +0800 Subject: [PATCH 10/15] =?UTF-8?q?feat(OTA=E5=8D=87=E7=BA=A7=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2):=20=E9=87=8D=E6=9E=84=E5=B9=B6=E4=BC=98=E5=8C=96OTA?= =?UTF-8?q?=E5=8D=87=E7=BA=A7=E9=A1=B5=E9=9D=A2UI=E5=92=8C=E4=BA=A4?= =?UTF-8?q?=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将设备信息卡片重构为独立组件,添加渐变背景和阴影效果 - 优化版本信息展示样式,增加NEW标签和状态指示器 - 改进加载、错误和进度指示器的视觉设计 - 使用响应式单位(.w/.h)替代固定尺寸 - 增强暗黑模式下的视觉表现 --- .../ota_upgrade/views/ota_upgrade_view.dart | 851 +++++++++++++----- 1 file changed, 637 insertions(+), 214 deletions(-) diff --git a/lib/modules/ota_upgrade/views/ota_upgrade_view.dart b/lib/modules/ota_upgrade/views/ota_upgrade_view.dart index 8245d4096..eb4c64e3c 100644 --- a/lib/modules/ota_upgrade/views/ota_upgrade_view.dart +++ b/lib/modules/ota_upgrade/views/ota_upgrade_view.dart @@ -89,33 +89,17 @@ class OtaUpgradeView extends GetView { ), body: SingleChildScrollView( child: Padding( - padding: EdgeInsets.all(20), + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h), child: Column( mainAxisSize: MainAxisSize.min, children: [ - // 设备信息卡片 - Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - color: isDarkMode ? Colors.grey[900] : Colors.white, - child: Padding( - padding: EdgeInsets.all(20), - child: Column( - children: [ - _buildDeviceIcon(isDarkMode), - SizedBox(height: 20), - _buildVersionInfo(isDarkMode), - ], - ), - ), - ), - SizedBox(height: 20), + // 设备信息卡片 - 优化后的版本 + _buildDeviceCard(isDarkMode), + SizedBox(height: 24.h), - // 更新说明 - _buildUpdateNotes(isDarkMode), - SizedBox(height: 20), + // 更新说明 - 当有可用更新时显示 + //_buildUpdateNotes(isDarkMode), + SizedBox(height: 16.h), // 状态显示区域 Obx(() { @@ -135,257 +119,696 @@ class OtaUpgradeView extends GetView { ); } + // 设备信息卡片 + Widget _buildDeviceCard(bool isDarkMode) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: isDarkMode + ? [ + const Color(0xFF1A1A1A).withValues(alpha: 0.9), + const Color(0xFF2A2A2A).withValues(alpha: 0.9), + ] + : [ + const Color(0xFFF8FAFF), + const Color(0xFFE3F2FD), + ], + ), + borderRadius: BorderRadius.circular(20.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withValues(alpha: 0.3) + : Colors.black.withValues(alpha: 0.08), + blurRadius: 20, + offset: const Offset(0, 8), + spreadRadius: 0, + ), + ], + ), + child: Padding( + padding: EdgeInsets.all(24.w), + child: Column( + children: [ + // 设备图标区域 + _buildDeviceIcon(isDarkMode), + SizedBox(height: 24.h), + // 版本信息区域 + _buildVersionInfo(isDarkMode), + ], + ), + ), + ); + } + // 设备图标 - Widget _buildDeviceIcon(bool isDarkMode) => Image.asset( + Widget _buildDeviceIcon(bool isDarkMode) { + return Container( + padding: EdgeInsets.all(20.w), + decoration: BoxDecoration( + color: isDarkMode + ? Colors.white.withValues(alpha: 0.05) + : Colors.white.withValues(alpha: 0.7), + borderRadius: BorderRadius.circular(16.r), + border: Border.all( + color: isDarkMode + ? Colors.white.withValues(alpha: 0.1) + : Colors.blue.withValues(alpha: 0.1), + width: 1, + ), + ), + child: Image.asset( isDarkMode ? 'assets/images/headphone_dark.png' : 'assets/images/headphone_light.png', - height: 180.h, + height: 120.h, fit: BoxFit.contain, - ); + ), + ); + } - Widget _buildVersionInfo(bool isDarkMode) => Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, + // 版本信息显示 + Widget _buildVersionInfo(bool isDarkMode) { + return Column( + children: [ + // 当前版本信息 + _buildVersionInfoRow( + label: '当前固件版本'.tr, + version: controller.currentVersion, + isDarkMode: isDarkMode, + isLatest: false, + ), + SizedBox(height: 16.h), + // 最新版本信息 + _buildVersionInfoRow( + label: '最新固件版本'.tr, + version: controller.latestVersion, + isDarkMode: isDarkMode, + isLatest: true, + ), + // 版本状态指示器 + SizedBox(height: 20.h), + _buildVersionStatusIndicator(isDarkMode), + ], + ); + } + + // 版本信息行组件 + Widget _buildVersionInfoRow({ + required String label, + required RxString version, + required bool isDarkMode, + required bool isLatest, + }) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), + decoration: BoxDecoration( + color: isDarkMode + ? Colors.white.withValues(alpha: 0.03) + : Colors.white.withValues(alpha: 0.8), + borderRadius: BorderRadius.circular(12.r), + border: Border.all( + color: isDarkMode + ? Colors.white.withValues(alpha: 0.08) + : Colors.grey.withValues(alpha: 0.2), + width: 1, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, // 改为center对齐 + Text( + label, + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.grey[300] : Colors.grey[700], + fontWeight: FontWeight.w500, + ), + ), + Obx(() => Row( + children: [ + if (isLatest && controller.isUpdateAvailable.value) ...[ + Container( + padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 2.h), + decoration: BoxDecoration( + color: Colors.orange.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(8.r), + ), + child: Text( + 'NEW', + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.bold, + color: Colors.orange, + ), + ), + ), + SizedBox(width: 8.w), + ], + Text( + 'V ${version.value}', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode + ? (isLatest ? Colors.green[300] : Colors.blue[300]) + : (isLatest ? Colors.green[600] : Colors.blue[600]), + ), + ), + ], + )), + ], + ), + ); + } + + // 版本状态指示器 + Widget _buildVersionStatusIndicator(bool isDarkMode) { + return Obx(() { + if (controller.isUpdateAvailable.value) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.orange.withValues(alpha: 0.1), + Colors.red.withValues(alpha: 0.1), + ], + ), + borderRadius: BorderRadius.circular(20.r), + border: Border.all( + color: Colors.orange.withValues(alpha: 0.3), + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, children: [ - Text('当前固件版本:'.tr, - style: TextStyle( - fontSize: 18, - color: isDarkMode ? Colors.grey[300] : Colors.black)), - SizedBox(width: 8), // 改为width - Obx(() => Text('V ${controller.currentVersion}', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.normal, - color: isDarkMode ? Colors.blue[300] : Colors.blue))), + Icon( + Icons.update, + size: 16.sp, + color: Colors.orange, + ), + SizedBox(width: 6.w), + Text( + '发现新版本', + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + color: Colors.orange, + ), + ), ], ), - SizedBox(height: 8), // 改为height - Row( - crossAxisAlignment: CrossAxisAlignment.center, // 改为center对齐 + ); + } else { + return Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h), + decoration: BoxDecoration( + color: Colors.green.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(20.r), + border: Border.all( + color: Colors.green.withValues(alpha: 0.3), + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, children: [ - Text('最新固件版本:'.tr, - style: TextStyle( - fontSize: 18, - color: isDarkMode ? Colors.grey[300] : Colors.black)), - SizedBox(width: 8), // 改为width - Obx(() => Text('V ${controller.latestVersion}', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.normal, - color: isDarkMode ? Colors.blue[300] : Colors.blue))), + Icon( + Icons.check_circle, + size: 16.sp, + color: Colors.green, + ), + SizedBox(width: 6.w), + Text( + '已是最新版本', + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + color: Colors.green, + ), + ), ], ), - ], - ); + ); + } + }); + } - // 添加固件更新说明 - Widget _buildUpdateNotes(bool isDarkMode) => - Obx(() => controller.isUpdateAvailable.value - ? Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + // 固件更新说明 + Widget _buildUpdateNotes(bool isDarkMode) { + return Obx(() => controller.isUpdateAvailable.value && + controller.updatedescription.value.isNotEmpty + ? Container( + width: double.infinity, + margin: EdgeInsets.only(bottom: 8.h), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: isDarkMode + ? [ + Colors.amber[900]!.withValues(alpha: 0.1), + Colors.orange[900]!.withValues(alpha: 0.05), + ] + : [ + Colors.amber[50]!, + Colors.orange[100]!.withValues(alpha: 0.3), + ], ), - color: isDarkMode ? Colors.grey[900] : Colors.white, - child: Padding( - padding: EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( + borderRadius: BorderRadius.circular(16.r), + border: Border.all( + color: isDarkMode + ? Colors.amber[700]!.withValues(alpha: 0.3) + : Colors.amber[200]!.withValues(alpha: 0.5), + width: 1, + ), + ), + child: Padding( + padding: EdgeInsets.all(20.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: EdgeInsets.all(8.w), + decoration: BoxDecoration( + color: Colors.amber.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(8.r), + ), + child: Icon( Icons.new_releases, - color: isDarkMode ? Colors.amber[300] : Colors.amber, - size: 20, + color: isDarkMode ? Colors.amber[300] : Colors.amber[700], + size: 18.sp, ), - SizedBox(width: 8), - Text( - '更新内容'.tr, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: isDarkMode ? Colors.white : Colors.black, - ), + ), + SizedBox(width: 12.w), + Text( + '更新内容'.tr, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.amber[200] : Colors.amber[800], ), - ], + ), + ], + ), + SizedBox(height: 16.h), + Container( + width: double.infinity, + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode + ? Colors.white.withValues(alpha: 0.03) + : Colors.white.withValues(alpha: 0.7), + borderRadius: BorderRadius.circular(12.r), ), - Divider(), - SizedBox(height: 8), - // 这里可以从controller中获取更新说明,如果有的话 - Text( + child: Text( controller.updatedescription.value, style: TextStyle( - fontSize: 14, - color: isDarkMode ? Colors.grey[300] : Colors.grey[800], + fontSize: 14.sp, + height: 1.5, + color: isDarkMode ? Colors.grey[300] : Colors.grey[700], ), ), - ], - ), + ), + ], ), - ) - : SizedBox.shrink()); + ), + ) + : const SizedBox.shrink()); + } - Widget _buildLoadingIndicator(bool isDarkMode) => Column( + // 加载指示器 + Widget _buildLoadingIndicator(bool isDarkMode) { + return Container( + padding: EdgeInsets.all(32.w), + decoration: BoxDecoration( + color: isDarkMode + ? Colors.white.withValues(alpha: 0.03) + : Colors.white.withValues(alpha: 0.8), + borderRadius: BorderRadius.circular(20.r), + border: Border.all( + color: isDarkMode + ? Colors.white.withValues(alpha: 0.08) + : Colors.grey.withValues(alpha: 0.2), + width: 1, + ), + ), + child: Column( children: [ - CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation( - isDarkMode ? Colors.blue[300] : Colors.blue), + // 自定义加载动画 + SizedBox( + width: 50.w, + height: 50.w, + child: CircularProgressIndicator( + strokeWidth: 3, + valueColor: AlwaysStoppedAnimation( + isDarkMode ? const Color(0xFF0066CC) : Colors.blue, + ), + backgroundColor: isDarkMode + ? Colors.white.withValues(alpha: 0.1) + : Colors.grey.withValues(alpha: 0.2), + ), + ), + SizedBox(height: 24.h), + Text( + '正在获取设备信息...', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.grey[300] : Colors.grey[700], + ), + ), + SizedBox(height: 8.h), + Text( + '请保持设备连接', + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.grey[500] : Colors.grey[500], + ), ), - SizedBox(height: 20), - Text('正在获取设备信息...', - style: - TextStyle(color: isDarkMode ? Colors.grey[300] : Colors.grey)) ], - ); + ), + ); + } - Widget _buildErrorInfo(bool isDarkMode) => Container( - padding: EdgeInsets.all(16), - decoration: BoxDecoration( - color: - isDarkMode ? Colors.red[900]!.withOpacity(0.2) : Colors.red[50], - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: isDarkMode ? Colors.red[700]! : Colors.red[200]!, - width: 1, - ), + // 错误信息显示 + Widget _buildErrorInfo(bool isDarkMode) { + return Container( + padding: EdgeInsets.all(24.w), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: isDarkMode + ? [ + Colors.red[900]!.withValues(alpha: 0.15), + Colors.red[800]!.withValues(alpha: 0.1), + ] + : [ + Colors.red[50]!, + Colors.red[100]!.withValues(alpha: 0.3), + ], ), - child: Column( - children: [ - Icon(Icons.error_outline, color: Colors.red, size: 40), - SizedBox(height: 10), - Text(controller.errorMessage.value, - textAlign: TextAlign.center, + borderRadius: BorderRadius.circular(20.r), + border: Border.all( + color: isDarkMode + ? Colors.red[700]!.withValues(alpha: 0.3) + : Colors.red[200]!.withValues(alpha: 0.5), + width: 1, + ), + ), + child: Column( + children: [ + // 错误图标 + Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: Colors.red.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(50.r), + ), + child: Icon( + Icons.error_outline, + color: Colors.red, + size: 32.sp, + ), + ), + SizedBox(height: 16.h), + // 错误标题 + Text( + '连接失败', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.red[300] : Colors.red[700], + ), + ), + SizedBox(height: 8.h), + // 错误信息 + Text( + controller.errorMessage.value, + textAlign: TextAlign.center, + style: TextStyle( + color: isDarkMode ? Colors.red[200] : Colors.red[600], + fontSize: 14.sp, + height: 1.4, + ), + ), + SizedBox(height: 24.h), + // 重试按钮 + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + icon: Icon(Icons.refresh, size: 18.sp), + label: Text( + '重试连接'.tr, style: TextStyle( - color: isDarkMode ? Colors.red[300] : Colors.red, - fontSize: 16)), - SizedBox(height: 20), - ElevatedButton.icon( - icon: Icon(Icons.refresh), - label: Text('重试连接'.tr), + fontSize: 16.sp, + fontWeight: FontWeight.w500, + ), + ), style: ElevatedButton.styleFrom( - backgroundColor: isDarkMode ? Colors.blue[700] : Colors.blue, + backgroundColor: isDarkMode ? const Color(0xFF0066CC) : Colors.blue, foregroundColor: Colors.white, - padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), + padding: EdgeInsets.symmetric(vertical: 14.h), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16.r), ), + elevation: 0, ), onPressed: controller.retryConnection, - ) - ], - ), - ); + ), + ), + ], + ), + ); + } + // 更新按钮 Widget _buildUpdateButton(bool isDarkMode) { - return controller.isUpgrade.value - ? _buildProgressIndicator(isDarkMode) - : Container( - width: double.infinity, - padding: EdgeInsets.symmetric(vertical: 10), - child: Column( - children: [ - ElevatedButton.icon( - icon: Icon( - controller.isUpdateAvailable.value - ? Icons.system_update - : Icons.check_circle, + return Obx(() { + if (controller.isUpgrade.value) { + return _buildProgressIndicator(isDarkMode); + } + + return SizedBox( + width: double.infinity, + child: Column( + children: [ + // 主要更新按钮 + Container( + width: double.infinity, + height: 56.h, + decoration: BoxDecoration( + gradient: controller.isUpdateAvailable.value + ? LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: isDarkMode + ? [const Color(0xFF0066CC), const Color(0xFF0052A3)] + : [Colors.blue, Colors.blue[700]!], + ) + : null, + color: !controller.isUpdateAvailable.value + ? (isDarkMode ? Colors.grey[800] : Colors.grey[300]) + : null, + borderRadius: BorderRadius.circular(16.r), + boxShadow: controller.isUpdateAvailable.value + ? [ + BoxShadow( + color: Colors.blue.withValues(alpha: 0.3), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ] + : null, + ), + child: ElevatedButton.icon( + icon: Icon( + controller.isUpdateAvailable.value + ? Icons.system_update + : Icons.check_circle, + size: 20.sp, + ), + label: Text( + controller.isUpdateAvailable.value ? '立即更新' : '已是最新版本', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, ), - label: Text( - controller.isUpdateAvailable.value ? '立即更新' : '已是最新版本', - style: TextStyle(fontSize: 16), + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + foregroundColor: Colors.white, + shadowColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.r), ), - style: ElevatedButton.styleFrom( - backgroundColor: controller.isUpdateAvailable.value - ? (isDarkMode ? Colors.blue[700] : Colors.blue) - : (isDarkMode ? Colors.grey[800] : Colors.grey), - foregroundColor: Colors.white, - padding: EdgeInsets.symmetric(horizontal: 40, vertical: 15), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30), - ), - minimumSize: Size(240, 50), + ), + onPressed: controller.isUpdateAvailable.value + ? controller.startUpdate + : null, + ), + ), + + // 更新提示信息 + if (controller.isUpdateAvailable.value) ...[ + SizedBox(height: 16.h), + Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode + ? Colors.orange.withValues(alpha: 0.1) + : Colors.orange[50], + borderRadius: BorderRadius.circular(12.r), + border: Border.all( + color: Colors.orange.withValues(alpha: 0.3), + width: 1, ), - onPressed: controller.isUpdateAvailable.value - ? controller.startUpdate - : null, ), - if (controller.isUpdateAvailable.value) ...[ - SizedBox(height: 8), - Text( - '取出双耳机,并保证电量均为30%以上的状态'.tr, - style: TextStyle( - fontSize: 14, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + child: Row( + children: [ + Icon( + Icons.info_outline, + color: Colors.orange, + size: 20.sp, ), - ), - ], - ], - ), - ); + SizedBox(width: 12.w), + Expanded( + child: Text( + '取出双耳机,并保证电量均为30%以上的状态'.tr, + style: TextStyle( + fontSize: 13.sp, + color: isDarkMode ? Colors.orange[200] : Colors.orange[800], + height: 1.3, + ), + ), + ), + ], + ), + ), + ], + ], + ), + ); + }); } + // 进度指示器 Widget _buildProgressIndicator(bool isDarkMode) { return Obx(() { final progress = controller.progress.value; final type = controller.type.value; - // // 处理升级失败状态:弹出 snackbar 并显示错误提示 - // if (type == OtaUpgradeController.UPGRADE_FAILED || - // type == OtaUpgradeController.CONNECTION_DISCONNECTION) { - // // 使用延迟确保在 build 完成后执行 - // Future.delayed(Duration.zero, () { - // Get.snackbar('升级失败', '重新连接设备后重试'); - // }); - - // return Container( - // width: double.infinity, - // padding: const EdgeInsets.symmetric(vertical: 10), - // child: Text( - // '升级失败,请重新连接设备', - // style: TextStyle( - // fontSize: 18, - // fontWeight: FontWeight.bold, - // color: Colors.red[700], - // ), - // ), - // ); - // } + final statusMessage = controller.getStatusMessage(type); - // 正常升级状态 return Container( width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 10), + padding: EdgeInsets.all(24.w), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: isDarkMode + ? [ + const Color(0xFF1A1A1A).withValues(alpha: 0.9), + const Color(0xFF2A2A2A).withValues(alpha: 0.9), + ] + : [ + const Color(0xFFF8FAFF), + const Color(0xFFE3F2FD), + ], + ), + borderRadius: BorderRadius.circular(20.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withValues(alpha: 0.3) + : Colors.black.withValues(alpha: 0.08), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ], + ), child: Column( children: [ - Text( - controller.getStatusMessage(type), - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: isDarkMode ? Colors.blue[300] : Colors.blue[700], - ), + // 状态图标和文字 + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: EdgeInsets.all(8.w), + decoration: BoxDecoration( + color: Colors.blue.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8.r), + ), + child: Icon( + Icons.system_update, + color: isDarkMode ? const Color(0xFF0066CC) : Colors.blue, + size: 20.sp, + ), + ), + SizedBox(width: 12.w), + Text( + statusMessage, + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.blue[300] : Colors.blue[700], + ), + ), + ], ), - const SizedBox(height: 16), - LinearProgressIndicator( - value: progress / 100.0, // 转换为 0.0-1.0 - backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[200], - valueColor: AlwaysStoppedAnimation( - isDarkMode ? Colors.blue[300]! : Colors.blue, + SizedBox(height: 24.h), + + // 进度条容器 + Container( + height: 8.h, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.white.withValues(alpha: 0.1) + : Colors.grey[200], + borderRadius: BorderRadius.circular(4.r), ), - ), - const SizedBox(height: 8), - Text( - '${progress.toStringAsFixed(1)}%', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: isDarkMode ? Colors.blue[300] : Colors.blue[700], + child: ClipRRect( + borderRadius: BorderRadius.circular(4.r), + child: LinearProgressIndicator( + value: progress / 100.0, + backgroundColor: Colors.transparent, + valueColor: AlwaysStoppedAnimation( + isDarkMode ? const Color(0xFF0066CC) : Colors.blue, + ), + ), ), ), + SizedBox(height: 16.h), + + // 进度百分比和详细信息 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${progress.toStringAsFixed(1)}%', + style: TextStyle( + fontSize: 24.sp, + fontWeight: FontWeight.bold, + color: isDarkMode ? Colors.blue[300] : Colors.blue[700], + ), + ), + Text( + '请勿断开连接', + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + ), + ], + ), ], ), ); From 1e551645e531d17903c0b4f6ff8b106d3bc9ee3e Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Mon, 7 Jul 2025 07:51:28 +0800 Subject: [PATCH 11/15] =?UTF-8?q?refactor(=E7=BF=BB=E8=AF=91=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1):=20=E7=A7=BB=E9=99=A4=E6=9C=AA=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E7=9A=84=E7=BF=BB=E8=AF=91=E6=9C=8D=E5=8A=A1=E5=8F=8A=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E8=AF=AD=E8=A8=80=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除不再使用的翻译服务实现(TranslationService)及其中英文语言文件(zh_cn.dart和en_us.dart) --- lib/core/themes/language/lang/en_us.dart | 95 ------------------- lib/core/themes/language/lang/zh_cn.dart | 93 ------------------ .../themes/language/translation_service.dart | 18 ---- 3 files changed, 206 deletions(-) delete mode 100644 lib/core/themes/language/lang/en_us.dart delete mode 100644 lib/core/themes/language/lang/zh_cn.dart delete mode 100644 lib/core/themes/language/translation_service.dart diff --git a/lib/core/themes/language/lang/en_us.dart b/lib/core/themes/language/lang/en_us.dart deleted file mode 100644 index e4c912e19..000000000 --- a/lib/core/themes/language/lang/en_us.dart +++ /dev/null @@ -1,95 +0,0 @@ -const Map enUS = { - "bottomNavHome": "home", - "bottomNavSleep": "sleep", - "bottomNavProfile": "profile", - "requestCancellation": "Request cancellation", - "connectionTimeout": "Connection timeout", - "requestTimeout": "Request timeout", - "responseTimeout": "Response timeout", - "unknownError": "Unknown error", - "welcomeHint": "DeepSound makes communication smoother", - "loginTitle": "Login/Register", - "contactCustomerService": "Connection", - "phoneHint": "Please enter your phone number", - "phoneError": "Wrong phone number!", - "emailHint": "Please enter email", - "emailError": "Email input error, please re-enter!", - "codeHint": "Please enter the verification code", - "codeError": "Verification code error", - "pleaseAgreeToTerms": - "Please check the box to agree to our Terms and Privacy Policy", - "getCode": "Get code", - "reacquireCode": "S Reacquire", - "bind": "Deterministic binding", - "login": "Login", - "otherLogin": "Other login methods", - "readAndAgree": "I have read and agree", - "serviceAgreement": " User Service Agreement ", - "and": "And", - "privacyAgreement": " User Privacy Agreement ", - "codeSend": "Verification code has been sent, please check email!", - "connectHeadphones": "Connect headphones", - "bindHeadphones": "Bind headphones", - "intelligentTool": "Intelligent tool", - "sessionMode": "AI conversation mode", - "translation": "Simultaneous translation", - "together": "Face-to-face translation", - "telephoneVoice": "Telephone translation", - "translationRecord": "Translation record", - "bindDevice": "Bind device", - "bound": "Bound", - "appVersion": "APP version", - "languageSetting": "Language setting", - "followingSystem": "Following system", - "userAgreement": "User agreement", - "privacyDeal": "Privacy agreement", - "chinese": "Chinese", - "english": "English", - "chineseComplex": "Chinese (Traditional)", - "japanese": "Japanese", - "korean": "Korean", - "russian": "Russian", - "french": "French", - "qrCode": "Authorization code or QR code", - "link": "Link", - "copy": "Copy", - "successfulReplication": "Successful replication", - "edit": "Edit", - "delete": "Delete", - "aiDialogue": "AI dialogue", - "initialChatMessage": - "Hi, I'm your new friend! Nice to meet you for the first time. I can answer your various questions, provide help with your work and studies, and even chat with you anytime. So, what would you like to ask?", - "defaultContent1": "Help me write a Python web scraper code", - "defaultContent2": "Help me plan a short video script", - "defaultContent3": "Help me design a mind-exercising game", - "defaultContent4": "Recommend a cost-effective phone", - "defaultContent5": "Come up with a creative name for my Honor of Kings ID", - "defaultContent6": - "Guide me on using Python to scrape real-time weather data", - "defaultContent7": "Tell me daily habits to boost kidney qi", - "defaultContent8": "Help me understand the basics of CNC programming", - "defaultContent9": "Help me find a short folk story", - "defaultContent10": "Recommend a cost-effective SUV", - "defaultContent11": "Teach me to make a small game using Python", - "defaultContent12": "Introduce some free-to-play mobile games", - "holdToSpeak": "Hold to speak", - "tapToSpeak": "Click to talk", - "hangUp": "Hang up", - "holdToStartSpeak": "Hold down and start talking", - "tapToStartSpeak": "Click to start Talking", - "sendMessage": "Ask me anything you want", - "beTalking": "Be talking ...", - "headphonesBeTalking": "Talking through headphones...", - "interlanguage": "Cannot choose the same language", - "notice": "Look out", - "connectingDevice": "Please set up the connected device first", - "bindDeviceTips": "Unbind the device first", - "cancel": "Cancel", - "confirm": "Confirm", - "goToSettings": "Go to Settings", - "goToBind": "Unbind", - "scanQrCode": "Scan the QR code", - "endRecording": "Do you want to end the recording?", - "earphone": "Earphone", - "voicerecorder": "VoiceRecorder" -}; diff --git a/lib/core/themes/language/lang/zh_cn.dart b/lib/core/themes/language/lang/zh_cn.dart deleted file mode 100644 index 604f31448..000000000 --- a/lib/core/themes/language/lang/zh_cn.dart +++ /dev/null @@ -1,93 +0,0 @@ -const Map zhCN = { - "bottomNavHome": "首页", - "bottomNavSleep": "睡眠", - "bottomNavProfile": "我的", - "requestCancellation": "请求取消", - "connectionTimeout": "连接超时", - "requestTimeout": "请求超时", - "responseTimeout": "响应超时", - "unknownError": "未知错误", - "welcomeHint": "DeepSound让沟通更顺畅", - "loginTitle": "登录/注册账号", - "contactCustomerService": "联系客服", - "phoneHint": "请输入手机号", - "phoneError": "手机号码错误!", - "emailHint": "请输入邮箱", - "emailError": "邮箱输入错误,请重新输入!", - "codeHint": "请输入验证码", - "codeError": "验证码错误", - "pleaseAgreeToTerms": "请勾选以同意用户协议和隐私政策", - "getCode": "获取验证码", - "reacquireCode": "S后重新获取", - "bind": "确定绑定", - "login": "登录", - "otherLogin": "其他登录方式", - "readAndAgree": "我已阅读并同意", - "serviceAgreement": "《用户服务协议》", - "and": "和", - "privacyAgreement": "《用户隐私协议》", - "codeSend": "验证码已发送,请到邮箱查看!", - "connectHeadphones": "请连接耳机", - "bindHeadphones": "请绑定耳机", - "intelligentTool": "智能工具", - "sessionMode": "AI对话模式", - "translation": "同声传译", - "together": "面对面翻译", - "telephoneVoice": "通话语音翻译", - "translationRecord": "翻译记录", - "bindDevice": "绑定设备", - "bound": "已绑定", - "appVersion": "APP版本", - "languageSetting": "语言设置", - "followingSystem": "跟随系统", - "userAgreement": "用户协议", - "privacyDeal": "隐私协议", - "chinese": "中文", - "english": "英语", - "chineseComplex": "中文(繁体)", - "japanese": "日语", - "korean": "韩语", - "russian": "俄语", - "french": "法语", - "qrCode": "授权码或二维码", - "link": "链接", - "copy": "复制", - "successfulReplication": "复制成功", - "edit": "编辑", - "delete": "删除", - "aiDialogue": "AI对话", - "initialChatMessage": - "嗨,我是你的新朋友!初次见面很开心。我呢,可以回答你的各种问题,给你工作学习上提供帮助,还能随时陪你聊天。嗯,你想问点什么呢?", - "defaultContent1": "请帮我写个Python爬虫代码", - "defaultContent2": "帮我策划一个短视频脚本", - "defaultContent3": "请帮我设计一个锻炼思维的小游戏", - "defaultContent4": "请推荐一部性价比高的手机", - "defaultContent5": "帮我想一个王者ID的创意名称", - "defaultContent6": "请指导我使用Python爬取实时天气数据", - "defaultContent7": "告诉我补肾气的日常习惯", - "defaultContent8": "帮我了解CNC编程的基础知识", - "defaultContent9": "帮我找一个简短的民间故事", - "defaultContent10": "给我推荐一辆性价比高的SUV", - "defaultContent11": "教我用Python制作小游戏", - "defaultContent12": "介绍一些适合零氪玩家的手游", - "holdToSpeak": "按住说话", - "tapToSpeak": "点击说话", - "hangUp": "挂断", - "holdToStartSpeak": "按住开始说话", - "tapToStartSpeak": "点击开始说话", - "sendMessage": "有什么问的尽管问我", - "beTalking": "正在说话...", - "headphonesBeTalking": "正在使用耳机说话...", - "interlanguage": "不能选择同语种", - "notice": "注意", - "connectingDevice": "请先去设置连接设备", - "bindDeviceTips": "请先去绑定设备", - "cancel": "取消", - "confirm": "确定", - "goToSettings": "去设置", - "goToBind": "去绑定", - "scanQrCode": "扫描二维码", - "endRecording": "你想结束录音吗?", - "earphone": "耳机", - "voicerecorder": "录音笔" -}; diff --git a/lib/core/themes/language/translation_service.dart b/lib/core/themes/language/translation_service.dart deleted file mode 100644 index 6ca44bda0..000000000 --- a/lib/core/themes/language/translation_service.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'dart:ui'; - -import 'package:get/get.dart'; - -import 'lang/en_us.dart'; -import 'lang/zh_cn.dart'; - -class TranslationService extends Translations { - static Locale? get locale => Get.deviceLocale; - static final fallbackLocale = Locale('zh', 'CN'); - static final supportedLocales = [ - const Locale('zh', 'CN'), - const Locale('en', 'US'), - ]; - - @override - Map> get keys => {'en_US': enUS, 'zh_CN': zhCN}; -} From 2fa63b9d365b7d9cf4d96ae0dfec88066f01ea91 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Mon, 7 Jul 2025 08:34:30 +0800 Subject: [PATCH 12/15] =?UTF-8?q?refactor(translation):=20=E5=B0=86?= =?UTF-8?q?=E7=A1=AC=E7=BC=96=E7=A0=81=E6=96=87=E6=9C=AC=E6=9B=BF=E6=8D=A2?= =?UTF-8?q?=E4=B8=BA=E5=9B=BD=E9=99=85=E5=8C=96=E7=BF=BB=E8=AF=91=E9=94=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改翻译历史、详情、列表和主视图中的硬编码文本为对应的翻译键 - 在中文翻译文件中添加缺失的翻译项 - 统一使用国际化方案管理界面文本 --- lib/core/translations/language/zh_cn.dart | 47 ++++++++++++++++++- .../views/translation_detail_view.dart | 40 ++++++++-------- .../views/translation_history_view.dart | 8 ++-- .../views/translation_list_view.dart | 28 +++++------ .../translation/views/translation_view.dart | 10 ++-- 5 files changed, 88 insertions(+), 45 deletions(-) diff --git a/lib/core/translations/language/zh_cn.dart b/lib/core/translations/language/zh_cn.dart index 43063954b..f0fa8b085 100644 --- a/lib/core/translations/language/zh_cn.dart +++ b/lib/core/translations/language/zh_cn.dart @@ -364,11 +364,54 @@ const Map zhCN = { "healthAdvisorWelcome": "您好,我是吴小江,很高兴能为您提供多功能AI聊天。我将用专业知识帮助您。请问有什么问题想咨询吗?", "healthAdvisorVoiceWelcome": "我在这里!", - // 翻译历史 + // 翻译模块 + // 翻译主页 (translation_view.dart) + "translationHistory": "翻译历史", + "enableRecording": "是否开启录音", + "viewAudioFiles": "查看录音文件", + "headsetUser": "耳机用户", + "phoneUser": "手机用户", + + // 翻译历史页 (translation_history_view.dart) "noTranslationHistory": "暂无历史记录", "clearHistory": "清除历史记录", "clearHistoryConfirm": "确定要清除所有翻译历史记录吗?此操作无法撤销。", - "translationHistory": "翻译历史", + "unknownLanguage": "未知语言", + "deleteSessionRecord": "删除会话记录", + "deleteSessionConfirm": "确定要删除该会话的所有翻译记录吗?此操作无法撤销。", + + // 翻译录音文件列表页 (translation_list_view.dart) + "translateAudioFiles": "翻译录音文件", + "noAudioFiles": "暂无音频文件", + "calculating": "计算中...", + "playing": "播放中...", + "paused": "已暂停", + "clickToPlay": "点击播放", + "rename": "重命名", + "smartTranscription": "智能转写", + "renameAudioFile": "重命名音频文件", + "enterNewFileName": "请输入新的文件名", + + // 翻译详情页 (translation_detail_view.dart) + "originalRecord": "原始记录", + "recordSummary": "记录摘要", + "clickToGenerateSummary": "点击下方按钮生成会话摘要", + "summaryContent": "摘要内容", + "copied": "已复制", + "summaryCopiledToClipboard": "摘要已复制到剪贴板", + "extractSummary": "提取摘要", + "summaryGenerated": "已生成摘要", + "generating": "生成中...", + "regenerate": "重新生成", + "summaryTimeoutMessage": "生成摘要请求超时,可能是网络问题或服务器繁忙。", + "deleteTranslationRecord": "删除翻译记录", + "deleteTranslationConfirm": "确定要删除这条翻译记录吗?此操作无法撤销。", + "originalText": "原文", + "translatedText": "译文", + "sessionSummary": "会话摘要", + "cannotShare": "无法分享", + "generateSummaryFirst": "请先生成摘要", + "translationSessionRecord": "翻译会话记录", //用户反馈页 "feedback": "用户反馈", diff --git a/lib/modules/translation/views/translation_detail_view.dart b/lib/modules/translation/views/translation_detail_view.dart index 52ad53397..fe0105f51 100644 --- a/lib/modules/translation/views/translation_detail_view.dart +++ b/lib/modules/translation/views/translation_detail_view.dart @@ -102,7 +102,7 @@ class TranslationDetailView extends GetView { children: [ Expanded( child: Obx(() => _buildTabButton( - title: '原始记录', + title: 'originalRecord'.tr, // 原始记录 isSelected: controller.selectedTabIndex.value == 0, onTap: () => controller.switchTab(0), isDarkMode: isDarkMode, @@ -112,7 +112,7 @@ class TranslationDetailView extends GetView { // SizedBox(width: 12.w), // Expanded( // child: Obx(() => _buildTabButton( - // title: '记录摘要', + // title: 'recordSummary'.tr, // 记录摘要 // isSelected: controller.selectedTabIndex.value == 1, // onTap: () => controller.switchTab(1), // isDarkMode: isDarkMode, @@ -361,7 +361,7 @@ class TranslationDetailView extends GetView { padding: EdgeInsets.all(24.r), child: Center( child: Text( - '点击下方按钮生成会话摘要', + 'clickToGenerateSummary'.tr, // 点击下方按钮生成会话摘要 style: TextStyle( fontSize: 15.sp, color: isDarkMode ? Colors.grey[400] : Colors.grey[600], @@ -384,7 +384,7 @@ class TranslationDetailView extends GetView { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '摘要内容', + 'summaryContent'.tr, // 摘要内容 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.bold, @@ -459,8 +459,8 @@ class TranslationDetailView extends GetView { void _copySummaryToClipboard() { Clipboard.setData(ClipboardData(text: controller.summaryResult.value)); Get.snackbar( - '已复制', - '摘要已复制到剪贴板', + 'copied'.tr, // 已复制 + 'summaryCopiledToClipboard'.tr, // 摘要已复制到剪贴板 snackPosition: SnackPosition.BOTTOM, duration: Duration(seconds: 2), ); @@ -490,7 +490,7 @@ class TranslationDetailView extends GetView { child: controller.isGeneratingSummary.value ? _buildGeneratingIndicator(isDarkMode) : Text( - controller.summaryResult.value.isEmpty ? '提取摘要' : '已生成摘要', + controller.summaryResult.value.isEmpty ? 'extractSummary'.tr : 'summaryGenerated'.tr, // 提取摘要 : 已生成摘要 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, @@ -515,7 +515,7 @@ class TranslationDetailView extends GetView { ), SizedBox(width: 12.w), Text( - '生成中...', + 'generating'.tr, // 生成中... style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, @@ -545,7 +545,7 @@ class TranslationDetailView extends GetView { ), ), child: Text( - '重新生成', + 'regenerate'.tr, // 重新生成 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, @@ -570,14 +570,14 @@ class TranslationDetailView extends GetView { Get.dialog( AlertDialog( title: Text( - '请求超时', + 'requestTimeout'.tr, // 请求超时 style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.bold, ), ), content: Text( - '生成摘要请求超时,可能是网络问题或服务器繁忙。', + 'summaryTimeoutMessage'.tr, // 生成摘要请求超时,可能是网络问题或服务器繁忙。 style: TextStyle( fontSize: 16.sp, ), @@ -586,7 +586,7 @@ class TranslationDetailView extends GetView { TextButton( onPressed: () => Get.back(), child: Text( - '取消', + 'cancel'.tr, // 取消 style: TextStyle( fontSize: 16.sp, color: Colors.grey[700], @@ -600,7 +600,7 @@ class TranslationDetailView extends GetView { controller.retryExtractSummaryWithExtendedTimeout(text); }, child: Text( - '重试', + 'retry'.tr, // 重试 style: TextStyle( fontSize: 16.sp, color: Colors.blue, @@ -624,7 +624,7 @@ class TranslationDetailView extends GetView { AlertDialog( backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white, title: Text( - '删除翻译记录', + 'deleteTranslationRecord'.tr, // 删除翻译记录 style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.bold, @@ -636,7 +636,7 @@ class TranslationDetailView extends GetView { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '确定要删除这条翻译记录吗?此操作无法撤销。', + 'deleteTranslationConfirm'.tr, // 确定要删除这条翻译记录吗?此操作无法撤销。 style: TextStyle( fontSize: 16.sp, color: isDarkMode ? Colors.grey[300] : Colors.black87, @@ -720,15 +720,15 @@ class TranslationDetailView extends GetView { // 分享原始记录 final String sourceText = _getCombinedSourceText(); final String translatedText = _getCombinedTranslatedText(); - shareText = '原文:\n$sourceText\n\n译文:\n$translatedText'; + shareText = '${'originalText'.tr}:\n$sourceText\n\n${'translatedText'.tr}:\n$translatedText'; // 原文:译文: } else { // 分享摘要 if (controller.summaryResult.value.isNotEmpty) { - shareText = '会话摘要:\n${controller.summaryResult.value}'; + shareText = '${'sessionSummary'.tr}:\n${controller.summaryResult.value}'; // 会话摘要: } else { Get.snackbar( - '无法分享', - '请先生成摘要', + 'cannotShare'.tr, // 无法分享 + 'generateSummaryFirst'.tr, // 请先生成摘要 snackPosition: SnackPosition.BOTTOM, duration: Duration(seconds: 2), ); @@ -739,7 +739,7 @@ class TranslationDetailView extends GetView { // 调用系统分享 Share.share( shareText, - subject: '翻译会话记录 - ${DateFormat('yyyy-MM-dd HH:mm').format(controller.currentItem.timestamp)}', + subject: '${'translationSessionRecord'.tr} - ${DateFormat('yyyy-MM-dd HH:mm').format(controller.currentItem.timestamp)}', // 翻译会话记录 ); } } \ No newline at end of file diff --git a/lib/modules/translation/views/translation_history_view.dart b/lib/modules/translation/views/translation_history_view.dart index 44733c576..52b35faca 100644 --- a/lib/modules/translation/views/translation_history_view.dart +++ b/lib/modules/translation/views/translation_history_view.dart @@ -238,8 +238,8 @@ class TranslationHistoryView extends GetView { final languageManager = Get.find(); // 获取源语言和目标语言的中文名称 - final sourceName = languageManager.getChineseNameByAsrCode(item.sourceLanguageCode) ?? '未知语言'; - final targetName = languageManager.getChineseNameByAsrCode(item.targetLanguageCode) ?? '未知语言'; + final sourceName = languageManager.getChineseNameByAsrCode(item.sourceLanguageCode) ?? 'unknownLanguage'.tr; // 未知语言 + final targetName = languageManager.getChineseNameByAsrCode(item.targetLanguageCode) ?? 'unknownLanguage'.tr; // 未知语言 // 使用双向箭头符号表示翻译方向 return '$sourceName ⇄ $targetName'; @@ -253,7 +253,7 @@ class TranslationHistoryView extends GetView { AlertDialog( backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white, title: Text( - '删除会话记录', + 'deleteSessionRecord'.tr, // 删除会话记录 style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.bold, @@ -261,7 +261,7 @@ class TranslationHistoryView extends GetView { ), ), content: Text( - '确定要删除该会话的所有翻译记录吗?此操作无法撤销。', + 'deleteSessionConfirm'.tr, // 确定要删除该会话的所有翻译记录吗?此操作无法撤销。 style: TextStyle( fontSize: 16.sp, color: isDarkMode ? Colors.grey[300] : Colors.black87, diff --git a/lib/modules/translation/views/translation_list_view.dart b/lib/modules/translation/views/translation_list_view.dart index dd13b7521..d559a8d8e 100644 --- a/lib/modules/translation/views/translation_list_view.dart +++ b/lib/modules/translation/views/translation_list_view.dart @@ -37,7 +37,7 @@ class TranslationListView extends GetView { onPressed: () => Get.back(), ), title: Text( - "翻译录音文件", // + "translateAudioFiles".tr, // 翻译录音文件 style: TextStyle( fontSize: 17.sp, fontWeight: FontWeight.w600, @@ -52,7 +52,7 @@ class TranslationListView extends GetView { return Obx(() { final fileList = controller.files; // 不要 toList() if (fileList.isEmpty) { - return Center(child: Text('暂无音频文件')); + return Center(child: Text('noAudioFiles'.tr)); // 暂无音频文件 } return ListView.builder( @@ -75,17 +75,17 @@ class TranslationListView extends GetView { return FutureBuilder( future: controller.getAudioDuration(file.path), builder: (context, snapshot) { - String durationText = "计算中..."; + String durationText = "calculating".tr; // 计算中... if (snapshot.hasData) { final duration = snapshot.data!; if (duration.inSeconds > 0) { durationText = "${duration.inMinutes}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}"; } else { - durationText = "未知"; + durationText = "unknown".tr; // 未知 } } else if (snapshot.hasError) { - durationText = "错误"; + durationText = "error".tr; // 错误 } return GestureDetector( onLongPressStart: (details) { @@ -216,8 +216,8 @@ class TranslationListView extends GetView { // 播放状态文本 Text( isCurrentFile - ? (isPlaying ? '播放中...' : '已暂停') - : '点击播放', + ? (isPlaying ? 'playing'.tr : 'paused'.tr) // 播放中... : 已暂停 + : 'clickToPlay'.tr, // 点击播放 style: TextStyle( color: Colors.blue, fontSize: 10.sp, @@ -269,7 +269,7 @@ class TranslationListView extends GetView { size: 20.w, color: isDarkMode ? Colors.white : Colors.black87, ), - '重命名', + 'rename'.tr, // 重命名 isDarkMode ? Colors.white : Colors.black87, () { // 弹出重命名输入框 @@ -282,7 +282,7 @@ class TranslationListView extends GetView { size: 20.w, color: isDarkMode ? Colors.white : Colors.black87, ), - '智能转写', + 'smartTranscription'.tr, // 智能转写 isDarkMode ? Colors.white : Colors.black87, () { Get.toNamed(Routes.meeting, arguments: { @@ -296,7 +296,7 @@ class TranslationListView extends GetView { size: 20.w, color: isDarkMode ? Colors.red[700] : Colors.red, ), - '删除', + 'delete'.tr, // 删除 isDarkMode ? Colors.red[700]! : Colors.red, () { file.delete(); @@ -358,7 +358,7 @@ class TranslationListView extends GetView { mainAxisSize: MainAxisSize.min, children: [ Text( - '重命名音频文件', + 'renameAudioFile'.tr, // 重命名音频文件 style: TextStyle( color: isDarkMode ? Colors.white : Colors.black87, fontSize: 16.sp, @@ -390,7 +390,7 @@ class TranslationListView extends GetView { fontSize: 16.sp, ), decoration: InputDecoration( - hintText: '请输入新的文件名', + hintText: 'enterNewFileName'.tr, // 请输入新的文件名 hintStyle: TextStyle( color: isDarkMode ? Colors.white70 : Colors.black54, fontSize: 16.sp, @@ -422,7 +422,7 @@ class TranslationListView extends GetView { borderRadius: BorderRadius.circular(12.w), ), child: Text( - '取消', + 'cancel'.tr, // 取消 style: TextStyle( fontSize: 14.sp, color: isDarkMode ? AppColors.primary : Colors.blue, @@ -455,7 +455,7 @@ class TranslationListView extends GetView { borderRadius: BorderRadius.circular(12.w), ), child: Text( - '确认', + 'confirm'.tr, // 确认 style: TextStyle( color: Colors.white, fontSize: 14.sp, diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index 239f5d823..1cd6779e3 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/lib/modules/translation/views/translation_view.dart @@ -154,7 +154,7 @@ class TranslationView extends GetView { size: 24.sp, ), onPressed: controller.toggleisRecord, - tooltip: '是否开启录音', + tooltip: 'enableRecording'.tr, // 是否开启录音 ), IconButton( icon: Icon( @@ -167,7 +167,7 @@ class TranslationView extends GetView { Get.toNamed(Routes.translationList); controller.setupScrollListener(); }, - tooltip: '查看录音文件', + tooltip: 'viewAudioFiles'.tr, // 查看录音文件 ), // 添加历史记录按钮 IconButton( @@ -180,7 +180,7 @@ class TranslationView extends GetView { await controller.stopAll(); Get.toNamed(Routes.translationHistory); }, - tooltip: '翻译历史', + tooltip: 'translationHistory'.tr, // 翻译历史 ), ], ); @@ -428,7 +428,7 @@ class TranslationView extends GetView { speakerId: 1, activeSpeaker: controller.activeSpeaker.value, icon: Icons.headset, // 耳机图标 - label: "耳机用户", + label: "headsetUser".tr, // 耳机用户 ), SizedBox(width: 40.w), // 手机图标按钮 (说话者2) @@ -437,7 +437,7 @@ class TranslationView extends GetView { speakerId: 2, activeSpeaker: controller.activeSpeaker.value, icon: Icons.phone_android, // 手机图标 - label: "手机用户", + label: "phoneUser".tr, // 手机用户 ), ], ); From 8fc12e58d01adf474ee08ea121d9f7905b836399 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Mon, 7 Jul 2025 10:16:16 +0800 Subject: [PATCH 13/15] =?UTF-8?q?feat(=E9=85=8D=E5=AF=B9=E6=A8=A1=E5=9D=97?= =?UTF-8?q?):=20=E6=B7=BB=E5=8A=A0=E9=85=8D=E5=AF=B9=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E7=9A=84=E5=A4=9A=E8=AF=AD=E8=A8=80=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为配对模块添加中文翻译并替换硬编码文本为多语言键 --- lib/core/translations/language/zh_cn.dart | 31 +++++++++++++ lib/modules/pairing/views/pairing_view.dart | 46 +++++++++---------- .../pairing/views/pairing_view_new.dart | 14 +++--- 3 files changed, 61 insertions(+), 30 deletions(-) diff --git a/lib/core/translations/language/zh_cn.dart b/lib/core/translations/language/zh_cn.dart index f0fa8b085..5b6f8624e 100644 --- a/lib/core/translations/language/zh_cn.dart +++ b/lib/core/translations/language/zh_cn.dart @@ -413,6 +413,37 @@ const Map zhCN = { "generateSummaryFirst": "请先生成摘要", "translationSessionRecord": "翻译会话记录", + // 配对模块 + // 配对页面 (pairing_view.dart & pairing_view_new.dart) + "devicePairing": "设备配对", + "connectYourDeepSoundHeadset": "连接您的 DeepSound 耳机", + "bind": "绑定", + "authorizeBind": "授权绑定", + "enterCode": "请输入代码", + "autoInputCode": "自动输入代码", + "skipBinding": "跳过绑定", + "rescan": "重新扫描", + "skip": "跳过", + + // 配对状态文本 + "readyToPair": "准备开始配对", + "preparingPairingEnvironment": "正在准备配对环境...", + "connectDeepSoundViaSystemBluetooth": "请先通过系统蓝牙连接DeapSound设备", + "verifyingDevice": "正在验证设备...", + "deviceBindingConfirm": "设备绑定确认...", + "establishingDeviceAssociation": "正在建立设备关联...", + "waitingCompletePairing": "请稍候,正在完成配对...", + "pairingSuccessRedirecting": "配对成功!即将跳转到主页...", + + // 配对错误状态文本 + "ensureConnectedDeepSoundDevice": "请确保连接的是DeapSound设备", + "deviceAlreadyBoundToOtherAccount": "该设备已被其他账号绑定", + "deviceAssociationFailed": "设备关联失败,请重试", + "scanTimeoutEnsureDeviceNearby": "扫描超时,请确保设备在附近", + "networkError": "网络错误", + "authorizationFailedCheckCode": "授权失败,请检查授权码是否正确", + "pairingFailedRetry": "配对失败,请重试", + //用户反馈页 "feedback": "用户反馈", 'feedbackInfo': '提供反馈或建议', diff --git a/lib/modules/pairing/views/pairing_view.dart b/lib/modules/pairing/views/pairing_view.dart index 98c0da59d..e2c57a222 100644 --- a/lib/modules/pairing/views/pairing_view.dart +++ b/lib/modules/pairing/views/pairing_view.dart @@ -305,44 +305,44 @@ class PairingView extends GetView { String _getSubtitleForState(PairingState state) { switch (state) { case PairingState.idle: - return '准备开始配对'; + return 'readyToPair'.tr; // 准备开始配对 case PairingState.checking: - return '正在准备配对环境...'; + return 'preparingPairingEnvironment'.tr; // 正在准备配对环境... case PairingState.waitingClassic: - return '请先通过系统蓝牙连接DeapSound设备'; + return 'connectDeepSoundViaSystemBluetooth'.tr; // 请先通过系统蓝牙连接DeapSound设备 case PairingState.verifying: - return '正在验证设备...'; + return 'verifyingDevice'.tr; // 正在验证设备... case PairingState.binding: - return '设备绑定确认...'; + return 'deviceBindingConfirm'.tr; // 设备绑定确认... case PairingState.associating: - return '正在建立设备关联...'; + return 'establishingDeviceAssociation'.tr; // 正在建立设备关联... case PairingState.waitingBle: - return '请稍候,正在完成配对...'; + return 'waitingCompletePairing'.tr; // 请稍候,正在完成配对... case PairingState.completed: - return '配对成功!即将跳转到主页...'; + return 'pairingSuccessRedirecting'.tr; // 配对成功!即将跳转到主页... case PairingState.error: final errorType = controller.errorType; switch (errorType) { case ErrorType.bluetoothOff: - return '请开启蓝牙后重试'; + return 'bluetoothTurnedOff'.tr; // 蓝牙已关闭,请打开蓝牙 case ErrorType.noPermission: - return '需要蓝牙权限才能继续'; + return 'lackPermissions'.tr; // 缺少蓝牙权限 case ErrorType.noClassicDevice: - return '请先通过系统蓝牙连接DeapSound设备'; + return 'connectDeepSoundViaSystemBluetooth'.tr; // 请先通过系统蓝牙连接DeapSound设备 case ErrorType.deviceNotMatch: - return '请确保连接的是DeapSound设备'; + return 'ensureConnectedDeepSoundDevice'.tr; // 请确保连接的是DeapSound设备 case ErrorType.deviceBound: - return '该设备已被其他账号绑定'; + return 'deviceAlreadyBoundToOtherAccount'.tr; // 该设备已被其他账号绑定 case ErrorType.associationFailed: - return '设备关联失败,请重试'; + return 'deviceAssociationFailed'.tr; // 设备关联失败,请重试 case ErrorType.bleTimeout: - return '扫描超时,请确保设备在附近'; + return 'scanTimeoutEnsureDeviceNearby'.tr; // 扫描超时,请确保设备在附近 case ErrorType.networkError: - return '网络错误,请检查网络连接'; + return 'networkError'.tr; // 网络错误 case ErrorType.authnFailed: - return '授权失败,请检查授权码是否正确'; + return 'authorizationFailedCheckCode'.tr; // 授权失败,请检查授权码是否正确 default: - return '配对失败,请重试'; + return 'pairingFailedRetry'.tr; // 配对失败,请重试 } default: return ''; @@ -489,7 +489,7 @@ class PairingView extends GetView { minimumSize: Size(double.infinity, 56.h), ), child: Text( - "绑定", + "bind".tr, // 绑定 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, @@ -530,7 +530,7 @@ class PairingView extends GetView { controller: controller .codeInputController, // 你可以在 Controller 中声明这个 decoration: InputDecoration( - hintText: '请输入代码', + hintText: 'enterCode'.tr, // 请输入代码 border: OutlineInputBorder( borderRadius: BorderRadius.circular(12.r), ), @@ -552,7 +552,7 @@ class PairingView extends GetView { elevation: 0, ), child: Text( - '授权绑定', + 'authorizeBind'.tr, // 授权绑定 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, @@ -596,7 +596,7 @@ class PairingView extends GetView { minimumSize: Size(double.infinity, 48.h), ), child: Text( - '跳过绑定'.tr, // 稍后再说 + 'skipBinding'.tr, // 跳过绑定 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w500, @@ -615,7 +615,7 @@ class PairingView extends GetView { minimumSize: Size(double.infinity, 48.h), ), child: Text( - '重新扫描'.tr, // 稍后再说 + 'rescan'.tr, // 重新扫描 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w500, diff --git a/lib/modules/pairing/views/pairing_view_new.dart b/lib/modules/pairing/views/pairing_view_new.dart index 59026841f..2df7c4435 100644 --- a/lib/modules/pairing/views/pairing_view_new.dart +++ b/lib/modules/pairing/views/pairing_view_new.dart @@ -45,7 +45,7 @@ class PairingView extends GetView { return Column( children: [ Text( - '设备配对', + 'devicePairing'.tr, // 设备配对 style: TextStyle( fontSize: 24.sp, fontWeight: FontWeight.bold, @@ -54,7 +54,7 @@ class PairingView extends GetView { ), SizedBox(height: 8.h), Text( - '连接您的 DeepSound 耳机', + 'connectYourDeepSoundHeadset'.tr, // 连接您的 DeepSound 耳机 style: TextStyle( fontSize: 14.sp, color: isDarkMode ? Colors.grey[400] : Colors.grey[600], @@ -208,7 +208,7 @@ class PairingView extends GetView { // 主要操作按钮 if (showConnectButton) _buildPrimaryButton( - '连接设备', + 'connectDevice'.tr, // 连接设备 controller.onConnectTap, isDarkMode, ), @@ -218,7 +218,7 @@ class PairingView extends GetView { TextButton( onPressed: controller.confirmBindDevice, child: Text( - '绑定', + 'bind'.tr, // 绑定 style: TextStyle( fontSize: 14.sp, color: isDarkMode ? Colors.grey[400] : Colors.grey[600], @@ -228,7 +228,7 @@ class PairingView extends GetView { if (showRetryButton) _buildPrimaryButton( - '重试', + 'retry'.tr, // 重试 controller.onRetryTap, isDarkMode, ), @@ -250,7 +250,7 @@ class PairingView extends GetView { elevation: 0, ), child: Text( - '自动输入代码', + 'autoInputCode'.tr, // 自动输入代码 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, @@ -265,7 +265,7 @@ class PairingView extends GetView { TextButton( onPressed: controller.onSkipTap, child: Text( - '跳过', + 'skip'.tr, // 跳过 style: TextStyle( fontSize: 14.sp, color: isDarkMode ? Colors.grey[400] : Colors.grey[600], From a5478db82b651c4cbbf346b5d36b640944d3643c Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Mon, 7 Jul 2025 11:19:58 +0800 Subject: [PATCH 14/15] =?UTF-8?q?ota=E5=8D=87=E7=BA=A7bug=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/services/ble_manager.dart | 38 +++++++++++++++---- .../main/kotlin/com/example/ota/OtaPlugin.kt | 8 ++-- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index 1c921fa07..73fc14b9e 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -472,13 +472,37 @@ class BleManager extends GetxService { firmwareProductColor: '', ); } - - // 并行获取所有设备信息,不使用延迟 - final results = await Future.wait([ - _bleService.getBatteryInfo(), - _bleService.getDeviceVersion(), - _bleService.getProductId(), - ]); + final commands = [ + _bleService.getBatteryInfo, + _bleService.getDeviceVersion, + _bleService.getProductId, + ]; + + final delays = [ + Duration.zero, // 第一个命令立即执行 + const Duration(milliseconds: 100), // 第二个命令延迟100ms + const Duration(milliseconds: 200), // 第三个命令延迟200ms + ]; + + final futures = commands + .asMap() + .map((index, command) { + final delay = delays[index]; + return MapEntry( + index, + Future.delayed(delay, command), + ); + }) + .values + .toList(); + + final results = await Future.wait(futures); + // // 并行获取所有设备信息,不使用延迟 + // final results = await Future.wait([ + // _bleService.getBatteryInfo(), + // _bleService.getDeviceVersion(), + // _bleService.getProductId(), + // ]); Logger.info( '设备信息获取结果: 电池=${results[0]}, 版本=${results[1]}, 产品ID=${results[2]}'); diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt b/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt index 8f68906de..c030cb725 100644 --- a/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt @@ -337,10 +337,10 @@ class OtaPlugin : BleService.Callback, FlutterPlugin, MethodCallHandler { BleService.isEnterOta = false isDisconnect = false if(otaManager!= null) { - - otaManager?.bluetoothHelper?.stopScan() - - otaManager?.bluetoothHelper?.disconnectDevice(otaDevice) + BleService.removeCallback(this) + otaManager?.release() + otaManager= null + bluetoothHelper=null } result.success("") } else { From abdb76100d98278c3dae29ac8ac771ed6f2a7035 Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Mon, 7 Jul 2025 11:52:31 +0800 Subject: [PATCH 15/15] =?UTF-8?q?=E8=BF=9B=E5=85=A5=E7=BF=BB=E8=AF=91?= =?UTF-8?q?=E6=9A=82=E5=81=9C=E9=9F=B3=E4=B9=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/services/ble_manager.dart | 2 +- .../controllers/translation_controller.dart | 66 ++++++++++++++----- 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index 73fc14b9e..fcd6450ed 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -481,7 +481,7 @@ class BleManager extends GetxService { final delays = [ Duration.zero, // 第一个命令立即执行 const Duration(milliseconds: 100), // 第二个命令延迟100ms - const Duration(milliseconds: 200), // 第三个命令延迟200ms + const Duration(milliseconds: 100), // 第三个命令延迟200ms ]; final futures = commands diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index cff3a3247..4f12b8d40 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -10,6 +10,7 @@ import 'package:get_storage/get_storage.dart'; import 'package:intl/intl.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; +import '../../../data/services/music_manager.dart'; import '../../../data/services/volcano_translation_service.dart'; import '../../../data/services/tts_service.dart'; import '../../../data/services/language_manager.dart'; @@ -35,6 +36,8 @@ class TranslationController extends GetxController { final GetStorage _storage = GetStorage(); // 蓝牙服务 final bleManager = Get.find(); + //音乐播放管理 + final _musiceManager = Get.find(); // 音频输入源 bool _audioSourceType = false; //是否开启播放 @@ -58,6 +61,7 @@ class TranslationController extends GetxController { RxInt activeSpeaker = 0.obs; // 0=无激活, 1=按钮1, 2=按钮2 RxInt lastActiveSpeaker = 0.obs; // 上一次状态 + bool isContinuePlaying = false; // 是否继续播放 // 状态变量 final isRecognizing = false.obs; final isTranslating = false.obs; @@ -177,7 +181,7 @@ class TranslationController extends GetxController { // 初始化服务 _initServices(); - + suspendOtherServices(); // 使用微任务在布局完成后自动滚动到底部 WidgetsBinding.instance.addPostFrameCallback((_) { _scrollToBottom(); @@ -252,6 +256,32 @@ class TranslationController extends GetxController { } } + // 暂停其他服务 + Future suspendOtherServices() async { + try { + var status = await _musiceManager.getStatus(); + var isPlaying = status['isPlaying'] ?? false; + + if (isPlaying == true) { + isContinuePlaying = true; + await _musiceManager.pause(); // 暂停音乐 + } else { + isContinuePlaying = false; + } + } catch (e) { + Logger.error('初始化服务失败: ${e.toString()}'); + } + } + + // 恢复其他服务 + Future restoreOtherServices() async { + try { + if (isContinuePlaying) await _musiceManager.resume(); // 暂停音乐 + } catch (e) { + Logger.error('初始化服务失败: ${e.toString()}'); + } + } + // 初始化服务 Future _initServices() async { try { @@ -290,6 +320,7 @@ class TranslationController extends GetxController { _voiceInteractionSubscription?.cancel(); _translationDebounceTimer?.cancel(); scrollController.dispose(); + restoreOtherServices(); super.onClose(); } @@ -639,8 +670,7 @@ class TranslationController extends GetxController { } } } else { - Logger.info( - '未找到匹配的历史项 $sourceText ${timestamp.toIso8601String()}'); + Logger.info('未找到匹配的历史项 $sourceText ${timestamp.toIso8601String()}'); } } } catch (e) { @@ -873,14 +903,14 @@ class TranslationController extends GetxController { void saveTranslationHistory() { try { // 只保存当前内存中的完成态记录,过滤掉中间态记录 - final List finalRecords = translationHistory - .where((item) => !item.isIntermediate) - .toList(); + final List finalRecords = + translationHistory.where((item) => !item.isIntermediate).toList(); // 应用存储限制,保留最新的1000条完成态记录 List historyToSave = finalRecords; if (finalRecords.length > _maxStorageItems) { - historyToSave = finalRecords.sublist(finalRecords.length - _maxStorageItems); + historyToSave = + finalRecords.sublist(finalRecords.length - _maxStorageItems); } // 保存到本地存储 @@ -909,7 +939,8 @@ class TranslationController extends GetxController { historyList.map((item) => TranslationItem.fromJson(item)).toList(); for (final item in storedHistory) { - final id = '${item.sessionId}_${item.timestamp.millisecondsSinceEpoch}'; + final id = + '${item.sessionId}_${item.timestamp.millisecondsSinceEpoch}'; if (!existingIds.contains(id)) { existingIds.add(id); result.add(item); @@ -920,7 +951,8 @@ class TranslationController extends GetxController { // 添加当前内存中的完成态记录(排除中间态记录) for (final item in translationHistory) { if (!item.isIntermediate) { - final id = '${item.sessionId}_${item.timestamp.millisecondsSinceEpoch}'; + final id = + '${item.sessionId}_${item.timestamp.millisecondsSinceEpoch}'; if (!existingIds.contains(id)) { existingIds.add(id); result.add(item); @@ -974,7 +1006,8 @@ class TranslationController extends GetxController { // 3. 刷新UI显示 translationHistory.refresh(); - Logger.info('删除会话历史成功: sessionId=$sessionId, 内存删除${removedFromMemory}条, 存储删除${removedFromStorage}条'); + Logger.info( + '删除会话历史成功: sessionId=$sessionId, 内存删除${removedFromMemory}条, 存储删除${removedFromStorage}条'); } catch (e) { Logger.error('删除会话历史失败: ${e.toString()}'); rethrow; // 重新抛出异常,让调用方处理 @@ -988,7 +1021,8 @@ class TranslationController extends GetxController { final originalCount = translationHistory.length; translationHistory.removeWhere((item) => item.sessionId == sessionId && - item.timestamp.millisecondsSinceEpoch == timestamp.millisecondsSinceEpoch); + item.timestamp.millisecondsSinceEpoch == + timestamp.millisecondsSinceEpoch); final removedFromMemory = originalCount - translationHistory.length; // 2. 从本地存储中删除指定的翻译记录 @@ -1003,7 +1037,8 @@ class TranslationController extends GetxController { final originalStoredCount = storedHistory.length; storedHistory.removeWhere((item) => item.sessionId == sessionId && - item.timestamp.millisecondsSinceEpoch == timestamp.millisecondsSinceEpoch); + item.timestamp.millisecondsSinceEpoch == + timestamp.millisecondsSinceEpoch); removedFromStorage = originalStoredCount - storedHistory.length; // 重新保存更新后的存储数据 @@ -1015,7 +1050,8 @@ class TranslationController extends GetxController { // 3. 刷新UI显示 translationHistory.refresh(); - Logger.info('删除单条翻译记录成功: sessionId=$sessionId, timestamp=${timestamp.toIso8601String()}, 内存删除${removedFromMemory}条, 存储删除${removedFromStorage}条'); + Logger.info( + '删除单条翻译记录成功: sessionId=$sessionId, timestamp=${timestamp.toIso8601String()}, 内存删除${removedFromMemory}条, 存储删除${removedFromStorage}条'); } catch (e) { Logger.error('删除单条翻译记录失败: ${e.toString()}'); rethrow; // 重新抛出异常,让调用方处理 @@ -1026,10 +1062,6 @@ class TranslationController extends GetxController { void scrollToBottom({bool animate = true}) { _scrollToBottom(); } - - - - } // 翻译项模型