Browse Source

优化翻译音频导入

weicu
fdp 1 year ago
parent
commit
c8298f2d79
  1. 88
      lib/modules/meeting/controllers/meeting_controller.dart
  2. 3
      lib/modules/meeting/controllers/meeting_record_controller.dart
  3. 2
      lib/modules/meeting/views/meeting_record_view.dart
  4. 133
      lib/modules/translation/controllers/translation_list_controller.dart
  5. 274
      lib/modules/translation/views/translation_list_view.dart

88
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:get/get.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:just_audio/just_audio.dart'; import 'package:just_audio/just_audio.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';
@ -17,6 +18,8 @@ import '../../../core/utils/permission_util.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import '../../../routes/app_routes.dart';
class MeetingController extends GetxController { class MeetingController extends GetxController {
final ScrollController listController = ScrollController(); final ScrollController listController = ScrollController();
final AudioPlayer _audioPlayer = AudioPlayer(); final AudioPlayer _audioPlayer = AudioPlayer();
@ -38,6 +41,7 @@ class MeetingController extends GetxController {
List dataListTask = []; List dataListTask = [];
bool isDisposed = false; bool isDisposed = false;
String filePath = '';
final String _appid = '2316081933'; final String _appid = '2316081933';
final String _token = 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En'; final String _token = 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En';
@ -57,6 +61,15 @@ class MeetingController extends GetxController {
}; };
_getDataList(); _getDataList();
addMeetingAudio(); addMeetingAudio();
final Map<String, dynamic> args = Get.arguments ?? {};
// 检查参数是否存在且包含有效路径
if (args != null &&
args.containsKey('filePath') &&
args['filePath'] is String &&
args['filePath'].isNotEmpty) {
final String path = args['filePath'];
addAudio(path);
}
} }
@override @override
@ -251,6 +264,7 @@ class MeetingController extends GetxController {
// 移动文件 // 移动文件
await file.copy(newPath); await file.copy(newPath);
await file.delete(); await file.delete();
addQuantity++; addQuantity++;
} }
} catch (e) { } catch (e) {
@ -304,39 +318,53 @@ class MeetingController extends GetxController {
EasyLoading.dismiss(); EasyLoading.dismiss();
} }
void addAudio() async { void addAudio([String? filePath]) async {
EasyLoading.show(status: '正在导入...'); EasyLoading.show(status: '正在导入...');
FilePickerResult? result = await FilePicker.platform.pickFiles( String fileName = "";
withData: true, bool isParamProvided = filePath != null; // 更清晰的变量名
); if (filePath != null) {
if (result != null) { // 正确截取文件名(跨平台安全方式)
PlatformFile file = result.files.first; fileName = path.basename(filePath);
String filePath = await _saveFile(file); } else {
String fileName = file.name; FilePickerResult? result = await FilePicker.platform.pickFiles(
await _audioPlayer.setAudioSource(AudioSource.file(filePath)); withData: true,
var audiourl = );
await UploadOss.upload(filepath: filePath, rootDir: 'ExternalAudio'); if (result != null) {
int seconds = _audioPlayer.duration?.inSeconds ?? 0; PlatformFile file = result.files.first;
int creationTime = DateTime.now().millisecondsSinceEpoch; filePath = await _saveFile(file);
Map<String, Object?> data = { fileName = file.name;
'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++;
} }
} }
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<String, Object?> 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(); EasyLoading.dismiss();
} }

3
lib/modules/meeting/controllers/meeting_record_controller.dart

