diff --git a/lib/modules/meeting/controllers/meeting_controller.dart b/lib/modules/meeting/controllers/meeting_controller.dart index 4277e1cea..f077169cc 100644 --- a/lib/modules/meeting/controllers/meeting_controller.dart +++ b/lib/modules/meeting/controllers/meeting_controller.dart @@ -7,6 +7,7 @@ import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; 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'; @@ -17,6 +18,8 @@ import '../../../core/utils/permission_util.dart'; import 'package:share_plus/share_plus.dart'; +import '../../../routes/app_routes.dart'; + class MeetingController extends GetxController { final ScrollController listController = ScrollController(); final AudioPlayer _audioPlayer = AudioPlayer(); @@ -38,6 +41,7 @@ class MeetingController extends GetxController { List dataListTask = []; bool isDisposed = false; + String filePath = ''; final String _appid = '2316081933'; final String _token = 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En'; @@ -57,6 +61,15 @@ class MeetingController extends GetxController { }; _getDataList(); addMeetingAudio(); + final Map args = Get.arguments ?? {}; + // 检查参数是否存在且包含有效路径 + if (args != null && + args.containsKey('filePath') && + args['filePath'] is String && + args['filePath'].isNotEmpty) { + final String path = args['filePath']; + addAudio(path); + } } @override @@ -251,6 +264,7 @@ class MeetingController extends GetxController { // 移动文件 await file.copy(newPath); await file.delete(); + addQuantity++; } } catch (e) { @@ -304,39 +318,53 @@ class MeetingController extends GetxController { EasyLoading.dismiss(); } - void addAudio() async { + void addAudio([String? filePath]) async { EasyLoading.show(status: '正在导入...'); - FilePickerResult? result = await FilePicker.platform.pickFiles( - withData: true, - ); - if (result != null) { - PlatformFile file = result.files.first; - String filePath = await _saveFile(file); - String fileName = file.name; - await _audioPlayer.setAudioSource(AudioSource.file(filePath)); - var audiourl = - await UploadOss.upload(filepath: filePath, rootDir: 'ExternalAudio'); - int seconds = _audioPlayer.duration?.inSeconds ?? 0; - int creationTime = DateTime.now().millisecondsSinceEpoch; - Map data = { - 'title': fileName, - 'type': 'IMPORT', - 'seconds': seconds, - 'filepath': filePath, - 'audiourl': audiourl, - 'tasktype': 0, - 'creationtime': creationTime, - }; - final meetingId = await SqfliteApi.insertMeeting(data); - if (meetingId != null) { - dataList.insert(0, { - 'id': meetingId, - 'taskid': '', - ...data, - }); - addQuantity++; + String fileName = ""; + bool isParamProvided = filePath != null; // 更清晰的变量名 + if (filePath != null) { + // 正确截取文件名(跨平台安全方式) + fileName = path.basename(filePath); + } else { + FilePickerResult? result = await FilePicker.platform.pickFiles( + withData: true, + ); + if (result != null) { + PlatformFile file = result.files.first; + filePath = await _saveFile(file); + fileName = file.name; } } + + await _audioPlayer.setAudioSource(AudioSource.file(filePath!)); + var audiourl = + await UploadOss.upload(filepath: filePath, rootDir: 'ExternalAudio'); + int seconds = _audioPlayer.duration?.inSeconds ?? 0; + int creationTime = DateTime.now().millisecondsSinceEpoch; + Map data = { + 'title': fileName, + 'type': 'IMPORT', + 'seconds': seconds, + 'filepath': filePath, + 'audiourl': audiourl, + 'tasktype': 0, + 'creationtime': creationTime, + }; + final meetingId = await SqfliteApi.insertMeeting(data); + if (meetingId != null) { + dataList.insert(0, { + 'id': meetingId, + 'taskid': '', + ...data, + }); + addQuantity++; + } + + if (isParamProvided) { + Get.toNamed(Routes.meetingDetails, arguments: { + 'id': meetingId, + }); + } EasyLoading.dismiss(); } diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart index aaae77c96..2fea38ea5 100644 --- a/lib/modules/meeting/controllers/meeting_record_controller.dart +++ b/lib/modules/meeting/controllers/meeting_record_controller.dart @@ -153,7 +153,8 @@ class MeetingRecordController extends GetxController } /// Saves new title and exits editing mode - void saveTitleChange() { + Future saveTitleChange() async { + isEditingTitle.value = true; if (isEditingTitle.value) { newName.value = titleEditingController.text; isEditingTitle.value = false; diff --git a/lib/modules/meeting/views/meeting_record_view.dart b/lib/modules/meeting/views/meeting_record_view.dart index ec10ee77b..10d288604 100644 --- a/lib/modules/meeting/views/meeting_record_view.dart +++ b/lib/modules/meeting/views/meeting_record_view.dart @@ -348,7 +348,7 @@ class MeetingRecordView extends GetView { Expanded( child: TextButton( onPressed: () async { - controller.saveTitleChange(); + await controller.saveTitleChange(); await controller.saveRecording(); Get.back(); }, diff --git a/lib/modules/translation/controllers/translation_list_controller.dart b/lib/modules/translation/controllers/translation_list_controller.dart index aa8f1a288..7b8d0a98e 100644 --- a/lib/modules/translation/controllers/translation_list_controller.dart +++ b/lib/modules/translation/controllers/translation_list_controller.dart @@ -1,18 +1,11 @@ -import 'dart:convert'; import 'dart:io'; - -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/material.dart'; +import 'package:audio_waveforms/audio_waveforms.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:get/get.dart'; -import 'package:http/http.dart' as http; import 'package:just_audio/just_audio.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; - import '../../../core/utils/logger.dart'; -import '../../../core/utils/upload_oss.dart'; -import '../../../data/services/db/sqflite_api.dart'; import '../../../core/utils/permission_util.dart'; import 'package:share_plus/share_plus.dart'; @@ -28,14 +21,21 @@ class TranslationListController extends GetxController { final appDocDir = getApplicationDocumentsDirectory(); // final directory = Directory( // '/storage/emulated/0/Android/data/com.yunqiinnovation.deepsound/files'); - List files = []; - TextEditingController titleController = TextEditingController(); // 录音标题 + final RxList files = [].obs; RxList dataList = [].obs; List dataListTask = []; bool isDisposed = false; - + // 添加音频播放器 + // 添加音频时长缓存 + final Map audioDurations = {}; + // 播放状态 + final RxBool isPlaying = false.obs; + // 当前播放的文件路径 + final RxString currentPlayingPath = ''.obs; + final AudioPlayer audioPlayer = AudioPlayer(); + final PlayerController playerController = PlayerController(); @override void onInit() { super.onInit(); @@ -49,16 +49,47 @@ class TranslationListController extends GetxController { // _getDataList(); // addMeetingAudio(); loadTranslatAudio(); + // 监听播放状态变化 + audioPlayer.playerStateStream.listen((playerState) { + isPlaying.value = playerState.playing; + if (playerState.processingState == ProcessingState.completed) { + // 播放完成后重置状态 + currentPlayingPath.value = ''; + } + }); } @override void onClose() { - titleController.dispose(); - - isDisposed = true; + audioPlayer.dispose(); // 释放播放器资源 super.onClose(); } +// 播放/暂停音频 + void togglePlay(FileSystemEntity file) async { + final filePath = file.path; + + if (currentPlayingPath.value == filePath) { + // 同一个文件:切换播放/暂停 + if (isPlaying.value) { + await audioPlayer.pause(); + } else { + await audioPlayer.play(); + } + } else { + // 新文件:停止当前播放,开始新文件 + currentPlayingPath.value = filePath; + try { + await audioPlayer.stop(); + await audioPlayer.setFilePath(filePath); + await audioPlayer.play(); + } catch (e) { + print("播放失败: $e"); + currentPlayingPath.value = ''; + } + } + } + // 请求存储权限 void _requestStoragePermission() async { // final BuildContext context = Get.overlayContext!; @@ -81,16 +112,22 @@ class TranslationListController extends GetxController { }); } - void loadTranslatopus() async { - final appDir = await appDocDir; // 先 await 获取目录 - final dir = Directory("${appDir.path}/OpusAudio"); - - print("appDocDir=${dir?.path}"); + // 添加方法:获取音频时长 + Future getAudioDuration(String filePath) async { + if (audioDurations.containsKey(filePath)) { + return audioDurations[filePath]!; + } - files = dir - .listSync() - .where((f) => f is File && (f.path.endsWith('.opus'))) - .toList(); + try { + final audioPlayer = AudioPlayer(); + await audioPlayer.setFilePath(filePath); + final duration = await audioPlayer.duration ?? Duration.zero; + audioDurations[filePath] = duration; + return duration; + } catch (e) { + print("获取音频时长失败: $e"); + return Duration.zero; + } } void loadTranslatAudio() async { @@ -102,12 +139,12 @@ class TranslationListController extends GetxController { // Create directory if it doesn't exist if (!await dir.exists()) { await dir.create(recursive: true); // Create directory tree if needed - files = []; // No files exist in new directory + files.value = []; // No files exist in new directory return; } // Directory exis - files = dir + files.value = dir .listSync() .where((f) => f is File && @@ -120,7 +157,7 @@ class TranslationListController extends GetxController { .toList(); } catch (e) { print("Error loading files: $e"); - files = []; // Return empty list on error + files.value = []; // Return empty list on error } } // 导入音频文件 @@ -144,48 +181,4 @@ class TranslationListController extends GetxController { EasyLoading.showToast('分享失败: ${e.toString()}'); } } - - 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, String title) async { - if (title.isNotEmpty) { - SqfliteApi.editMeetingTitle(id, {'title': title}); - int index = dataList.indexWhere((item) => item['id'] == id); - if (index != -1) { - final dataMap = Map.from(dataList[index]); - dataMap['title'] = title; - dataList[index] = dataMap; - } - } - Get.back(); - } - - // 删除数据 - void deleteMeeting(int id, String filepath) async { - dataList.removeWhere((item) => item['id'] == id); - // 删除数据库中的记录 - SqfliteApi.deleteMeeting(id); - _deleteFile(filepath); - addQuantity--; - } - - // 删除文件 - Future _deleteFile(String filepath) async { - File file = File(filepath); - bool fileExists = await file.exists(); - if (fileExists) { - await file.delete(); - } - } } diff --git a/lib/modules/translation/views/translation_list_view.dart b/lib/modules/translation/views/translation_list_view.dart index 2941eda68..a24f56c01 100644 --- a/lib/modules/translation/views/translation_list_view.dart +++ b/lib/modules/translation/views/translation_list_view.dart @@ -1,12 +1,10 @@ import 'dart:io'; -import 'package:common_utils/common_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:haptic_feedback/haptic_feedback.dart'; import 'package:intl/intl.dart'; - import '../../../core/theme/app_colors.dart'; import '../../../routes/app_routes.dart'; import '../controllers/translation_list_controller.dart'; @@ -39,7 +37,7 @@ class TranslationListView extends GetView { onPressed: () => Get.back(), ), title: Text( - 'conferenceAssistant'.tr, // 商务会议助手 + "翻译录音文件", // style: TextStyle( fontSize: 17.sp, fontWeight: FontWeight.w600, @@ -52,7 +50,8 @@ class TranslationListView extends GetView { Widget _buildAudioList(BuildContext context, bool isDarkMode) { return Obx(() { - if (controller.files.isEmpty) { + final fileList = controller.files; // 不要 toList() + if (fileList.isEmpty) { return Center(child: Text('暂无音频文件')); } @@ -72,115 +71,168 @@ class TranslationListView extends GetView { final fileName = file.path.split('/').last; final fileStat = file.statSync(); // 获取文件信息 final modifiedTime = fileStat.modified; - - return GestureDetector( - onTap: () { - // TODO: 播放音频 - print("播放音频: $fileName"); - }, - onLongPressStart: (details) { - _onLongPressAudioItem(context, isDarkMode, details, file); - print("长按操作: $fileName"); - }, - child: Container( - padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.w), - margin: EdgeInsets.symmetric(vertical: 5.w), - decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, - borderRadius: BorderRadius.circular(12.w), - boxShadow: [ - BoxShadow( - color: isDarkMode - ? Colors.black.withOpacity(0.2) - : Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - /// 文件名 - Text( - fileName, - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black87, - fontSize: 18.sp, - ), - overflow: TextOverflow.ellipsis, - ), - - /// 修改时间 & 大小 - Padding( - padding: EdgeInsets.symmetric(vertical: 5.w), - child: Row( - children: [ - Icon( - Icons.access_time, - size: 14.w, - color: isDarkMode ? Colors.grey[400] : Colors.grey[500], - ), - 4.horizontalSpace, - Text( - DateFormat('yyyy-MM-dd HH:mm').format(modifiedTime), - style: TextStyle( - color: isDarkMode ? Colors.grey[400] : Colors.grey[500], - fontSize: 12.sp, - ), - ), - 20.horizontalSpace, - Icon( - Icons.sd_storage, - size: 14.w, - color: isDarkMode ? Colors.grey[400] : Colors.grey[500], - ), - 4.horizontalSpace, - Text( - '${(fileStat.size / 1024).toStringAsFixed(1)} KB', - style: TextStyle( - color: isDarkMode ? Colors.grey[400] : Colors.grey[500], - fontSize: 12.sp, - ), - ), - ], - ), + // 添加 FutureBuilder 来异步获取音频时长 + return FutureBuilder( + future: controller.getAudioDuration(file.path), + builder: (context, snapshot) { + String durationText = "计算中..."; + if (snapshot.hasData) { + final duration = snapshot.data!; + if (duration.inSeconds > 0) { + durationText = + "${duration.inMinutes}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}"; + } else { + durationText = "未知"; + } + } else if (snapshot.hasError) { + durationText = "错误"; + } + return GestureDetector( + onLongPressStart: (details) { + _onLongPressAudioItem(context, isDarkMode, details, file); + print("长按操作: $fileName"); + }, + onTap: () { + // 添加点击播放功能 + controller.togglePlay(file); + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.w), + margin: EdgeInsets.symmetric(vertical: 5.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, + borderRadius: BorderRadius.circular(12.w), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], ), - - /// 文件类型标签 - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 2.w), - margin: EdgeInsets.only(top: 5.w), - decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[900] : Colors.grey[100], - borderRadius: BorderRadius.circular(4.w), - ), - child: Text( - file.path.split('.').last.toUpperCase(), - style: TextStyle( - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - fontSize: 10.sp, - ), + /// 文件名 + Text( + fileName, + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 18.sp, ), + overflow: TextOverflow.ellipsis, ), + + /// 修改时间 & 大小 Padding( - padding: EdgeInsets.only(top: 5.w), - child: Text( - '点击播放', - style: TextStyle( - color: Colors.blue, - fontSize: 10.sp, - ), + padding: EdgeInsets.symmetric(vertical: 5.w), + child: Row( + children: [ + Icon( + Icons.access_time, + size: 14.w, + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + ), + 4.horizontalSpace, + Text( + DateFormat('yyyy-MM-dd HH:mm').format(modifiedTime), + style: TextStyle( + color: + isDarkMode ? Colors.grey[400] : Colors.grey[500], + fontSize: 12.sp, + ), + ), + 20.horizontalSpace, + Icon( + Icons.sd_storage, + size: 14.w, + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + ), + 4.horizontalSpace, + Text( + '${(fileStat.size / 1024).toStringAsFixed(1)} KB', + style: TextStyle( + color: + isDarkMode ? Colors.grey[400] : Colors.grey[500], + fontSize: 12.sp, + ), + ), + 20.horizontalSpace, + Icon(Icons.music_note, + size: 14.w, + color: + isDarkMode ? Colors.grey[400] : Colors.grey[500]), + 4.horizontalSpace, + Text( + durationText, // 使用计算出的时长文本 + style: TextStyle( + color: + isDarkMode ? Colors.grey[400] : Colors.grey[500], + fontSize: 12.sp, + ), + ), + ], ), ), + + /// 文件类型标签 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: + EdgeInsets.symmetric(horizontal: 5.w, vertical: 2.w), + margin: EdgeInsets.only(top: 5.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.grey[100], + borderRadius: BorderRadius.circular(4.w), + ), + child: Text( + file.path.split('.').last.toUpperCase(), + style: TextStyle( + color: + isDarkMode ? Colors.grey[400] : Colors.grey[600], + fontSize: 10.sp, + ), + ), + ), + Obx(() { + final isCurrentFile = + controller.currentPlayingPath.value == file.path; + final isPlaying = controller.isPlaying.value; + + return Row( + children: [ + // 播放/暂停图标 + if (isCurrentFile) + Icon( + isPlaying ? Icons.pause : Icons.play_arrow, + size: 20.w, + color: Colors.blue, + ), + + // 播放状态文本 + Text( + isCurrentFile + ? (isPlaying ? '播放中...' : '已暂停') + : '点击播放', + style: TextStyle( + color: Colors.blue, + fontSize: 10.sp, + ), + ), + ], + ); + }), + ], + ), ], ), - ], - ), - ), + ), + ); + }, ); } @@ -237,6 +289,20 @@ class TranslationListView extends GetView { controller.loadTranslatAudio(); // 刷新列表 }, ), + _popupMenuEntry( + Icon( + Icons.delete_outline, + size: 20.w, + color: isDarkMode ? Colors.red[700] : Colors.red, + ), + '导入', + isDarkMode ? Colors.red[700]! : Colors.red, + () { + Get.toNamed(Routes.meeting, arguments: { + 'filePath': file.path, + }); + }, + ), ], ); }