From c3218c6ae8c8d51a9c7f1e3bf75b77816d5c0f07 Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Tue, 17 Jun 2025 18:58:05 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=8D=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E6=9D=83=E9=99=90bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/meeting_controller.dart | 10 +- .../meeting_record_controller.dart | 518 +++++++++++------- .../meeting/views/meeting_record_view.dart | 165 +++--- .../bindings/translation_list_binding.dart | 10 + .../controllers/translation_controller.dart | 56 +- .../translation_list_controller.dart | 191 +++++++ .../views/translation_list_view.dart | 407 ++++++++++++++ .../translation/views/translation_view.dart | 49 +- lib/routes/app_pages.dart | 9 + lib/routes/app_routes.dart | 2 +- 10 files changed, 1103 insertions(+), 314 deletions(-) create mode 100644 lib/modules/translation/bindings/translation_list_binding.dart create mode 100644 lib/modules/translation/controllers/translation_list_controller.dart create mode 100644 lib/modules/translation/views/translation_list_view.dart diff --git a/lib/modules/meeting/controllers/meeting_controller.dart b/lib/modules/meeting/controllers/meeting_controller.dart index baf2e0d36..4277e1cea 100644 --- a/lib/modules/meeting/controllers/meeting_controller.dart +++ b/lib/modules/meeting/controllers/meeting_controller.dart @@ -49,7 +49,7 @@ class MeetingController extends GetxController { void onInit() { super.onInit(); listController.addListener(_onScroll); - //_requestStoragePermission(); + _requestStoragePermission(); //Permission.storage.request(); _headers = { 'Authorization': 'Bearer; $_token', @@ -93,7 +93,7 @@ class MeetingController extends GetxController { isLoading = false; } - // 请求录音权限 + // 请求存储权限 void _requestStoragePermission() async { // final BuildContext context = Get.overlayContext!; @@ -208,8 +208,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; // 获取文件修改时间 @@ -253,8 +255,6 @@ class MeetingController extends GetxController { } } catch (e) { print("文件处理失败: ${fileEntity.path}, 错误: $e"); - } finally { - await _audioPlayer.dispose(); // 释放播放器资源 } } if (addQuantity > 0) EasyLoading.showSuccess('成功导入本地 $addQuantity 条录音'); diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart index b1bb68481..aaae77c96 100644 --- a/lib/modules/meeting/controllers/meeting_record_controller.dart +++ b/lib/modules/meeting/controllers/meeting_record_controller.dart @@ -2,281 +2,257 @@ 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(); }); } + /// Saves new title and exits editing mode void saveTitleChange() { 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 +261,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..ec10ee77b 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.saveRecording(); - // 最后关闭弹窗 Get.back(); }, style: TextButton.styleFrom( 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..aa8f1a288 --- /dev/null +++ b/lib/modules/translation/controllers/translation_list_controller.dart @@ -0,0 +1,191 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; +import 'package:just_audio/just_audio.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import '../../../core/utils/logger.dart'; +import '../../../core/utils/upload_oss.dart'; +import '../../../data/services/db/sqflite_api.dart'; +import '../../../core/utils/permission_util.dart'; + +import 'package:share_plus/share_plus.dart'; + +class TranslationListController extends GetxController { + // 是否还有更多数据 + bool isGetMore = true; + bool isLoading = false; + int page = 1; + int addQuantity = 0; + List selected = []; + + final appDocDir = getApplicationDocumentsDirectory(); + // final directory = Directory( + // '/storage/emulated/0/Android/data/com.yunqiinnovation.deepsound/files'); + List files = []; + TextEditingController titleController = TextEditingController(); // 录音标题 + + RxList dataList = [].obs; + + List dataListTask = []; + bool isDisposed = false; + + @override + void onInit() { + super.onInit(); + // listController.addListener(_onScroll); + _requestStoragePermission(); + //Permission.storage.request(); + // _headers = { + // 'Authorization': 'Bearer; $_token', + // 'Content-Type': 'application/json', + // }; + // _getDataList(); + // addMeetingAudio(); + loadTranslatAudio(); + } + + @override + void onClose() { + titleController.dispose(); + + isDisposed = true; + super.onClose(); + } + + // 请求存储权限 + void _requestStoragePermission() async { + // final BuildContext context = Get.overlayContext!; + + PermissionUtil.instance + .requestPermission( + // context: context, + permissionType: Permission.storage, + permissionName: "存储", + explanationText: "我们需要访问存储权限,以便您可以选择并上传音频文件。", + permanentDenialText: "检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。", + ) + .then((granted) { + if (granted) { + Logger.d("Permission", "用户授予了存储权限"); + } else { + Logger.d("Permission", "用户未授予存储权限"); + // 可以提示用户无法进行文件操作 + } + }); + } + + void loadTranslatopus() async { + final appDir = await appDocDir; // 先 await 获取目录 + final dir = Directory("${appDir.path}/OpusAudio"); + + print("appDocDir=${dir?.path}"); + + files = dir + .listSync() + .where((f) => f is File && (f.path.endsWith('.opus'))) + .toList(); + } + + void loadTranslatAudio() async { + try { + final appDir = await appDocDir; // 先 await 获取目录 + final dir = Directory("${appDir.path}/TranslatAudio"); + + print("appDocDir=${dir?.path}"); + // Create directory if it doesn't exist + if (!await dir.exists()) { + await dir.create(recursive: true); // Create directory tree if needed + files = []; // No files exist in new directory + return; + } + + // Directory exis + files = dir + .listSync() + .where((f) => + f is File && + (f.path.endsWith('.mp3') || + f.path.endsWith('.wav') || + f.path.endsWith('.aac') || + f.path.endsWith('.m4a') || + f.path.endsWith('.mp4') || + f.path.endsWith('.mov'))) + .toList(); + } catch (e) { + print("Error loading files: $e"); + files = []; // Return empty list on error + } + } +// 导入音频文件 + + Future shareAllTranslatAudio() async { + final filePath = selected[0]; + // for (final filePath in selected) { + // if (filePath.isEmpty) { + // EasyLoading.showToast('没有可分享的音频文件'); + // return; + // } + // } + try { + await Share.shareXFiles( + [XFile(filePath)], + text: '分享会议音频', + ); + + EasyLoading.showToast('分享完成!'); + } catch (e) { + EasyLoading.showToast('分享失败: ${e.toString()}'); + } + } + + Future _saveFile(PlatformFile file) async { + final directory = await getApplicationDocumentsDirectory(); + final directoryDir = Directory('${directory.path}/ExternalAudio'); + if (!await directoryDir.exists()) { + await directoryDir.create(recursive: true); + } + final String filePath = '${directory.path}/ExternalAudio/${file.name}'; + final File localFile = File(filePath); + await localFile.writeAsBytes(file.bytes!); + return filePath; + } + + // 修改标题 + void editMeetingTitle(int id, String title) async { + if (title.isNotEmpty) { + SqfliteApi.editMeetingTitle(id, {'title': title}); + int index = dataList.indexWhere((item) => item['id'] == id); + if (index != -1) { + final dataMap = Map.from(dataList[index]); + dataMap['title'] = title; + dataList[index] = dataMap; + } + } + Get.back(); + } + + // 删除数据 + void deleteMeeting(int id, String filepath) async { + dataList.removeWhere((item) => item['id'] == id); + // 删除数据库中的记录 + SqfliteApi.deleteMeeting(id); + _deleteFile(filepath); + addQuantity--; + } + + // 删除文件 + Future _deleteFile(String filepath) async { + File file = File(filepath); + bool fileExists = await file.exists(); + if (fileExists) { + await file.delete(); + } + } +} 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..2941eda68 --- /dev/null +++ b/lib/modules/translation/views/translation_list_view.dart @@ -0,0 +1,407 @@ +import 'dart:io'; + +import 'package:common_utils/common_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:haptic_feedback/haptic_feedback.dart'; +import 'package:intl/intl.dart'; + +import '../../../core/theme/app_colors.dart'; +import '../../../routes/app_routes.dart'; +import '../controllers/translation_list_controller.dart'; + +class TranslationListView extends GetView { + const TranslationListView({super.key}); + static OverlayEntry? _recordingOverlayEntry; + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], + appBar: _buildAppBar(context, isDarkMode), + body: _buildAudioList(context, isDarkMode), + ); + } + + // 构建应用栏 + PreferredSizeWidget _buildAppBar(BuildContext context, bool isDarkMode) { + return AppBar( + backgroundColor: isDarkMode ? Colors.black : Colors.white, + elevation: 0.5, + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios, + color: isDarkMode ? Colors.white : Colors.black, + size: 20.sp, + ), + onPressed: () => Get.back(), + ), + title: Text( + 'conferenceAssistant'.tr, // 商务会议助手 + style: TextStyle( + fontSize: 17.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + centerTitle: true, + ); + } + + Widget _buildAudioList(BuildContext context, bool isDarkMode) { + return Obx(() { + if (controller.files.isEmpty) { + return Center(child: Text('暂无音频文件')); + } + + return ListView.builder( + itemCount: controller.files.length, + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w), + itemBuilder: (context, index) { + final file = controller.files[index]; + return _audioItem(context, isDarkMode, file); + }, + ); + }); + } + + Widget _audioItem( + BuildContext context, bool isDarkMode, FileSystemEntity file) { + final fileName = file.path.split('/').last; + final fileStat = file.statSync(); // 获取文件信息 + final modifiedTime = fileStat.modified; + + return GestureDetector( + onTap: () { + // TODO: 播放音频 + print("播放音频: $fileName"); + }, + onLongPressStart: (details) { + _onLongPressAudioItem(context, isDarkMode, details, file); + print("长按操作: $fileName"); + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.w), + margin: EdgeInsets.symmetric(vertical: 5.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, + borderRadius: BorderRadius.circular(12.w), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + /// 文件名 + Text( + fileName, + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 18.sp, + ), + overflow: TextOverflow.ellipsis, + ), + + /// 修改时间 & 大小 + Padding( + padding: EdgeInsets.symmetric(vertical: 5.w), + child: Row( + children: [ + Icon( + Icons.access_time, + size: 14.w, + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + ), + 4.horizontalSpace, + Text( + DateFormat('yyyy-MM-dd HH:mm').format(modifiedTime), + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + fontSize: 12.sp, + ), + ), + 20.horizontalSpace, + Icon( + Icons.sd_storage, + size: 14.w, + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + ), + 4.horizontalSpace, + Text( + '${(fileStat.size / 1024).toStringAsFixed(1)} KB', + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + fontSize: 12.sp, + ), + ), + ], + ), + ), + + /// 文件类型标签 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 2.w), + margin: EdgeInsets.only(top: 5.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.grey[100], + borderRadius: BorderRadius.circular(4.w), + ), + child: Text( + file.path.split('.').last.toUpperCase(), + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + fontSize: 10.sp, + ), + ), + ), + Padding( + padding: EdgeInsets.only(top: 5.w), + child: Text( + '点击播放', + style: TextStyle( + color: Colors.blue, + fontSize: 10.sp, + ), + ), + ), + ], + ), + ], + ), + ), + ); + } + + void _onLongPressAudioItem(BuildContext context, bool isDarkMode, + LongPressStartDetails details, FileSystemEntity file) { + Haptics.vibrate(HapticsType.success); + + RenderBox? renderBox = + Overlay.of(context).context.findRenderObject() as RenderBox; + + final RelativeRect position = RelativeRect.fromRect( + Rect.fromLTRB( + details.globalPosition.dx, + details.globalPosition.dy, + details.globalPosition.dx + 110, + details.globalPosition.dy - 40, + ), + Offset.zero & renderBox.size, + ); + + final fileName = file.path.split('/').last; + + showMenu( + context: context, + position: position, + color: isDarkMode ? AppColors.cardBackground : Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + items: [ + _popupMenuEntry( + Icon( + Icons.drive_file_rename_outline, + size: 20.w, + color: isDarkMode ? Colors.white : Colors.black87, + ), + '重命名', + isDarkMode ? Colors.white : Colors.black87, + () { + // 弹出重命名输入框 + _renameAudioBottomSheet(context, isDarkMode, file); + }, + ), + _popupMenuEntry( + Icon( + Icons.delete_outline, + size: 20.w, + color: isDarkMode ? Colors.red[700] : Colors.red, + ), + '删除', + isDarkMode ? Colors.red[700]! : Colors.red, + () { + file.delete(); + controller.loadTranslatAudio(); // 刷新列表 + }, + ), + ], + ); + } + + PopupMenuEntry _popupMenuEntry( + Icon icon, + String title, + Color color, + Function() onTap, + ) { + return PopupMenuItem( + onTap: onTap, + child: SizedBox( + width: 140.w, + child: Row( + children: [ + icon, + 8.horizontalSpace, + Text( + title, + style: TextStyle( + color: color, + fontSize: 14.sp, + ), + ), + ], + ), + ), + ); + } + + void _renameAudioBottomSheet( + BuildContext context, + bool isDarkMode, + FileSystemEntity file, + ) { + final TextEditingController _renameController = TextEditingController( + text: file.path.split('/').last, + ); + + Get.bottomSheet( + Padding( + padding: MediaQuery.of(context).viewInsets, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 20.w), + color: isDarkMode ? AppColors.darkBackground : Colors.grey[50], + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '重命名音频文件', + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 16.sp, + ), + ), + Container( + height: 48.w, + margin: EdgeInsets.only(top: 10.w, bottom: 20.w), + alignment: Alignment.center, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.white.withOpacity(0.05) + : Colors.white, + borderRadius: BorderRadius.circular(12.w), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: TextField( + controller: _renameController, + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 16.sp, + ), + decoration: InputDecoration( + hintText: '请输入新的文件名', + hintStyle: TextStyle( + color: isDarkMode ? Colors.white70 : Colors.black54, + fontSize: 16.sp, + ), + fillColor: Colors.transparent, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(horizontal: 10.w), + ), + ), + ), + Padding( + padding: EdgeInsets.only(top: 30.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + GestureDetector( + onTap: () { + Get.back(); + }, + child: Container( + width: 120.w, + height: 40.w, + alignment: Alignment.center, + decoration: BoxDecoration( + border: Border.all( + width: 2.w, + color: isDarkMode ? AppColors.primary : Colors.blue, + ), + borderRadius: BorderRadius.circular(12.w), + ), + child: Text( + '取消', + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? AppColors.primary : Colors.blue, + ), + ), + ), + ), + GestureDetector( + onTap: () async { + final newName = _renameController.text.trim(); + if (newName.isEmpty) return; + + final dir = file.parent.path; + final newPath = '$dir/$newName'; + + try { + await file.rename(newPath); + controller.loadTranslatAudio(); // 刷新 + Get.back(); + } catch (e) { + print('重命名失败: $e'); + } + }, + child: Container( + width: 120.w, + height: 40.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: isDarkMode ? AppColors.primary : Colors.blue, + borderRadius: BorderRadius.circular(12.w), + ), + child: Text( + '确认', + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + isScrollControlled: true, + clipBehavior: Clip.hardEdge, + ); + } +} 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'; // 语音识别测试