diff --git a/lib/modules/meeting/controllers/meeting_controller.dart b/lib/modules/meeting/controllers/meeting_controller.dart index baf2e0d36..f077169cc 100644 --- a/lib/modules/meeting/controllers/meeting_controller.dart +++ b/lib/modules/meeting/controllers/meeting_controller.dart @@ -7,6 +7,7 @@ import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; import 'package:just_audio/just_audio.dart'; +import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -17,6 +18,8 @@ import '../../../core/utils/permission_util.dart'; import 'package:share_plus/share_plus.dart'; +import '../../../routes/app_routes.dart'; + class MeetingController extends GetxController { final ScrollController listController = ScrollController(); final AudioPlayer _audioPlayer = AudioPlayer(); @@ -38,6 +41,7 @@ class MeetingController extends GetxController { List dataListTask = []; bool isDisposed = false; + String filePath = ''; final String _appid = '2316081933'; final String _token = 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En'; @@ -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 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 data = { - 'title': fileName, - 'type': 'IMPORT', - 'seconds': seconds, - 'filepath': filePath, - 'audiourl': audiourl, - 'tasktype': 0, - 'creationtime': creationTime, - }; - final meetingId = await SqfliteApi.insertMeeting(data); - if (meetingId != null) { - dataList.insert(0, { - 'id': meetingId, - 'taskid': '', - ...data, - }); - addQuantity++; + String fileName = ""; + bool isParamProvided = filePath != null; // 更清晰的变量名 + if (filePath != null) { + // 正确截取文件名(跨平台安全方式) + fileName = path.basename(filePath); + } else { + FilePickerResult? result = await FilePicker.platform.pickFiles( + withData: true, + ); + if (result != null) { + PlatformFile file = result.files.first; + filePath = await _saveFile(file); + fileName = file.name; } } + + await _audioPlayer.setAudioSource(AudioSource.file(filePath!)); + var audiourl = + await UploadOss.upload(filepath: filePath, rootDir: 'ExternalAudio'); + int seconds = _audioPlayer.duration?.inSeconds ?? 0; + int creationTime = DateTime.now().millisecondsSinceEpoch; + Map data = { + 'title': fileName, + 'type': 'IMPORT', + 'seconds': seconds, + 'filepath': filePath, + 'audiourl': audiourl, + 'tasktype': 0, + 'creationtime': creationTime, + }; + final meetingId = await SqfliteApi.insertMeeting(data); + if (meetingId != null) { + dataList.insert(0, { + 'id': meetingId, + 'taskid': '', + ...data, + }); + addQuantity++; + } + + if (isParamProvided) { + Get.toNamed(Routes.meetingDetails, arguments: { + 'id': meetingId, + }); + } EasyLoading.dismiss(); } diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart index b1bb68481..2fea38ea5 100644 --- a/lib/modules/meeting/controllers/meeting_record_controller.dart +++ b/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(); - //语言服务监听 - StreamSubscription? _recognitionSubscription; - //是否开启识别 - final RxBool isVoiceRecognitionActive = false.obs; -// 音频输入源 - bool _audioSourceType = false; + final BleManager _bleManager = Get.find(); - //识别文本 - RxString intermediateContent = ''.obs; - RxString finalContent = ''.obs; - // final RxBool isRecording = false.obs; // 是否正在录音 - // 蓝牙服务 - final _bleManager = Get.find(); - final RxInt seconds = 0.obs; // 录音秒数 - final RxInt audioTypes = 0.obs; // 音频类型 - final RxBool isMarked = false.obs; // 是否标记重点 - final RxList waveData = [].obs; // 波形图数据 - RxBool showTextContent = false.obs; // 控制文本显示 - late TabController tabController; - Timer? _timer; - int _currentPosition = 0; // 当前播放位置(用于波形图动画) + // Stream subscriptions + StreamSubscription? _recognitionSubscription; - // 新增计时器状态变量 + // Recording state final Rx 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 waveData = [].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 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 _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 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 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 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 _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 _pauseRecording() async { - // 暂停录音 await _asrService.stopContinuousRecognition(); await _asrService.pauseRecord(); pauseTimer(); - //await _asrService.pauseRecognition(); // 假设ASR服务有暂停方法 } + /// Resumes paused recording session Future _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 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; } diff --git a/lib/modules/meeting/views/meeting_record_view.dart b/lib/modules/meeting/views/meeting_record_view.dart index 990d99f44..10d288604 100644 --- a/lib/modules/meeting/views/meeting_record_view.dart +++ b/lib/modules/meeting/views/meeting_record_view.dart @@ -12,86 +12,101 @@ class MeetingRecordView extends GetView { 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 { ), ), - // 操作按钮 Padding( padding: EdgeInsets.only(top: 30.w), child: Row( @@ -310,9 +324,7 @@ class MeetingRecordView extends GetView { // 取消按钮 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 { Expanded( child: TextButton( onPressed: () async { - // 先执行保存标题修改 - controller.saveTitleChange(); - // 然后执行保存录音 + await controller.saveTitleChange(); await controller.saveRecording(); - // 最后关闭弹窗 Get.back(); }, style: TextButton.styleFrom( diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index e8fd9945e..69fe1dfd3 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/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 _checkAndRequestAgentPermissions() async { + try { + if (Platform.isIOS) { + // iOS权限由系统自动处理 + return true; + } + + // Android权限检查 - 使用 PermissionUtil + // 根据 Android 版本确定需要的权限 + List requiredPermissions = [ + Permission.locationWhenInUse, + ]; + + if (Platform.isAndroid && await _getAndroidSdkVersion() >= 31) { requiredPermissions.add(Permission.locationAlways); requiredPermissions.add(Permission.notification); } diff --git a/lib/modules/translation/bindings/translation_list_binding.dart b/lib/modules/translation/bindings/translation_list_binding.dart new file mode 100644 index 000000000..4dc264ada --- /dev/null +++ b/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()); + } +} diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index b7e42e44c..9a911accf 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/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 _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 startRecognition() async { Logger.info('开始语音识别'); + // 等待权限请求完成并获取结果 + final bool granted = await _requestRecordPermission(); + + // 如果权限被拒绝,直接返回 + if (!granted) { + Logger.d("Permission", "录音权限被拒绝,无法开始录音"); + return; + } + if (isRecognizing.value) return; try { diff --git a/lib/modules/translation/controllers/translation_list_controller.dart b/lib/modules/translation/controllers/translation_list_controller.dart new file mode 100644 index 000000000..7b8d0a98e --- /dev/null +++ b/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 selected = []; + + final appDocDir = getApplicationDocumentsDirectory(); + // final directory = Directory( + // '/storage/emulated/0/Android/data/com.yunqiinnovation.deepsound/files'); + final RxList files = [].obs; + + RxList dataList = [].obs; + + List dataListTask = []; + bool isDisposed = false; + // 添加音频播放器 + // 添加音频时长缓存 + final Map audioDurations = {}; + // 播放状态 + final RxBool isPlaying = false.obs; + // 当前播放的文件路径 + final RxString currentPlayingPath = ''.obs; + final AudioPlayer audioPlayer = AudioPlayer(); + final PlayerController playerController = PlayerController(); + @override + void onInit() { + super.onInit(); + // 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 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 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()}'); + } + } +} diff --git a/lib/modules/translation/views/translation_list_view.dart b/lib/modules/translation/views/translation_list_view.dart new file mode 100644 index 000000000..a24f56c01 --- /dev/null +++ b/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 { + 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( + 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, + ); + } +} diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index 85cb3bbd6..01f3a1ec6 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/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 { @@ -61,25 +62,34 @@ class TranslationView extends GetView { _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 { onPressed: () async { // 停止语音识别和TTS,释放资源 await controller.stopAll(); - await Get.to( - () => const TranslationHistoryView(), - binding: TranslationHistoryBinding(), - ); + Get.toNamed(Routes.translationList); // 返回时不自动开始翻译,只需重新设置滚动监听器 controller.setupScrollListener(); diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index 2bb173efe..fd468e296 100644 --- a/lib/routes/app_pages.dart +++ b/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(), diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 8ee5515dd..066c8af22 100644 --- a/lib/routes/app_routes.dart +++ b/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'; // 语音识别测试