10 changed files with 1103 additions and 314 deletions
@ -0,0 +1,10 @@ |
|||||
|
import 'package:get/get.dart'; |
||||
|
import '../controllers/translation_list_controller.dart'; |
||||
|
|
||||
|
class TranslationListBinding extends Bindings { |
||||
|
@override |
||||
|
void dependencies() { |
||||
|
// 注册控制器 |
||||
|
Get.lazyPut<TranslationListController>(() => TranslationListController()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,191 @@ |
|||||
|
import 'dart:convert'; |
||||
|
import 'dart:io'; |
||||
|
|
||||
|
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: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'; |
||||
|
|
||||
|
class TranslationListController extends GetxController { |
||||
|
// 是否还有更多数据 |
||||
|
bool isGetMore = true; |
||||
|
bool isLoading = false; |
||||
|
int page = 1; |
||||
|
int addQuantity = 0; |
||||
|
List<String> selected = []; |
||||
|
|
||||
|
final appDocDir = getApplicationDocumentsDirectory(); |
||||
|
// final directory = Directory( |
||||
|
// '/storage/emulated/0/Android/data/com.yunqiinnovation.deepsound/files'); |
||||
|
List<FileSystemEntity> files = []; |
||||
|
TextEditingController titleController = TextEditingController(); // 录音标题 |
||||
|
|
||||
|
RxList dataList = [].obs; |
||||
|
|
||||
|
List dataListTask = []; |
||||
|
bool isDisposed = false; |
||||
|
|
||||
|
@override |
||||
|
void onInit() { |
||||
|
super.onInit(); |
||||
|
// listController.addListener(_onScroll); |
||||
|
_requestStoragePermission(); |
||||
|
//Permission.storage.request(); |
||||
|
// _headers = { |
||||
|
// 'Authorization': 'Bearer; $_token', |
||||
|
// 'Content-Type': 'application/json', |
||||
|
// }; |
||||
|
// _getDataList(); |
||||
|
// addMeetingAudio(); |
||||
|
loadTranslatAudio(); |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
void onClose() { |
||||
|
titleController.dispose(); |
||||
|
|
||||
|
isDisposed = true; |
||||
|
super.onClose(); |
||||
|
} |
||||
|
|
||||
|
// 请求存储权限 |
||||
|
void _requestStoragePermission() async { |
||||
|
// final BuildContext context = Get.overlayContext!; |
||||
|
|
||||
|
PermissionUtil.instance |
||||
|
.requestPermission( |
||||
|
// context: context, |
||||
|
permissionType: Permission.storage, |
||||
|
permissionName: "存储", |
||||
|
explanationText: "我们需要访问存储权限,以便您可以选择并上传音频文件。", |
||||
|
permanentDenialText: "检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。", |
||||
|
) |
||||
|
.then((granted) { |
||||
|
if (granted) { |
||||
|
Logger.d("Permission", "用户授予了存储权限"); |
||||
|
} else { |
||||
|
Logger.d("Permission", "用户未授予存储权限"); |
||||
|
// 可以提示用户无法进行文件操作 |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
void loadTranslatopus() async { |
||||
|
final appDir = await appDocDir; // 先 await 获取目录 |
||||
|
final dir = Directory("${appDir.path}/OpusAudio"); |
||||
|
|
||||
|
print("appDocDir=${dir?.path}"); |
||||
|
|
||||
|
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"); |
||||
|
|
||||
|
print("appDocDir=${dir?.path}"); |
||||
|
// 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 |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Directory exis |
||||
|
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) { |
||||
|
print("Error loading files: $e"); |
||||
|
files = []; // Return empty list on error |
||||
|
} |
||||
|
} |
||||
|
// 导入音频文件 |
||||
|
|
||||
|
Future<void> shareAllTranslatAudio() async { |
||||
|
final filePath = selected[0]; |
||||
|
// for (final filePath in selected) { |
||||
|
// if (filePath.isEmpty) { |
||||
|
// EasyLoading.showToast('没有可分享的音频文件'); |
||||
|
// return; |
||||
|
// } |
||||
|
// } |
||||
|
try { |
||||
|
await Share.shareXFiles( |
||||
|
[XFile(filePath)], |
||||
|
text: '分享会议音频', |
||||
|
); |
||||
|
|
||||
|
EasyLoading.showToast('分享完成!'); |
||||
|
} catch (e) { |
||||
|
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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,407 @@ |
|||||
|
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'; |
||||
|
|
||||
|
class TranslationListView extends GetView<TranslationListController> { |
||||
|
const TranslationListView({super.key}); |
||||
|
static OverlayEntry? _recordingOverlayEntry; |
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark; |
||||
|
|
||||
|
return Scaffold( |
||||
|
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], |
||||
|
appBar: _buildAppBar(context, isDarkMode), |
||||
|
body: _buildAudioList(context, isDarkMode), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 构建应用栏 |
||||
|
PreferredSizeWidget _buildAppBar(BuildContext context, bool isDarkMode) { |
||||
|
return AppBar( |
||||
|
backgroundColor: isDarkMode ? Colors.black : Colors.white, |
||||
|
elevation: 0.5, |
||||
|
leading: IconButton( |
||||
|
icon: Icon( |
||||
|
Icons.arrow_back_ios, |
||||
|
color: isDarkMode ? Colors.white : Colors.black, |
||||
|
size: 20.sp, |
||||
|
), |
||||
|
onPressed: () => Get.back(), |
||||
|
), |
||||
|
title: Text( |
||||
|
'conferenceAssistant'.tr, // 商务会议助手 |
||||
|
style: TextStyle( |
||||
|
fontSize: 17.sp, |
||||
|
fontWeight: FontWeight.w600, |
||||
|
color: isDarkMode ? Colors.white : Colors.black, |
||||
|
), |
||||
|
), |
||||
|
centerTitle: true, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Widget _buildAudioList(BuildContext context, bool isDarkMode) { |
||||
|
return Obx(() { |
||||
|
if (controller.files.isEmpty) { |
||||
|
return Center(child: Text('暂无音频文件')); |
||||
|
} |
||||
|
|
||||
|
return ListView.builder( |
||||
|
itemCount: controller.files.length, |
||||
|
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w), |
||||
|
itemBuilder: (context, index) { |
||||
|
final file = controller.files[index]; |
||||
|
return _audioItem(context, isDarkMode, file); |
||||
|
}, |
||||
|
); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
Widget _audioItem( |
||||
|
BuildContext context, bool isDarkMode, FileSystemEntity file) { |
||||
|
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, |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
|
||||
|
/// 文件类型标签 |
||||
|
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, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
Padding( |
||||
|
padding: EdgeInsets.only(top: 5.w), |
||||
|
child: Text( |
||||
|
'点击播放', |
||||
|
style: TextStyle( |
||||
|
color: Colors.blue, |
||||
|
fontSize: 10.sp, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
void _onLongPressAudioItem(BuildContext context, bool isDarkMode, |
||||
|
LongPressStartDetails details, FileSystemEntity file) { |
||||
|
Haptics.vibrate(HapticsType.success); |
||||
|
|
||||
|
RenderBox? renderBox = |
||||
|
Overlay.of(context).context.findRenderObject() as RenderBox; |
||||
|
|
||||
|
final RelativeRect position = RelativeRect.fromRect( |
||||
|
Rect.fromLTRB( |
||||
|
details.globalPosition.dx, |
||||
|
details.globalPosition.dy, |
||||
|
details.globalPosition.dx + 110, |
||||
|
details.globalPosition.dy - 40, |
||||
|
), |
||||
|
Offset.zero & renderBox.size, |
||||
|
); |
||||
|
|
||||
|
final fileName = file.path.split('/').last; |
||||
|
|
||||
|
showMenu( |
||||
|
context: context, |
||||
|
position: position, |
||||
|
color: isDarkMode ? AppColors.cardBackground : Colors.white, |
||||
|
shape: RoundedRectangleBorder( |
||||
|
borderRadius: BorderRadius.circular(12.r), |
||||
|
), |
||||
|
items: [ |
||||
|
_popupMenuEntry( |
||||
|
Icon( |
||||
|
Icons.drive_file_rename_outline, |
||||
|
size: 20.w, |
||||
|
color: isDarkMode ? Colors.white : Colors.black87, |
||||
|
), |
||||
|
'重命名', |
||||
|
isDarkMode ? Colors.white : Colors.black87, |
||||
|
() { |
||||
|
// 弹出重命名输入框 |
||||
|
_renameAudioBottomSheet(context, isDarkMode, file); |
||||
|
}, |
||||
|
), |
||||
|
_popupMenuEntry( |
||||
|
Icon( |
||||
|
Icons.delete_outline, |
||||
|
size: 20.w, |
||||
|
color: isDarkMode ? Colors.red[700] : Colors.red, |
||||
|
), |
||||
|
'删除', |
||||
|
isDarkMode ? Colors.red[700]! : Colors.red, |
||||
|
() { |
||||
|
file.delete(); |
||||
|
controller.loadTranslatAudio(); // 刷新列表 |
||||
|
}, |
||||
|
), |
||||
|
], |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
PopupMenuEntry _popupMenuEntry( |
||||
|
Icon icon, |
||||
|
String title, |
||||
|
Color color, |
||||
|
Function() onTap, |
||||
|
) { |
||||
|
return PopupMenuItem( |
||||
|
onTap: onTap, |
||||
|
child: SizedBox( |
||||
|
width: 140.w, |
||||
|
child: Row( |
||||
|
children: [ |
||||
|
icon, |
||||
|
8.horizontalSpace, |
||||
|
Text( |
||||
|
title, |
||||
|
style: TextStyle( |
||||
|
color: color, |
||||
|
fontSize: 14.sp, |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
void _renameAudioBottomSheet( |
||||
|
BuildContext context, |
||||
|
bool isDarkMode, |
||||
|
FileSystemEntity file, |
||||
|
) { |
||||
|
final TextEditingController _renameController = TextEditingController( |
||||
|
text: file.path.split('/').last, |
||||
|
); |
||||
|
|
||||
|
Get.bottomSheet( |
||||
|
Padding( |
||||
|
padding: MediaQuery.of(context).viewInsets, |
||||
|
child: Container( |
||||
|
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 20.w), |
||||
|
color: isDarkMode ? AppColors.darkBackground : Colors.grey[50], |
||||
|
child: Column( |
||||
|
crossAxisAlignment: CrossAxisAlignment.start, |
||||
|
mainAxisSize: MainAxisSize.min, |
||||
|
children: [ |
||||
|
Text( |
||||
|
'重命名音频文件', |
||||
|
style: TextStyle( |
||||
|
color: isDarkMode ? Colors.white : Colors.black87, |
||||
|
fontSize: 16.sp, |
||||
|
), |
||||
|
), |
||||
|
Container( |
||||
|
height: 48.w, |
||||
|
margin: EdgeInsets.only(top: 10.w, bottom: 20.w), |
||||
|
alignment: Alignment.center, |
||||
|
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: TextField( |
||||
|
controller: _renameController, |
||||
|
style: TextStyle( |
||||
|
color: isDarkMode ? Colors.white : Colors.black87, |
||||
|
fontSize: 16.sp, |
||||
|
), |
||||
|
decoration: InputDecoration( |
||||
|
hintText: '请输入新的文件名', |
||||
|
hintStyle: TextStyle( |
||||
|
color: isDarkMode ? Colors.white70 : Colors.black54, |
||||
|
fontSize: 16.sp, |
||||
|
), |
||||
|
fillColor: Colors.transparent, |
||||
|
border: InputBorder.none, |
||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 10.w), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
Padding( |
||||
|
padding: EdgeInsets.only(top: 30.w), |
||||
|
child: Row( |
||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
||||
|
children: [ |
||||
|
GestureDetector( |
||||
|
onTap: () { |
||||
|
Get.back(); |
||||
|
}, |
||||
|
child: Container( |
||||
|
width: 120.w, |
||||
|
height: 40.w, |
||||
|
alignment: Alignment.center, |
||||
|
decoration: BoxDecoration( |
||||
|
border: Border.all( |
||||
|
width: 2.w, |
||||
|
color: isDarkMode ? AppColors.primary : Colors.blue, |
||||
|
), |
||||
|
borderRadius: BorderRadius.circular(12.w), |
||||
|
), |
||||
|
child: Text( |
||||
|
'取消', |
||||
|
style: TextStyle( |
||||
|
fontSize: 14.sp, |
||||
|
color: isDarkMode ? AppColors.primary : Colors.blue, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
GestureDetector( |
||||
|
onTap: () async { |
||||
|
final newName = _renameController.text.trim(); |
||||
|
if (newName.isEmpty) return; |
||||
|
|
||||
|
final dir = file.parent.path; |
||||
|
final newPath = '$dir/$newName'; |
||||
|
|
||||
|
try { |
||||
|
await file.rename(newPath); |
||||
|
controller.loadTranslatAudio(); // 刷新 |
||||
|
Get.back(); |
||||
|
} catch (e) { |
||||
|
print('重命名失败: $e'); |
||||
|
} |
||||
|
}, |
||||
|
child: Container( |
||||
|
width: 120.w, |
||||
|
height: 40.w, |
||||
|
alignment: Alignment.center, |
||||
|
decoration: BoxDecoration( |
||||
|
color: isDarkMode ? AppColors.primary : Colors.blue, |
||||
|
borderRadius: BorderRadius.circular(12.w), |
||||
|
), |
||||
|
child: Text( |
||||
|
'确认', |
||||
|
style: TextStyle( |
||||
|
color: Colors.white, |
||||
|
fontSize: 14.sp, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
isScrollControlled: true, |
||||
|
clipBehavior: Clip.hardEdge, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue