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'); + }); + }); +}