11 changed files with 1269 additions and 347 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,184 @@ |
|||
import 'dart:io'; |
|||
import 'package:audio_waveforms/audio_waveforms.dart'; |
|||
import 'package:flutter_easyloading/flutter_easyloading.dart'; |
|||
import 'package:get/get.dart'; |
|||
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/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'); |
|||
final RxList<FileSystemEntity> files = <FileSystemEntity>[].obs; |
|||
|
|||
RxList dataList = [].obs; |
|||
|
|||
List dataListTask = []; |
|||
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 |
|||
void onInit() { |
|||
super.onInit(); |
|||
// listController.addListener(_onScroll); |
|||
_requestStoragePermission(); |
|||
//Permission.storage.request(); |
|||
// _headers = { |
|||
// 'Authorization': 'Bearer; $_token', |
|||
// 'Content-Type': 'application/json', |
|||
// }; |
|||
// _getDataList(); |
|||
// addMeetingAudio(); |
|||
loadTranslatAudio(); |
|||
// 监听播放状态变化 |
|||
audioPlayer.playerStateStream.listen((playerState) { |
|||
isPlaying.value = playerState.playing; |
|||
if (playerState.processingState == ProcessingState.completed) { |
|||
// 播放完成后重置状态 |
|||
currentPlayingPath.value = ''; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
@override |
|||
void onClose() { |
|||
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!; |
|||
|
|||
PermissionUtil.instance |
|||
.requestPermission( |
|||
// context: context, |
|||
permissionType: Permission.storage, |
|||
permissionName: "存储", |
|||
explanationText: "我们需要访问存储权限,以便您可以选择并上传音频文件。", |
|||
permanentDenialText: "检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。", |
|||
) |
|||
.then((granted) { |
|||
if (granted) { |
|||
Logger.d("Permission", "用户授予了存储权限"); |
|||
} else { |
|||
Logger.d("Permission", "用户未授予存储权限"); |
|||
// 可以提示用户无法进行文件操作 |
|||
} |
|||
}); |
|||
} |
|||
|
|||
// 添加方法:获取音频时长 |
|||
Future<Duration> getAudioDuration(String filePath) async { |
|||
if (audioDurations.containsKey(filePath)) { |
|||
return audioDurations[filePath]!; |
|||
} |
|||
|
|||
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 { |
|||
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.value = []; // No files exist in new directory |
|||
return; |
|||
} |
|||
|
|||
// Directory exis |
|||
files.value = 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.value = []; // 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()}'); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,473 @@ |
|||
import 'dart:io'; |
|||
|
|||
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( |
|||
"翻译录音文件", // |
|||
style: TextStyle( |
|||
fontSize: 17.sp, |
|||
fontWeight: FontWeight.w600, |
|||
color: isDarkMode ? Colors.white : Colors.black, |
|||
), |
|||
), |
|||
centerTitle: true, |
|||
); |
|||
} |
|||
|
|||
Widget _buildAudioList(BuildContext context, bool isDarkMode) { |
|||
return Obx(() { |
|||
final fileList = controller.files; // 不要 toList() |
|||
if (fileList.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; |
|||
// 添加 FutureBuilder 来异步获取音频时长 |
|||
return FutureBuilder<Duration>( |
|||
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), |
|||
), |
|||
], |
|||
), |
|||
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, |
|||
), |
|||
), |
|||
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, |
|||
), |
|||
), |
|||
], |
|||
); |
|||
}), |
|||
], |
|||
), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
}, |
|||
); |
|||
} |
|||
|
|||
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( |
|||
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, |
|||
}); |
|||
}, |
|||
), |
|||
], |
|||
); |
|||
} |
|||
|
|||
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