Browse Source

Merge branch 'new_dev' of https://github.com/deepcloud2048/deep_voice into new_dev

newdev_shunjiawei
tanlongsheng 1 year ago
parent
commit
4d3f4e3742
  1. 98
      lib/modules/meeting/controllers/meeting_controller.dart
  2. 521
      lib/modules/meeting/controllers/meeting_record_controller.dart
  3. 167
      lib/modules/meeting/views/meeting_record_view.dart
  4. 47
      lib/modules/pairing/controllers/pairing_controller.dart
  5. 10
      lib/modules/translation/bindings/translation_list_binding.dart
  6. 56
      lib/modules/translation/controllers/translation_controller.dart
  7. 184
      lib/modules/translation/controllers/translation_list_controller.dart
  8. 473
      lib/modules/translation/views/translation_list_view.dart
  9. 49
      lib/modules/translation/views/translation_view.dart
  10. 9
      lib/routes/app_pages.dart
  11. 2
      lib/routes/app_routes.dart

98
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';
@ -49,7 +53,7 @@ class MeetingController extends GetxController {
void onInit() {
super.onInit();
listController.addListener(_onScroll);
//_requestStoragePermission();
_requestStoragePermission();
//Permission.storage.request();
_headers = {
'Authorization': 'Bearer; $_token',
@ -57,6 +61,15 @@ class MeetingController extends GetxController {
};
_getDataList();
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
@ -93,7 +106,7 @@ class MeetingController extends GetxController {
isLoading = false;
}
// 请求录音权限
// 请求存储权限
void _requestStoragePermission() async {
// final BuildContext context = Get.overlayContext!;
@ -208,8 +221,10 @@ class MeetingController extends GetxController {
final filePath = file.path;
// 设置音频源获取时长
final _audioPlayer = AudioPlayer();
await _audioPlayer.setAudioSource(AudioSource.file(filePath));
print(
"_audioPlayer$_audioPlayer,filePath=$filePath,_audioPlayer.duration?.inSeconds${_audioPlayer.duration?.inSeconds}");
int seconds = _audioPlayer.duration?.inSeconds ?? 0;
// 获取文件修改时间
@ -249,12 +264,11 @@ class MeetingController extends GetxController {
// 移动文件
await file.copy(newPath);
await file.delete();
addQuantity++;
}
} catch (e) {
print("文件处理失败: ${fileEntity.path}, 错误: $e");
} finally {
await _audioPlayer.dispose(); // 释放播放器资源
}
}
if (addQuantity > 0) EasyLoading.showSuccess('成功导入本地 $addQuantity 条录音');
@ -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<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++;
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<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();
}

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

@ -2,281 +2,258 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:deep_voice/core/utils/logger.dart';
import 'package:deep_voice/core/utils/permission_util.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../../data/services/asr_service.dart';
import '../../../data/services/ble_manager.dart';
// 新增计时器状态枚举
/// Enumerates the possible states of the recording timer
enum TimerStatus {
stopped, // 已停止/未开始
running, // 运行中
paused, // 已暂停
stopped, // Timer is inactive
running, // Timer is actively counting
paused, // Timer is paused
}
/// 控制器:处理录音状态、波形、文本转录等逻辑
/// Controller for meeting recording screen
/// Handles recording state, audio processing, transcription, and UI interactions
class MeetingRecordController extends GetxController
with SingleGetTickerProviderMixin {
//语言服务
// Services
final AsrService _asrService = Get.find<AsrService>();
//语言服务监听
StreamSubscription? _recognitionSubscription;
//是否开启识别
final RxBool isVoiceRecognitionActive = false.obs;
// 音频输入源
bool _audioSourceType = false;
final BleManager _bleManager = Get.find<BleManager>();
//识别文本
RxString intermediateContent = ''.obs;
RxString finalContent = ''.obs;
// final RxBool isRecording = false.obs; // 是否正在录音
// 蓝牙服务
final _bleManager = Get.find<BleManager>();
final RxInt seconds = 0.obs; // 录音秒数
final RxInt audioTypes = 0.obs; // 音频类型
final RxBool isMarked = false.obs; // 是否标记重点
final RxList<double> waveData = <double>[].obs; // 波形图数据
RxBool showTextContent = false.obs; // 控制文本显示
late TabController tabController;
Timer? _timer;
int _currentPosition = 0; // 当前播放位置(用于波形图动画)
// Stream subscriptions
StreamSubscription? _recognitionSubscription;
// 新增计时器状态变量
// Recording state
final Rx<TimerStatus> timerStatus = TimerStatus.stopped.obs;
int _elapsedMilliseconds = 0; // 累计毫秒数
int? _lastStartTime; // 上次开始计时的时间戳
final RxInt seconds = 0.obs;
final RxInt audioType = 0.obs; // 0=Live, 1=Media, 2=Call
final RxBool isMarked = false.obs;
final RxBool showTextContent = false.obs;
final RxBool isVoiceRecognitionActive = false.obs;
final RxList<double> waveData = <double>[].obs;
// Transcription content
final RxString intermediateContent = ''.obs;
final RxString finalContent = ''.obs;
final newName = "".obs;
final fileName = "初始文件名".obs;
final isEditingTitle = false.obs;
// File management
final RxString fileName = "初始文件名".obs;
final RxString newName = "".obs;
final RxBool isEditingTitle = false.obs;
late TextEditingController titleEditingController;
late FocusNode titleFocusNode;
final appDocDir = getApplicationDocumentsDirectory();
final Future<Directory> appDocDir = getApplicationDocumentsDirectory();
// Timing variables
Timer? _timer;
int _elapsedMilliseconds = 0;
int? _lastStartTime;
int _currentPosition = 0;
bool _audioSourceType = false;
late TabController tabController;
@override
void onInit() {
super.onInit();
tabController = TabController(length: 2, vsync: this); // Add this line
_initializeComponents();
_setupFromArguments();
generateWaveData();
}
@override
void onClose() {
_cleanupResources();
super.onClose();
}
// 请求录音权限
Future<bool> _requestRecordPermission() async {
try {
final bool granted = await PermissionUtil.instance.requestPermission(
permissionType: Permission.microphone,
permissionName: "录音",
explanationText: "我们需要访问麦克风权限,以便您录制音频内容。",
permanentDenialText: "检测到您已永久拒绝录音权限。如需继续使用此功能,请前往应用设置中手动开启麦克风权限。",
);
if (granted) {
Logger.d("Permission", "用户授予了录音权限");
return true;
} else {
Logger.d("Permission", "用户未授予录音权限");
return false;
}
} catch (e) {
Logger.e("Permission", "请求录音权限时出错: $e");
return false;
}
}
/// Initializes UI controllers and focus nodes
void _initializeComponents() {
tabController = TabController(length: 2, vsync: this);
titleEditingController = TextEditingController(text: fileName.value);
titleFocusNode = FocusNode();
titleFocusNode.addListener(_handleTitleFocusChange);
}
// 监听焦点变化
titleFocusNode.addListener(() {
if (!titleFocusNode.hasFocus && isEditingTitle.value) {
saveTitleChange();
}
});
/// Sets up initial state from navigation arguments
void _setupFromArguments() {
final Map<String, dynamic> args = Get.arguments ?? {};
audioTypes.value = args['audioTypes'];
// 蓝牙处理
if (audioTypes.value == 0) {
fileName.value = "现场录音";
} else if (audioTypes.value == 1) {
fileName.value = "音、视频录音";
} else if (audioTypes.value == 2) {
fileName.value = "通话录音";
audioType.value = args['audioTypes'] ?? 0;
// Set default filename based on audio type
switch (audioType.value) {
case 0:
fileName.value = "现场录音";
break;
case 1:
fileName.value = "音、视频录音";
break;
case 2:
fileName.value = "通话录音";
break;
}
newName.value = fileName.value;
generateWaveData();
}
@override
void onClose() {
/// Cleans up resources when controller is disposed
void _cleanupResources() {
_timer?.cancel();
_recognitionSubscription?.cancel();
titleEditingController.dispose();
titleFocusNode.dispose();
super.onClose();
_bleManager.closeCodec();
_asrService.stopContinuousRecognition();
_asrService.stopRecord(true);
}
/// Handles focus changes in title text field
void _handleTitleFocusChange() {
if (!titleFocusNode.hasFocus && isEditingTitle.value) {
saveTitleChange();
}
}
/// Starts title editing mode
void startTitleEditing() {
isEditingTitle.value = true;
titleEditingController.text = fileName.value;
newName.value = fileName.value;
// 延迟请求焦点确保TextField已渲染
Future.delayed(Duration(milliseconds: 100), () {
Future.delayed(const Duration(milliseconds: 100), () {
titleFocusNode.requestFocus();
});
}
void saveTitleChange() {
/// Saves new title and exits editing mode
Future<void> saveTitleChange() async {
isEditingTitle.value = true;
if (isEditingTitle.value) {
newName.value = titleEditingController.text;
isEditingTitle.value = false;
// 这里添加实际的文件重命名逻辑
// 例如: renameFile(fileName.value);
// Actual file renaming happens during saveRecording()
}
}
/// 启动/继续计时器
void startTimer() {
if (timerStatus.value != TimerStatus.running) {
timerStatus.value = TimerStatus.running;
_lastStartTime = DateTime.now().millisecondsSinceEpoch;
_timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
if (timerStatus.value == TimerStatus.running) {
// 计算真实经过的时间(毫秒)
final currentTime = DateTime.now().millisecondsSinceEpoch;
_elapsedMilliseconds += currentTime - _lastStartTime!;
_lastStartTime = currentTime;
// 转换为秒(四舍五入)
seconds.value = (_elapsedMilliseconds / 1000).round();
update();
}
});
}
}
/// Toggles visibility of transcribed text content
void toggleTextDisplay() => showTextContent.toggle();
/// 暂停计时器
void pauseTimer() {
if (timerStatus.value == TimerStatus.running) {
timerStatus.value = TimerStatus.paused;
_timer?.cancel();
_timer = null;
// 更新累计时间
final currentTime = DateTime.now().millisecondsSinceEpoch;
_elapsedMilliseconds += currentTime - _lastStartTime!;
_lastStartTime = null;
}
}
/// 重置计时器
void resetTimer() {
_timer?.cancel();
_timer = null;
_elapsedMilliseconds = 0;
_lastStartTime = null;
seconds.value = 0;
timerStatus.value = TimerStatus.stopped;
}
/// Toggles important mark status
void toggleMarked() => isMarked.toggle();
void toggleTextDisplay() {
showTextContent.toggle(); // 点击按钮切换显示状态
/// Formats seconds to MM:SS display format
String formatTime(int seconds) {
return '${(seconds ~/ 60).toString().padLeft(2, '0')}'
':${(seconds % 60).toString().padLeft(2, '0')}';
}
/// 切换录音状态
/// Starts/resumes recording based on current state
Future<void> toggleRecording() async {
if (timerStatus.value == TimerStatus.stopped) {
await _startRecording();
} else if (timerStatus.value == TimerStatus.running) {
await _pauseRecording();
} else if (timerStatus.value == TimerStatus.paused) {
await _resumeRecording();
}
}
// 等待权限请求完成并获取结果
final bool granted = await _requestRecordPermission();
/// 生成模拟波形图数据
void generateWaveData() {
waveData.value = List.generate(120, (i) {
final rand = (i % 20) / 20;
final baseHeight = (sin(i / 5) * 0.5 + 0.5) * 25;
return baseHeight + rand * 10;
});
}
// 如果权限被拒绝,直接返回
if (!granted) {
Logger.d("Permission", "录音权限被拒绝,无法开始录音");
return;
}
// 处理语音识别事件
void _handleRecognitionEvent(RecognitionEvent event) {
switch (event.type) {
case RecognitionEventType.intermediateResult:
print(
"_handleRecognitionEvent,intermediateResult,event.text=${event.text}");
intermediateContent.value = event.text;
// 根据当前计时器状态执行相应操作
switch (timerStatus.value) {
case TimerStatus.stopped:
await _startRecording();
break;
case RecognitionEventType.finalResult:
print("_handleRecognitionEvent,finalResult,event.text=${event.text}");
finalContent.value += _spliceText(event);
intermediateContent.value = '';
//_originaBytes.add(event.audio!);
case TimerStatus.running:
await _pauseRecording();
break;
default:
// 不做任何处理
case TimerStatus.paused:
await _resumeRecording();
break;
}
}
String _spliceText(RecognitionEvent event) {
String eventText = event.text;
String text = '';
if (finalContent.value.isNotEmpty) {
if (event.role != '') {
final punctuations = {
',',
'。',
'!',
'?',
';',
':',
',',
'.',
':',
';',
'!',
'?'
};
final firstChar = eventText[0];
if (punctuations.contains(firstChar)) {
text += '$firstChar\n';
eventText = eventText.substring(1);
} else {
text += '\n';
}
text += '说话人${event.role}:';
}
} else {
text += '说话人${event.role}:';
}
text += eventText;
return text;
}
/// Starts recording session
Future<void> _startRecording() async {
// 开始录音
// Initialize speech recognition
final recognitionStream =
await _asrService.startContinuousRecognition(_audioSourceType);
_recognitionSubscription?.cancel();
_recognitionSubscription =
recognitionStream.listen(_handleRecognitionEvent);
final appDir = await appDocDir; // 先 await 获取目录
// Prepare storage directory
final appDir = await appDocDir;
final dir = Directory("${appDir.path}/MeetingAudio/");
if (!await dir.exists()) {
await dir.create(recursive: true); // Add recursive creation
}
print("appDocDir=${dir?.path}");
// 使用可读性更好的时间格式
if (!await dir.exists()) await dir.create(recursive: true);
// Generate timestamped filename
final formattedTime = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now());
final fullFileName = "${fileName.value}_$formattedTime";
await _asrService
.enableRecord("${dir?.path}/${fileName.value}_$formattedTime.wav");
fileName.value = "${fileName.value}_$formattedTime";
newName.value = fileName.value;
titleEditingController.text = fileName.value;
// Start audio recording
await _asrService.enableRecord("${dir.path}/$fullFileName.wav");
// Update state
fileName.value = fullFileName;
newName.value = fullFileName;
titleEditingController.text = fullFileName;
// Start hardware components
_startHardwareServices();
startTimer();
}
// 蓝牙处理
if (audioTypes.value == 0) {
_bleManager.openEncoder();
} else if (audioTypes.value == 1) {
_bleManager.openDecoder();
} else if (audioTypes.value == 2) {
_bleManager.openA2DPDecoder();
/// Activates appropriate hardware service based on audio type
void _startHardwareServices() {
switch (audioType.value) {
case 0:
_bleManager.openEncoder();
break;
case 1:
_bleManager.openDecoder();
break;
case 2:
_bleManager.openA2DPDecoder();
break;
}
}
/// Pauses current recording session
Future<void> _pauseRecording() async {
// 暂停录音
await _asrService.stopContinuousRecognition();
await _asrService.pauseRecord();
pauseTimer();
//await _asrService.pauseRecognition(); // 假设ASR服务有暂停方法
}
/// Resumes paused recording session
Future<void> _resumeRecording() async {
// 开始录音
final recognitionStream =
await _asrService.startContinuousRecognition(_audioSourceType);
_recognitionSubscription?.cancel();
@ -285,32 +262,158 @@ class MeetingRecordController extends GetxController
startTimer();
}
/// 切换是否标记为重点
void toggleMarked() {
isMarked.toggle();
/// Handles speech recognition events
void _handleRecognitionEvent(RecognitionEvent event) {
switch (event.type) {
case RecognitionEventType.intermediateResult:
intermediateContent.value = event.text;
break;
case RecognitionEventType.finalResult:
finalContent.value += _formatTranscript(event);
intermediateContent.value = '';
break;
default:
break;
}
}
/// 格式化时间为 mm:ss
String formatTime(int seconds) {
// 实现你的时间格式化逻辑
return '${(seconds ~/ 60).toString().padLeft(2, '0')}:${(seconds % 60).toString().padLeft(2, '0')}';
/// Formats transcript with speaker identification
String _formatTranscript(RecognitionEvent event) {
final buffer = StringBuffer();
if (finalContent.value.isNotEmpty) {
if (event.role.isNotEmpty) {
// Add line break before new speaker
if (_isPunctuation(event.text[0])) {
buffer.write('${event.text[0]}\n');
buffer.write('说话人${event.role}:');
buffer.write(event.text.substring(1));
} else {
buffer.write('\n说话人${event.role}:');
buffer.write(event.text);
}
} else {
buffer.write(event.text);
}
} else {
buffer.write('说话人${event.role}:${event.text}');
}
return buffer.toString();
}
/// 游标在波形图中的位置(0~1)
double get cursorPosition => _currentPosition / waveData.length;
/// Checks if character is Chinese/English punctuation
bool _isPunctuation(String char) {
const punctuations = {
',',
'。',
'!',
'?',
';',
':',
',',
'.',
':',
';',
'!',
'?'
};
return punctuations.contains(char);
}
/// 保存录音(目前模拟为返回上一页)
/// Saves recording and cleans up resources
Future<void> saveRecording() async {
resetTimer(); // 重置计时器
resetTimer();
_bleManager.closeCodec();
await _asrService.stopRecord(true);
final appDir = await appDocDir; // 先 await 获取目录
final dir = Directory("${appDir.path}/MeetingAudio/");
if (!await dir.exists()) {
await dir.create(recursive: true); // Add recursive creation
// Rename file if title was edited
if (fileName.value != newName.value) {
final appDir = await appDocDir;
final dir = Directory("${appDir.path}/MeetingAudio/");
if (!await dir.exists()) await dir.create(recursive: true);
await _asrService.renameFile(
"${dir.path}/${fileName.value}.wav",
newName.value,
);
}
await _asrService.renameFile(
"${dir.path}/${fileName.value}.wav", newName.value);
await _asrService.stopContinuousRecognition();
_resetToDefaultState();
}
/// Resets controller to initial state
void _resetToDefaultState() {
switch (audioType.value) {
case 0:
fileName.value = "现场录音";
break;
case 1:
fileName.value = "音、视频录音";
break;
case 2:
fileName.value = "通话录音";
break;
}
newName.value = fileName.value;
finalContent.value = '';
intermediateContent.value = '';
isMarked.value = false;
}
/// TIMER CONTROL SECTION ///
/// Starts or resumes timer
void startTimer() {
if (timerStatus.value != TimerStatus.running) {
timerStatus.value = TimerStatus.running;
_lastStartTime = DateTime.now().millisecondsSinceEpoch;
_timer = Timer.periodic(const Duration(milliseconds: 200), (_) {
if (timerStatus.value != TimerStatus.running) return;
final currentTime = DateTime.now().millisecondsSinceEpoch;
_elapsedMilliseconds += currentTime - _lastStartTime!;
_lastStartTime = currentTime;
seconds.value = (_elapsedMilliseconds / 1000).round();
});
}
}
/// Pauses active timer
void pauseTimer() {
if (timerStatus.value == TimerStatus.running) {
timerStatus.value = TimerStatus.paused;
_timer?.cancel();
_timer = null;
final currentTime = DateTime.now().millisecondsSinceEpoch;
_elapsedMilliseconds += currentTime - _lastStartTime!;
_lastStartTime = null;
}
}
/// Resets timer to initial state
void resetTimer() {
_timer?.cancel();
_timer = null;
_elapsedMilliseconds = 0;
_lastStartTime = null;
seconds.value = 0;
timerStatus.value = TimerStatus.stopped;
}
/// UI HELPERS SECTION ///
/// Generates mock waveform data for visualization
void generateWaveData() {
waveData.value = List.generate(120, (i) {
final rand = (i % 20) / 20;
final baseHeight = (sin(i / 5) * 0.5 + 0.5) * 25;
return baseHeight + rand * 10;
});
}
/// Current playback position in waveform (0-1)
double get cursorPosition => _currentPosition / waveData.length;
}

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

@ -12,86 +12,101 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
// 在控制器中添加编辑状态管理
return Scaffold(
appBar: AppBar(
leading: const BackButton(),
title: Obx(() {
if (controller.isEditingTitle.value) {
return SizedBox(
height: 40,
child: TextField(
controller: controller.titleEditingController,
focusNode: controller.titleFocusNode,
autofocus: true,
style: TextStyle(
fontSize: 18,
color: isDarkMode ? Colors.white : Colors.black,
),
decoration: InputDecoration(
isDense: true,
contentPadding: EdgeInsets.zero,
border: InputBorder.none,
hintText: '输入文件名',
hintStyle: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
return WillPopScope(
onWillPop: () async {
// 在返回时执行保存操作
controller.saveTitleChange();
await controller.saveRecording();
return true; // 允许退出
},
child: Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () async {
// 在返回按钮点击时执行保存操作
controller.saveTitleChange();
await controller.saveRecording();
Get.back();
},
),
title: Obx(() {
if (controller.isEditingTitle.value) {
return SizedBox(
height: 40,
child: TextField(
controller: controller.titleEditingController,
focusNode: controller.titleFocusNode,
autofocus: true,
style: TextStyle(
fontSize: 18,
color: isDarkMode ? Colors.white : Colors.black,
),
decoration: InputDecoration(
isDense: true,
contentPadding: EdgeInsets.zero,
border: InputBorder.none,
hintText: '输入文件名',
hintStyle: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
textInputAction: TextInputAction.done,
onSubmitted: (value) {
controller.saveTitleChange();
},
),
textInputAction: TextInputAction.done,
onSubmitted: (value) {
controller.saveTitleChange();
);
} else {
return GestureDetector(
onTap: () {
controller.startTitleEditing();
},
),
);
} else {
return GestureDetector(
onTap: () {
controller.startTitleEditing();
},
child: Text(
controller.newName.value,
style: const TextStyle(fontSize: 18),
),
);
}
}),
elevation: 0,
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white,
foregroundColor: isDarkMode ? Colors.white : Colors.black,
actions: [
Obx(() {
if (controller.isEditingTitle.value) {
return IconButton(
icon: const Icon(Icons.check),
onPressed: controller.saveTitleChange,
child: Text(
controller.newName.value,
style: const TextStyle(fontSize: 18),
),
);
}
return const SizedBox.shrink();
}),
],
),
body: Column(
children: [
TabBar(
controller: controller.tabController,
indicatorColor: Colors.red,
labelColor: isDarkMode ? Colors.white : Colors.black,
unselectedLabelColor: Colors.grey,
tabs: const [
Tab(text: '音频'),
Tab(text: '文本'),
],
),
Expanded(
child: TabBarView(
elevation: 0,
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white,
foregroundColor: isDarkMode ? Colors.white : Colors.black,
actions: [
Obx(() {
if (controller.isEditingTitle.value) {
return IconButton(
icon: const Icon(Icons.check),
onPressed: controller.saveTitleChange,
);
}
return const SizedBox.shrink();
}),
],
),
body: Column(
children: [
TabBar(
controller: controller.tabController,
children: [
_buildAudioTab(context, isDarkMode),
_buildTextTab(context, isDarkMode),
indicatorColor: Colors.red,
labelColor: isDarkMode ? Colors.white : Colors.black,
unselectedLabelColor: Colors.grey,
tabs: const [
Tab(text: '音频'),
Tab(text: '文本'),
],
),
),
],
Expanded(
child: TabBarView(
controller: controller.tabController,
children: [
_buildAudioTab(context, isDarkMode),
_buildTextTab(context, isDarkMode),
],
),
),
],
),
),
);
}
@ -302,7 +317,6 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
),
),
// 操作按钮
Padding(
padding: EdgeInsets.only(top: 30.w),
child: Row(
@ -310,9 +324,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
// 取消按钮
Expanded(
child: TextButton(
onPressed: () {
Get.back;
},
onPressed: Get.back, //
style: TextButton.styleFrom(
foregroundColor:
isDarkMode ? AppColors.primary : Colors.blue,
@ -336,11 +348,8 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
Expanded(
child: TextButton(
onPressed: () async {
// 先执行保存标题修改
controller.saveTitleChange();
// 然后执行保存录音
await controller.saveTitleChange();
await controller.saveRecording();
// 最后关闭弹窗
Get.back();
},
style: TextButton.styleFrom(

47
lib/modules/pairing/controllers/pairing_controller.dart

@ -137,7 +137,7 @@ class PairingController extends GetxController {
void skipPairing() async {
Logger.i(_tag, '用户选择跳过配对');
// 权限申请
await _checkAndRequestPermissions();
await _checkAndRequestAgentPermissions();
// 启动BLE后台服务
await BleManager.to.startAgentService();
Logger.i(_tag, 'BLE后台服务已启动');
@ -502,6 +502,8 @@ class PairingController extends GetxController {
statusDescription.value = '配对成功!';
try {
//检测agent权限是否已授予
_checkAndRequestAgentPermissions();
// 启动BLE后台服务
await BleManager.to.startAgentService();
Logger.i(_tag, 'BLE后台服务已启动');
@ -577,6 +579,49 @@ class PairingController extends GetxController {
if (Platform.isAndroid && await _getAndroidSdkVersion() >= 31) {
requiredPermissions.add(Permission.bluetoothScan);
requiredPermissions.add(Permission.bluetoothConnect);
}
// 逐个请求权限
for (var permission in requiredPermissions) {
final permissionName = _getPermissionName(permission);
Logger.d(_tag, '正在请求 $permissionName 权限');
final granted = await PermissionUtil.instance.requestPermission(
permissionType: permission,
permissionName: permissionName,
explanationText: _getExplanationText(permission),
permanentDenialText: '您已永久拒绝$permissionName权限,请前往应用设置开启',
settingsButtonText: '去设置',
cancelButtonText: '取消',
);
if (!granted) {
Logger.w(_tag, '$permissionName 权限未授予');
return false;
}
}
return true;
} catch (e) {
Logger.e(_tag, '权限检查失败: $e');
return false;
}
}
Future<bool> _checkAndRequestAgentPermissions() async {
try {
if (Platform.isIOS) {
// iOS权限由系统自动处理
return true;
}
// Android权限检查 - 使用 PermissionUtil
// 根据 Android 版本确定需要的权限
List<Permission> requiredPermissions = [
Permission.locationWhenInUse,
];
if (Platform.isAndroid && await _getAndroidSdkVersion() >= 31) {
requiredPermissions.add(Permission.locationAlways);
requiredPermissions.add(Permission.notification);
}

10
lib/modules/translation/bindings/translation_list_binding.dart

@ -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());
}
}

56
lib/modules/translation/controllers/translation_controller.dart

@ -1,5 +1,6 @@
import 'dart:io';
import 'package:deep_voice/core/utils/permission_util.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'dart:async';
@ -7,6 +8,7 @@ import 'dart:convert';
import 'package:get_storage/get_storage.dart';
import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../../data/services/volcano_translation_service.dart';
import '../../../data/services/tts_service.dart';
import '../../../data/services/language_manager.dart';
@ -102,7 +104,7 @@ class TranslationController extends GetxController {
//_setupEventListeners();
// 加载上次保存的语言设置
_loadSavedLanguages();
_requestStoragePermission();
// 初始化语言代码
final sourceAsrCode =
_languageManager.getAsrCodeByChineseName(sourceLanguage.value);
@ -150,6 +152,49 @@ class TranslationController extends GetxController {
_storage.write(_targetLanguageKey, targetLanguage.value);
}
Future<bool> _requestRecordPermission() async {
try {
final bool granted = await PermissionUtil.instance.requestPermission(
permissionType: Permission.microphone,
permissionName: "录音",
explanationText: "我们需要访问麦克风权限,以便您录制音频内容。",
permanentDenialText: "检测到您已永久拒绝录音权限。如需继续使用此功能,请前往应用设置中手动开启麦克风权限。",
);
if (granted) {
Logger.d("Permission", "用户授予了录音权限");
return true;
} else {
Logger.d("Permission", "用户未授予录音权限");
return false;
}
} catch (e) {
Logger.e("Permission", "请求录音权限时出错: $e");
return false;
}
}
// 请求存储权限
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 _setupEventListeners() {
// // 监听扫描结果
@ -220,6 +265,15 @@ class TranslationController extends GetxController {
// 开始语音识别
Future<void> startRecognition() async {
Logger.info('开始语音识别');
// 等待权限请求完成并获取结果
final bool granted = await _requestRecordPermission();
// 如果权限被拒绝,直接返回
if (!granted) {
Logger.d("Permission", "录音权限被拒绝,无法开始录音");
return;
}
if (isRecognizing.value) return;
try {

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

@ -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()}');
}
}
}

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

@ -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,
);
}
}

49
lib/modules/translation/views/translation_view.dart

@ -5,6 +5,7 @@ import '../controllers/translation_controller.dart';
import '../views/translation_history_view.dart';
import '../bindings/translation_history_binding.dart';
import '../../../core/theme/app_colors.dart';
import '../../../routes/app_routes.dart';
// import 'package:intl/intl.dart';
class TranslationView extends GetView<TranslationController> {
@ -61,25 +62,34 @@ class TranslationView extends GetView<TranslationController> {
_buildPopupMenuItem('audioVideo', 'audioVideoTranslation'.tr),
_buildPopupMenuItem('call', 'callTranslation'.tr),
],
child: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
controller.currentModeTitle.value.tr, // 添加 .value 访问实际值
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
child: Row(
mainAxisSize: MainAxisSize.min, // 关键修改:让宽度自适应内容
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
// 防止文本溢出
child: FittedBox(
// 保持文本缩放比例
fit: BoxFit.scaleDown,
child: Text(
controller.currentModeTitle.value.tr,
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600, // 保持字体加粗
color: isDarkMode ? Colors.white : Colors.black,
),
overflow: TextOverflow.ellipsis, // 长文本显示省略号
maxLines: 1,
),
),
Icon(
Icons.arrow_drop_down,
color: isDarkMode ? Colors.white : Colors.black,
size: 20.sp,
),
],
),
),
SizedBox(width: 4), // 添加间距
Icon(
Icons.arrow_drop_down,
color: isDarkMode ? Colors.white : Colors.black,
size: 20.sp,
),
],
),
)),
centerTitle: true,
@ -106,10 +116,7 @@ class TranslationView extends GetView<TranslationController> {
onPressed: () async {
// 停止语音识别和TTS,释放资源
await controller.stopAll();
await Get.to(
() => const TranslationHistoryView(),
binding: TranslationHistoryBinding(),
);
Get.toNamed(Routes.translationList);
// 返回时不自动开始翻译,只需重新设置滚动监听器
controller.setupScrollListener();

9
lib/routes/app_pages.dart

@ -21,6 +21,8 @@ import '../modules/timbre/views/timbre_view.dart';
import '../modules/translation/views/translation_view.dart';
import '../modules/translation/bindings/translation_binding.dart';
import '../modules/translation/views/translation_history_view.dart';
import '../modules/translation/bindings/translation_list_binding.dart';
import '../modules/translation/views/translation_list_view.dart';
import '../modules/translation/bindings/translation_history_binding.dart';
import '../modules/login/views/login_view.dart';
import '../modules/login/bindings/login_binding.dart';
@ -128,6 +130,13 @@ abstract class AppPages {
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.translationList,
page: () => const TranslationListView(),
binding: TranslationListBinding(),
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.meeting,
page: () => const MeetingView(),

2
lib/routes/app_routes.dart

@ -27,7 +27,7 @@ abstract class Routes {
static const translation = '/translation'; // 实时翻译
static const translationHistory = '/translation/history'; // 翻译历史
static const speechDemo = '/speech_demo'; // 语音演示
static const translationList = '/translation/List'; // 翻译历史
// 测试路由(生产环境建议移除)
static const ttsTest = '/tts_test'; // 文字转语音测试
static const asrTest = '/asr_test'; // 语音识别测试

Loading…
Cancel
Save