Browse Source

fix(meeting): 修复录音文件名生成与文件遍历问题

修复录音文件名中可能包含路径分隔符的问题,新增 sanitizeRecordingFileStem 方法处理本地化标题中的特殊字符
修复文件遍历时未递归查找子目录的问题,使用 recursive: true 参数
修复活动录音文件比较时未统一使用 basename 的问题,避免路径不匹配
weicu
lxm 8 months ago
parent
commit
ecac9b3af6
  1. 15
      lib/modules/meeting/controllers/meeting_home_controller.dart
  2. 58
      lib/modules/meeting/controllers/meeting_record_controller.dart
  3. 31
      test/meeting_record_i18n_test.dart

15
lib/modules/meeting/controllers/meeting_home_controller.dart

@ -222,9 +222,10 @@ class MeetingHomeController extends GetxController {
for (final dir in sourceDirs) { for (final dir in sourceDirs) {
if (await dir.exists()) { if (await dir.exists()) {
//遍历每个源目录,筛选出文件实体(仅包含文件,不包含子目录) final files = await dir
final files = .list(recursive: true, followLinks: false)
await dir.list().where((entity) => entity is File).toList(); .where((entity) => entity is File)
.toList();
allFiles.addAll(files); //合并到总文件列表 allFiles.addAll(files); //合并到总文件列表
} }
} }
@ -233,8 +234,10 @@ class MeetingHomeController extends GetxController {
for (final fileEntity in allFiles) { for (final fileEntity in allFiles) {
try { try {
final entityName = path.basename(fileEntity.path); final entityName = path.basename(fileEntity.path);
if (activeRecordingWavName != null && final activeName = activeRecordingWavName == null
entityName == activeRecordingWavName) { ? null
: path.basename(activeRecordingWavName);
if (activeName != null && entityName == activeName) {
continue; continue;
} }
@ -356,7 +359,7 @@ class MeetingHomeController extends GetxController {
if (!controller.isRecording.value) return null; if (!controller.isRecording.value) return null;
final name = controller.fileName.value; final name = controller.fileName.value;
if (name.isEmpty) return null; if (name.isEmpty) return null;
return '$name.wav'; return path.basename('$name.wav');
} }
// 新增(2025-11-04):将翻译音频归档到 LocalAudio 并返回新路径(优先 rename,失败再 copy+delete) // 新增(2025-11-04):将翻译音频归档到 LocalAudio 并返回新路径(优先 rename,失败再 copy+delete)

58
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:get/get.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:floating_ui_plugin/native_plugin.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:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
@ -37,6 +38,43 @@ class MeetingRecordController extends GetxController
static FlutterLocalNotificationsPlugin? _localNotificationsPlugin; static FlutterLocalNotificationsPlugin? _localNotificationsPlugin;
final NativePlugin _nativePlugin = NativePlugin(); 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? _recordingFloatingBarTimer;
Timer? _routeMonitorTimer; Timer? _routeMonitorTimer;
StreamSubscription<String>? _recordingFloatingActionSubscription; StreamSubscription<String>? _recordingFloatingActionSubscription;
@ -536,15 +574,19 @@ class MeetingRecordController extends GetxController
// Generate timestamped filename // Generate timestamped filename
final formattedTime = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); 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(
await _asrService.enableRecord(false, "${dir.path}$fullFileName.wav", audioType.value != 0,
acceptAudioData: true); filePath,
} else { acceptAudioData: true,
await _asrService.enableRecord(true, "${dir.path}$fullFileName.wav", );
acceptAudioData: true);
}
// 通过 asrService 获取音频数据流 // 通过 asrService 获取音频数据流
_audioDataSubscription = _audioDataSubscription =

31
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');
});
});
}
Loading…
Cancel
Save