From 0e39764035c5137f2add76380fb7f35a3be111aa Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Wed, 4 Feb 2026 19:10:37 +0800 Subject: [PATCH] =?UTF-8?q?fix(meeting):=20=E4=BF=AE=E5=A4=8D=E5=BD=95?= =?UTF-8?q?=E9=9F=B3=E6=96=87=E4=BB=B6=E5=90=8D=E7=94=9F=E6=88=90=E4=B8=8E?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E9=81=8D=E5=8E=86=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复录音文件名中可能包含路径分隔符的问题,新增 sanitizeRecordingFileStem 方法处理本地化标题中的特殊字符 修复文件遍历时未递归查找子目录的问题,使用 recursive: true 参数 修复活动录音文件比较时未统一使用 basename 的问题,避免路径不匹配 --- .../controllers/meeting_home_controller.dart | 15 +++-- .../meeting_record_controller.dart | 58 ++++++++++++++++--- test/meeting_record_i18n_test.dart | 31 ++++++++++ 3 files changed, 90 insertions(+), 14 deletions(-) create mode 100644 test/meeting_record_i18n_test.dart diff --git a/lib/modules/meeting/controllers/meeting_home_controller.dart b/lib/modules/meeting/controllers/meeting_home_controller.dart index 0417fa05f..736826303 100644 --- a/lib/modules/meeting/controllers/meeting_home_controller.dart +++ b/lib/modules/meeting/controllers/meeting_home_controller.dart @@ -222,9 +222,10 @@ class MeetingHomeController extends GetxController { for (final dir in sourceDirs) { if (await dir.exists()) { - //遍历每个源目录,筛选出文件实体(仅包含文件,不包含子目录) - final files = - await dir.list().where((entity) => entity is File).toList(); + final files = await dir + .list(recursive: true, followLinks: false) + .where((entity) => entity is File) + .toList(); allFiles.addAll(files); //合并到总文件列表 } } @@ -233,8 +234,10 @@ class MeetingHomeController extends GetxController { for (final fileEntity in allFiles) { try { final entityName = path.basename(fileEntity.path); - if (activeRecordingWavName != null && - entityName == activeRecordingWavName) { + final activeName = activeRecordingWavName == null + ? null + : path.basename(activeRecordingWavName); + if (activeName != null && entityName == activeName) { continue; } @@ -356,7 +359,7 @@ class MeetingHomeController extends GetxController { if (!controller.isRecording.value) return null; final name = controller.fileName.value; if (name.isEmpty) return null; - return '$name.wav'; + return path.basename('$name.wav'); } // 新增(2025-11-04):将翻译音频归档到 LocalAudio 并返回新路径(优先 rename,失败再 copy+delete) diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart index ef5d7f58b..f03f22bf2 100644 --- a/lib/modules/meeting/controllers/meeting_record_controller.dart +++ b/lib/modules/meeting/controllers/meeting_record_controller.dart @@ -12,6 +12,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:floating_ui_plugin/native_plugin.dart'; +import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; @@ -37,6 +38,43 @@ class MeetingRecordController extends GetxController static FlutterLocalNotificationsPlugin? _localNotificationsPlugin; final NativePlugin _nativePlugin = NativePlugin(); + + static String sanitizeRecordingFileStem( + String input, { + String fallback = 'recording', + }) { + final trimmed = input.trim(); + if (trimmed.isEmpty) return fallback; + + var safe = trimmed + .replaceAll(RegExp(r'[\\/:*?"<>|]'), '_') + .replaceAll(RegExp(r'[\u0000-\u001F]'), '_') + .replaceAll(RegExp(r'\s+'), '_') + .replaceAll(RegExp(r'_+'), '_'); + + safe = safe.replaceAll(RegExp(r'^\.+'), ''); + safe = safe.replaceAll(RegExp(r'^_+|_+$'), ''); + + if (safe.isEmpty) return fallback; + if (safe.length > 80) { + safe = safe.substring(0, 80); + } + return safe; + } + + String _defaultRecordingFileStem() { + switch (audioType.value) { + case 0: + return 'live_recording'; + case 1: + return 'audio_video_recording'; + case 2: + return 'call_recording'; + default: + return 'recording'; + } + } + Timer? _recordingFloatingBarTimer; Timer? _routeMonitorTimer; StreamSubscription? _recordingFloatingActionSubscription; @@ -536,15 +574,19 @@ class MeetingRecordController extends GetxController // Generate timestamped filename final formattedTime = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); - final fullFileName = "${fileName.value}_$formattedTime"; + final baseTitle = newName.value.isNotEmpty ? newName.value : fileName.value; + final safeBase = sanitizeRecordingFileStem( + baseTitle, + fallback: _defaultRecordingFileStem(), + ); + final fullFileName = "${safeBase}_$formattedTime"; + final filePath = path.join(dir.path, '$fullFileName.wav'); - if (fileName.value == "liveRecording".tr) { - await _asrService.enableRecord(false, "${dir.path}$fullFileName.wav", - acceptAudioData: true); - } else { - await _asrService.enableRecord(true, "${dir.path}$fullFileName.wav", - acceptAudioData: true); - } + await _asrService.enableRecord( + audioType.value != 0, + filePath, + acceptAudioData: true, + ); // 通过 asrService 获取音频数据流 _audioDataSubscription = diff --git a/test/meeting_record_i18n_test.dart b/test/meeting_record_i18n_test.dart new file mode 100644 index 000000000..c09e04f1b --- /dev/null +++ b/test/meeting_record_i18n_test.dart @@ -0,0 +1,31 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:voitrans/modules/meeting/controllers/meeting_record_controller.dart'; + +void main() { + group('MeetingRecordController.sanitizeRecordingFileStem', () { + test('removes path separators from localized titles', () { + final safe = MeetingRecordController.sanitizeRecordingFileStem( + 'Audio/Video Recording', + ); + expect(safe.contains('/'), isFalse); + expect(safe.contains('\\'), isFalse); + expect(safe, isNotEmpty); + }); + + test('works for other locales that contain "/"', () { + final safe = MeetingRecordController.sanitizeRecordingFileStem( + 'تسجيل صوت/فيديو', + ); + expect(safe.contains('/'), isFalse); + expect(safe, isNotEmpty); + }); + + test('falls back when sanitized result is empty', () { + final safe = MeetingRecordController.sanitizeRecordingFileStem( + '///', + fallback: 'fallback', + ); + expect(safe, 'fallback'); + }); + }); +}