import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:get/get.dart'; import 'package:just_audio/just_audio.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import '../../../core/utils/logger.dart'; import '../../../core/utils/synchrodata.dart'; import '../../../data/services/ble_manager.dart'; import '../../../data/services/db/sqflite_api.dart'; import '../../../core/utils/permission_util.dart'; import 'package:share_plus/share_plus.dart'; import '../../../data/services/meeting/meeting_task_service.dart'; import '../../../data/services/meeting/meeting_upload_service.dart'; import '../../../routes/app_routes.dart'; import 'meeting_controller.dart'; class MeetingHomeController extends GetxController { final meetingController = Get.find(); final meetingUploadService = Get.find(); final meetingTaskService = Get.find(); final AudioPlayer _audioPlayer = AudioPlayer(); List selected = []; final appDocDir = getApplicationDocumentsDirectory(); List files = []; TextEditingController titleController = TextEditingController(); // 录音标题 String titleCSuffix = ""; // 录音标题后缀 RxList originalDataList = [].obs; RxList dataList = [].obs; RxString filtrateType = ''.obs; bool isAsc = false; TextEditingController filtrateController = TextEditingController(); // 蓝牙服务 final bleManager = Get.find(); @override void onInit() { super.onInit(); _requestStoragePermission(); _initGetDataList(); addMeetingAudio(); final Map args = Get.arguments ?? {}; // 检查参数是否存在且包含有效路径 if (args.isNotEmpty && args.containsKey('filePath') && args['filePath'] is String && args['filePath'].isNotEmpty) { final String path = args['filePath']; addAudio(path); } } @override void onClose() { titleController.dispose(); filtrateController.dispose(); _audioPlayer.dispose(); super.onClose(); } void onPopInvokedWithResult(didPop, result) { Synchrodata.uploadOperationRecords(isSynchro: true); } void _initGetDataList() async { await Synchrodata.uploadOperationRecords(isSynchro: true); _getDataList(); } // 请求本地数据 void _getDataList() async { List list = await SqfliteApi.getMeetingList(); originalDataList.value = List.from(list); dataList.value = List.from(list); } // 请求存储权限 void _requestStoragePermission() async { // 只在Android 10及以下版本请求存储权限 if (Platform.isAndroid) { try { final deviceInfo = DeviceInfoPlugin(); final androidInfo = await deviceInfo.androidInfo; final sdkVersion = androidInfo.version.sdkInt; // Android 11+ (API 30+) 不需要存储权限 if (sdkVersion >= 30) { Logger.d("Permission", "Android 11+版本,无需请求存储权限"); return; } } catch (e) { Logger.e("Permission", "获取Android版本信息失败: $e"); // 如果获取版本失败,为了兼容性还是请求权限 } } PermissionUtil.instance .requestPermission( permissionType: Permission.storage, permissionName: "meetingStoragePermission".tr, // 对应中文:存储 explanationText: "meetingStoragePermissionExplanation" .tr, // 对应中文:我们需要访问存储权限,以便您可以选择并上传音频文件。 permanentDenialText: "meetingStoragePermissionPermanentDenial" .tr, // 对应中文:检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。 ) .then((granted) { if (granted) { Logger.d("Permission", "用户授予了存储权限"); } else { Logger.d("Permission", "用户未授予存储权限"); } }); } void loadTranslatopus() async { final dir = Directory("/data/user/0/com.saitong.voitrans/files/OpusAudio"); files = dir .listSync() .where((f) => f is File && (f.path.endsWith('.opus'))) .toList(); } void loadTranslatAudio() async { try { final appDir = await appDocDir; // 先 await 获取目录 final dir = Directory("${appDir.path}/TranslatAudio"); if (!await dir.exists()) { await dir.create(recursive: true); files = []; return; } files = dir .listSync() .where((f) => f is File && (f.path.endsWith('.mp3') || f.path.endsWith('.wav') || f.path.endsWith('.aac') || f.path.endsWith('.m4a') || f.path.endsWith('.mp4') || f.path.endsWith('.mov'))) .toList(); } catch (e) { files = []; } } Future shareAllTranslatAudio() async { final filePath = selected[0]; try { await Share.shareXFiles( [XFile(filePath)], text: 'shareAudio'.tr, // 对应中文:分享会议音频 ); EasyLoading.showToast('shareComplete'.tr); // 对应中文:分享完成! } catch (e) { EasyLoading.showToast('shareFailed' .tr .replaceAll('{error}', e.toString())); // 对应中文:分享失败: {error} } } void addMeetingAudio() async { EasyLoading.show(status: 'importingLocalRecordings'.tr); // 对应中文:正在导入本地录音... try { int addUp = 0; final appDir = await appDocDir; final sourceDirs = [ Directory("${appDir.path}/MeetingAudio/"), Directory("${appDir.path}/DestAudio/"), ]; // 目标目录:LocalAudio final destDir = Directory("${appDir.path}/LocalAudio/"); if (!await destDir.exists()) { // 检查目标目录是否存在 await destDir.create(recursive: true); } List allFiles = []; for (final dir in sourceDirs) { if (await dir.exists()) { final files = await dir.list().where((entity) => entity is File).toList(); allFiles.addAll(files); } } for (final fileEntity in allFiles) { try { final file = File(fileEntity.path); final filePath = file.path; // 设置音频源获取时长 await _audioPlayer.setAudioSource(AudioSource.file(filePath)); int seconds = _audioPlayer.duration?.inSeconds ?? 0; // 获取文件修改时间 final fileStat = await file.stat(); final creationTime = fileStat.changed.millisecondsSinceEpoch; // 准备新路径 final fileName = filePath.split('/').last; final newPath = "${destDir.path}/$fileName"; // 构建数据 Map data = { 'title': fileName, 'type': 'LOCAL', 'seconds': seconds, 'filepath': newPath, // 存储新路径 'audiourl': '', 'tasktype': 0, 'creationtime': creationTime, }; // 插入数据库 final meetingId = await SqfliteApi.insertMeeting(data); if (meetingId != null) { // 上传OSS meetingUploadService.addUpload(meetingId, newPath, 'LocalAudio'); originalDataList.insert(0, { 'id': meetingId, ...data, }); // 更新UI数据 if (isAsc) { dataList.add({ 'id': meetingId, ...data, }); } else { dataList.insert(0, { 'id': meetingId, ...data, }); } // 移动文件 await file.copy(newPath); await file.delete(); addUp++; } } catch (e) { print('fileProcessingFailed' .tr .replaceAll('{filePath}', fileEntity.path) .replaceAll('{error}', e.toString())); // 对应中文:文件处理失败: {filePath}, 错误: {error} } } if (addUp > 0) EasyLoading.showSuccess('importSuccess' .tr .replaceAll('{count}', '$addUp')); // 对应中文:成功导入本地 {count} 条录音 } catch (e) { EasyLoading.showError('importError' .tr .replaceAll('{error}', e.toString())); // 对应中文:导入失败: {error} } finally { EasyLoading.dismiss(); } } void addTranslatAudio() async { EasyLoading.show(status: 'batchImporting'.tr); // 对应中文:正在批量导入... for (final filePath in selected) { try { await _audioPlayer.setAudioSource(AudioSource.file(filePath)); final fileStat = await File(filePath).stat(); final creationTime = fileStat.changed.millisecondsSinceEpoch; // 时间戳 int seconds = _audioPlayer.duration?.inSeconds ?? 0; final fileName = filePath.split('/').last; Map data = { 'title': fileName, 'type': 'TRANSLAT', 'seconds': seconds, 'filepath': filePath, 'audiourl': '', 'tasktype': 0, 'creationtime': creationTime, }; final meetingId = await SqfliteApi.insertMeeting(data); if (meetingId != null) { meetingUploadService.addUpload(meetingId, filePath, 'TranslatAudio'); originalDataList.insert(0, { 'id': meetingId, ...data, }); if (isAsc) { dataList.add({ 'id': meetingId, ...data, }); } else { dataList.insert(0, { 'id': meetingId, ...data, }); } } } catch (e) { Logger.error('importFailed' .tr .replaceAll('{filePath}', filePath) .replaceAll('{error}', e.toString())); // 对应中文:导入文件失败: {filePath}, error: {error} } } EasyLoading.dismiss(); } void addAudio([String? filePath]) async { EasyLoading.show(status: 'importing'.tr); // 对应中文:正在导入... String fileName = ""; bool isParamProvided = filePath != null; // 更清晰的变量名 String audioType = 'IMPORT'; // 默认类型为IMPORT if (filePath != null) { // 正确截取文件名(跨平台安全方式) fileName = path.basename(filePath); // 检查是否来自翻译模块 if (filePath.contains('/TranslatAudio/')) { audioType = 'TRANSLAT'; // 设置为翻译音频类型 } } else { FilePickerResult? result = await FilePicker.platform.pickFiles( withData: false, //type: FileType.audio, allowMultiple: false, ); if (result != null) { PlatformFile file = result.files.first; if (!_isAudioFile(file.name)) { Get.snackbar( 'error'.tr, // 错误 'pleaseSelectAudioFile'.tr, // 对应中文:请选择音频文件 snackPosition: SnackPosition.TOP, backgroundColor: Colors.red.withOpacity(0.1), colorText: Colors.red, ); EasyLoading.dismiss(); return; } // 使用文件路径而不是字节数据 filePath = file.path ?? await _copyFileToLocal(file); fileName = file.name; } else { EasyLoading.dismiss(); return; } } final meetingId = await insertMeetingAudio(fileName, filePath, type: audioType); if (isParamProvided) { Get.toNamed(Routes.meetingDetails, arguments: { 'id': meetingId, }); } EasyLoading.dismiss(); } Future insertMeetingAudio( String fileName, String filePath, { bool isUpload = true, String type = 'IMPORT', //添加type参数,默认为'IMPORT' }) async { await _audioPlayer.setAudioSource(AudioSource.file(filePath)); int seconds = _audioPlayer.duration?.inSeconds ?? 0; int creationTime = DateTime.now().millisecondsSinceEpoch; Map data = { 'title': fileName, 'type': type, // 使用传入的type参数 'seconds': seconds, 'filepath': filePath, 'audiourl': '', 'tasktype': 0, 'creationtime': creationTime, }; final meetingId = await SqfliteApi.insertMeeting(data); if (meetingId != null) { meetingUploadService.addUpload( meetingId, filePath, 'ExternalAudio', isUpload: isUpload, ); originalDataList.insert(0, { 'id': meetingId, ...data, }); if (isAsc) { dataList.add({ 'id': meetingId, ...data, }); } else { dataList.insert(0, { 'id': meetingId, ...data, }); } } return meetingId; } // 新增:流式复制文件方法 Future _copyFileToLocal(PlatformFile file) async { final directory = await getApplicationDocumentsDirectory(); final directoryDir = Directory('${directory.path}/ExternalAudio'); if (!await directoryDir.exists()) { await directoryDir.create(recursive: true); } final String targetPath = '${directory.path}/ExternalAudio/${file.name}'; if (file.path != null) { // 如果有原始路径,直接复制文件 final sourceFile = File(file.path!); await sourceFile.copy(targetPath); } else { // 如果没有路径,使用流式写入(分块处理) final targetFile = File(targetPath); final sink = targetFile.openWrite(); try { // 分块写入,避免一次性加载大文件到内存 const chunkSize = 1024 * 1024; // 1MB chunks final bytes = file.bytes!; for (int i = 0; i < bytes.length; i += chunkSize) { final end = (i + chunkSize < bytes.length) ? i + chunkSize : bytes.length; sink.add(bytes.sublist(i, end)); // 给UI线程一些时间处理其他任务 await Future.delayed(Duration(milliseconds: 1)); } } finally { await sink.close(); } } return targetPath; } // 移除原来的_saveFile方法,替换为上面的_copyFileToLocal Future _saveFile(PlatformFile file) async { final directory = await getApplicationDocumentsDirectory(); final directoryDir = Directory('${directory.path}/ExternalAudio'); if (!await directoryDir.exists()) { await directoryDir.create(recursive: true); } final String filePath = '${directory.path}/ExternalAudio/${file.name}'; final File localFile = File(filePath); await localFile.writeAsBytes(file.bytes!); return filePath; } // 修改标题 void editMeetingTitle(int id) async { if (titleController.text.isNotEmpty) { String title = titleController.text + titleCSuffix; SqfliteApi.editMeetingTitle(id, {'title': title}); int originalIndex = originalDataList.indexWhere((item) => item['id'] == id); int index = dataList.indexWhere((item) => item['id'] == id); if (originalIndex != -1) { final dataMap = Map.from(originalDataList[originalIndex]); dataMap['title'] = title; originalDataList[originalIndex] = dataMap; } if (index != -1) { final dataMap = Map.from(dataList[index]); dataMap['title'] = title; dataList[index] = dataMap; } } Get.back(); } // 删除数据 void deleteMeeting(int id, String filepath) async { originalDataList.removeWhere((item) => item['id'] == id); dataList.removeWhere((item) => item['id'] == id); // 删除数据库中的记录 SqfliteApi.deleteMeeting(id); _deleteFile(filepath); meetingUploadService.removeUpload(id, isDeleteTask: true); meetingTaskService.removeTask(id, isDeleteTask: true); } // 删除文件 Future _deleteFile(String filepath) async { File file = File(filepath); bool fileExists = await file.exists(); if (fileExists) { await file.delete(); } } bool _isAudioFile(String fileName) { const List audioExtensions = ['.mp3', '.wav', '.m4a', '.aac']; final extension = path.extension(fileName).toLowerCase(); return audioExtensions.contains(extension); } }