@ -153,7 +153,8 @@ class MeetingRecordController extends GetxController
} }
/// Saves new title and exits editing mode /// Saves new title and exits editing mode
void saveTitleChange() { Future<void> saveTitleChange() async {
isEditingTitle.value = true;
if (isEditingTitle.value) { if (isEditingTitle.value) {
newName.value = titleEditingController.text; newName.value = titleEditingController.text;
isEditingTitle.value = false; isEditingTitle.value = false;

2
lib/modules/meeting/views/meeting_record_view.dart

@ -348,7 +348,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
Expanded( Expanded(
child: TextButton( child: TextButton(
onPressed: () async { onPressed: () async {
controller.saveTitleChange(); await controller.saveTitleChange();
await controller.saveRecording(); await controller.saveRecording();
Get.back(); Get.back();
}, },

133
lib/modules/translation/controllers/translation_list_controller.dart

@ -1,18 +1,11 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:audio_waveforms/audio_waveforms.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import 'package:just_audio/just_audio.dart'; import 'package:just_audio/just_audio.dart';
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 '../../../core/utils/logger.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 '../../../core/utils/permission_util.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
@ -28,14 +21,21 @@ class TranslationListController extends GetxController {
final appDocDir = getApplicationDocumentsDirectory(); final appDocDir = getApplicationDocumentsDirectory();
// final directory = Directory( // final directory = Directory(
// '/storage/emulated/0/Android/data/com.yunqiinnovation.deepsound/files'); // '/storage/emulated/0/Android/data/com.yunqiinnovation.deepsound/files');
List<FileSystemEntity> files = []; final RxList<FileSystemEntity> files = <FileSystemEntity>[].obs;
TextEditingController titleController = TextEditingController(); // 录音标题
RxList dataList = [].obs; RxList dataList = [].obs;
List dataListTask = []; List dataListTask = [];
bool isDisposed = false; bool isDisposed = false;
// 添加音频播放器
// 添加音频时长缓存
final Map<String, Duration> audioDurations = {};
// 播放状态
final RxBool isPlaying = false.obs;
// 当前播放的文件路径
final RxString currentPlayingPath = ''.obs;
final AudioPlayer audioPlayer = AudioPlayer();
final PlayerController playerController = PlayerController();
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -49,16 +49,47 @@ class TranslationListController extends GetxController {
// _getDataList(); // _getDataList();
// addMeetingAudio(); // addMeetingAudio();
loadTranslatAudio(); loadTranslatAudio();
// 监听播放状态变化
audioPlayer.playerStateStream.listen((playerState) {
isPlaying.value = playerState.playing;
if (playerState.processingState == ProcessingState.completed) {
// 播放完成后重置状态
currentPlayingPath.value = '';
}
});
} }
@override @override
void onClose() { void onClose() {
titleController.dispose(); audioPlayer.dispose(); // 释放播放器资源
isDisposed = true;
super.onClose(); 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 { void _requestStoragePermission() async {
// final BuildContext context = Get.overlayContext!; // final BuildContext context = Get.overlayContext!;
@ -81,16 +112,22 @@ class TranslationListController extends GetxController {
}); });
} }
void loadTranslatopus() async { // 添加方法:获取音频时长
final appDir = await appDocDir; // 先 await 获取目录 Future<Duration> getAudioDuration(String filePath) async {
final dir = Directory("${appDir.path}/OpusAudio"); if (audioDurations.containsKey(filePath)) {
return audioDurations[filePath]!;
print("appDocDir=${dir?.path}"); }
files = dir try {
.listSync() final audioPlayer = AudioPlayer();
.where((f) => f is File && (f.path.endsWith('.opus'))) await audioPlayer.setFilePath(filePath);
.toList(); final duration = await audioPlayer.duration ?? Duration.zero;
audioDurations[filePath] = duration;
return duration;
} catch (e) {
print("获取音频时长失败: $e");
return Duration.zero;
}
} }
void loadTranslatAudio() async { void loadTranslatAudio() async {
@ -102,12 +139,12 @@ class TranslationListController extends GetxController {
// Create directory if it doesn't exist // Create directory if it doesn't exist
if (!await dir.exists()) { if (!await dir.exists()) {
await dir.create(recursive: true); // Create directory tree if needed 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; return;
} }
// Directory exis // Directory exis
files = dir files.value = dir
.listSync() .listSync()
.where((f) => .where((f) =>
f is File && f is File &&
@ -120,7 +157,7 @@ class TranslationListController extends GetxController {
.toList(); .toList();
} catch (e) { } catch (e) {
print("Error loading files: $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()}'); EasyLoading.showToast('分享失败: ${e.toString()}');
} }
} }
Future<String> _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<String, dynamic>.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<void> _deleteFile(String filepath) async {
File file = File(filepath);
bool fileExists = await file.exists();
if (fileExists) {
await file.delete();
}
}
} }

274
lib/modules/translation/views/translation_list_view.dart

@ -1,12 +1,10 @@
import 'dart:io'; import 'dart:io';
import 'package:common_utils/common_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:haptic_feedback/haptic_feedback.dart'; import 'package:haptic_feedback/haptic_feedback.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../routes/app_routes.dart'; import '../../../routes/app_routes.dart';
import '../controllers/translation_list_controller.dart'; import '../controllers/translation_list_controller.dart';
@ -39,7 +37,7 @@ class TranslationListView extends GetView<TranslationListController> {
onPressed: () => Get.back(), onPressed: () => Get.back(),
), ),
title: Text( title: Text(
'conferenceAssistant'.tr, // 商务会议助手 "翻译录音文件", //
style: TextStyle( style: TextStyle(
fontSize: 17.sp, fontSize: 17.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -52,7 +50,8 @@ class TranslationListView extends GetView<TranslationListController> {
Widget _buildAudioList(BuildContext context, bool isDarkMode) { Widget _buildAudioList(BuildContext context, bool isDarkMode) {
return Obx(() { return Obx(() {
if (controller.files.isEmpty) { final fileList = controller.files; // 不要 toList()
if (fileList.isEmpty) {
return Center(child: Text('暂无音频文件')); return Center(child: Text('暂无音频文件'));
} }
@ -72,115 +71,168 @@ class TranslationListView extends GetView<TranslationListController> {
final fileName = file.path.split('/').last; final fileName = file.path.split('/').last;
final fileStat = file.statSync(); // 获取文件信息 final fileStat = file.statSync(); // 获取文件信息
final modifiedTime = fileStat.modified; final modifiedTime = fileStat.modified;
// 添加 FutureBuilder 来异步获取音频时长
return GestureDetector( return FutureBuilder<Duration>(
onTap: () { future: controller.getAudioDuration(file.path),
// TODO: 播放音频 builder: (context, snapshot) {
print("播放音频: $fileName"); String durationText = "计算中...";
}, if (snapshot.hasData) {
onLongPressStart: (details) { final duration = snapshot.data!;
_onLongPressAudioItem(context, isDarkMode, details, file); if (duration.inSeconds > 0) {
print("长按操作: $fileName"); durationText =
}, "${duration.inMinutes}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}";
child: Container( } else {
padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.w), durationText = "未知";
margin: EdgeInsets.symmetric(vertical: 5.w), }
decoration: BoxDecoration( } else if (snapshot.hasError) {
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, durationText = "错误";
borderRadius: BorderRadius.circular(12.w), }
boxShadow: [ return GestureDetector(
BoxShadow( onLongPressStart: (details) {
color: isDarkMode _onLongPressAudioItem(context, isDarkMode, details, file);
? Colors.black.withOpacity(0.2) print("长按操作: $fileName");
: Colors.black.withOpacity(0.05), },
blurRadius: 10, onTap: () {
offset: const Offset(0, 2), // 添加点击播放功能
), controller.togglePlay(file);
], },
), child: Container(
child: Column( padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.w),
crossAxisAlignment: CrossAxisAlignment.start, margin: EdgeInsets.symmetric(vertical: 5.w),
children: [ decoration: BoxDecoration(
/// 文件名 color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
Text( borderRadius: BorderRadius.circular(12.w),
fileName, boxShadow: [
style: TextStyle( BoxShadow(
color: isDarkMode ? Colors.white : Colors.black87, color: isDarkMode
fontSize: 18.sp, ? Colors.black.withOpacity(0.2)
), : Colors.black.withOpacity(0.05),
overflow: TextOverflow.ellipsis, blurRadius: 10,
), offset: const Offset(0, 2),
),
/// 修改时间 & 大小 ],
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,
),
),
],
),
), ),
child: Column(
/// 文件类型标签 crossAxisAlignment: CrossAxisAlignment.start,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Container( /// 文件名
padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 2.w), Text(
margin: EdgeInsets.only(top: 5.w), fileName,
decoration: BoxDecoration( style: TextStyle(
color: isDarkMode ? Colors.grey[900] : Colors.grey[100], color: isDarkMode ? Colors.white : Colors.black87,
borderRadius: BorderRadius.circular(4.w), fontSize: 18.sp,
),
child: Text(
file.path.split('.').last.toUpperCase(),
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
fontSize: 10.sp,
),
), ),
overflow: TextOverflow.ellipsis,
), ),
/// 修改时间 & 大小
Padding( Padding(
padding: EdgeInsets.only(top: 5.w), padding: EdgeInsets.symmetric(vertical: 5.w),
child: Text( child: Row(
'点击播放', children: [
style: TextStyle( Icon(
color: Colors.blue, Icons.access_time,
fontSize: 10.sp, 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<TranslationListController> {
controller.loadTranslatAudio(); // 刷新列表 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,
});
},
),
], ],
); );
} }

Loading…
Cancel
Save