diff --git a/lib/modules/meeting/bindings/meeting_record_binding.dart b/lib/modules/meeting/bindings/meeting_record_binding.dart new file mode 100644 index 000000000..3fd92a57c --- /dev/null +++ b/lib/modules/meeting/bindings/meeting_record_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; +import '../controllers/meeting_record_controller.dart'; + +class MeetingRecordBinding extends Bindings { + @override + void dependencies() { + // 注册控制器 + Get.lazyPut(() => MeetingRecordController()); + } +} diff --git a/lib/modules/meeting/controllers/meeting_controller.dart b/lib/modules/meeting/controllers/meeting_controller.dart index f80e033db..da440e590 100644 --- a/lib/modules/meeting/controllers/meeting_controller.dart +++ b/lib/modules/meeting/controllers/meeting_controller.dart @@ -1,98 +1,13 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import '../../../data/services/asr_service.dart'; - -class MeetingController extends GetxController - with GetSingleTickerProviderStateMixin { - late AnimationController lottieController; - late AsrService _asrService; - - Timer? timer; - - RxBool isRecording = false.obs; //是否录音中 - RxInt seconds = 0.obs; - - RxString intermediateContent = ''.obs; - RxString finalContent = ''.obs; - - StreamSubscription? _recognitionSubscription; - +class MeetingController extends GetxController { @override void onInit() { super.onInit(); - lottieController = AnimationController(vsync: this); - _asrService = Get.find(); } @override void onClose() { - lottieController.dispose(); - timer?.cancel(); - _asrService.stopContinuousRecognition(); - _recognitionSubscription?.cancel(); super.onClose(); } - - // 点击开始、暂停按钮 - void recorderHandle() { - if (isRecording.value) { - _pause(); - } else { - _start(); - } - } - - // 开始 - void _start() async { - await _startRecorder(); - lottieController.repeat(); - isRecording.value = true; - timer = Timer.periodic(const Duration(seconds: 1), (_) => seconds++); - } - - // 暂停 - void _pause() async { - await _stopRecorder(); - lottieController.stop(); - isRecording.value = false; - timer?.cancel(); - } - - //开始录音 - Future _startRecorder() async { - final recognitionStream = await _asrService.startContinuousRecognition(); - _recognitionSubscription = recognitionStream.listen( - _handleRecognitionEvent, - ); - } - - //结束录音 - Future _stopRecorder() async { - await _asrService.stopContinuousRecognition(); - _recognitionSubscription?.cancel(); - } - - // 处理语音识别事件 - void _handleRecognitionEvent(RecognitionEvent event) { - switch (event.type) { - case RecognitionEventType.intermediateResult: - intermediateContent.value = event.text; - break; - case RecognitionEventType.finalResult: - finalContent.value += event.text; - intermediateContent.value = ''; - break; - default: - // 不做任何处理 - break; - } - } - - // 完成 - void complete() async { - Get.back(); - } } diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart new file mode 100644 index 000000000..d00b823c1 --- /dev/null +++ b/lib/modules/meeting/controllers/meeting_record_controller.dart @@ -0,0 +1,98 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../../data/services/asr_service.dart'; + +class MeetingRecordController extends GetxController + with GetSingleTickerProviderStateMixin { + late AnimationController lottieController; + late AsrService _asrService; + + Timer? timer; + + RxBool isRecording = false.obs; //是否录音中 + RxInt seconds = 0.obs; + + RxString intermediateContent = ''.obs; + RxString finalContent = ''.obs; + + StreamSubscription? _recognitionSubscription; + + @override + void onInit() { + super.onInit(); + lottieController = AnimationController(vsync: this); + _asrService = Get.find(); + } + + @override + void onClose() { + lottieController.dispose(); + timer?.cancel(); + _asrService.stopContinuousRecognition(); + _recognitionSubscription?.cancel(); + super.onClose(); + } + + // 点击开始、暂停按钮 + void recorderHandle() { + if (isRecording.value) { + _pause(); + } else { + _start(); + } + } + + // 开始 + void _start() async { + await _startRecorder(); + lottieController.repeat(); + isRecording.value = true; + timer = Timer.periodic(const Duration(seconds: 1), (_) => seconds++); + } + + // 暂停 + void _pause() async { + await _stopRecorder(); + lottieController.stop(); + isRecording.value = false; + timer?.cancel(); + } + + //开始录音 + Future _startRecorder() async { + final recognitionStream = await _asrService.startContinuousRecognition(); + _recognitionSubscription = recognitionStream.listen( + _handleRecognitionEvent, + ); + } + + //结束录音 + Future _stopRecorder() async { + await _asrService.stopContinuousRecognition(); + _recognitionSubscription?.cancel(); + } + + // 处理语音识别事件 + void _handleRecognitionEvent(RecognitionEvent event) { + switch (event.type) { + case RecognitionEventType.intermediateResult: + intermediateContent.value = event.text; + break; + case RecognitionEventType.finalResult: + finalContent.value += event.text; + intermediateContent.value = ''; + break; + default: + // 不做任何处理 + break; + } + } + + // 完成 + void complete() async { + Get.back(); + } +} diff --git a/lib/modules/meeting/views/meeting_record_view.dart b/lib/modules/meeting/views/meeting_record_view.dart new file mode 100644 index 000000000..d83db0cc9 --- /dev/null +++ b/lib/modules/meeting/views/meeting_record_view.dart @@ -0,0 +1,320 @@ +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:lottie/lottie.dart'; + +import '../../../core/theme/app_colors.dart'; +import '../controllers/meeting_record_controller.dart'; + +class MeetingRecordView extends GetView { + const MeetingRecordView({super.key}); + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) { + if (didPop) return; + _showAlertDialog(isDarkMode); + }, + child: Scaffold( + backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], + appBar: _buildAppBar(isDarkMode), + body: _buildBody(isDarkMode), + ), + ); + } + + // 构建应用栏 + PreferredSizeWidget _buildAppBar(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: () { + _showAlertDialog(isDarkMode); + }, + ), + title: Padding( + padding: const EdgeInsets.only(right: 56), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Obx( + () => Icon( + Icons.keyboard_voice, + color: isDarkMode + ? controller.seconds.value > 0 + ? AppColors.primary + : Colors.white + : controller.seconds.value > 0 + ? Colors.blue + : Colors.black, + ), + ), + 5.horizontalSpace, + Obx( + () => Text( + DateUtil.formatDateMs( + controller.seconds.value * 1000, + format: 'HH:mm:ss', + isUtc: true, + ), + style: TextStyle( + fontSize: 17.sp, + fontWeight: FontWeight.w600, + color: isDarkMode + ? controller.seconds.value > 0 + ? AppColors.primary + : Colors.white + : controller.seconds.value > 0 + ? Colors.blue + : Colors.black, + ), + ), + ), + ], + ), + ), + centerTitle: true, + ); + } + + // 构建主体内容 + Widget _buildBody(bool isDarkMode) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + '2025-03-03 录音', + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 17.sp, + ), + ), + ), + 10.horizontalSpace, + Text( + '中文', + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 15.sp, + ), + ), + ], + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 5.w), + child: Text( + '2025-03-03 16:18', + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + fontSize: 14.sp, + ), + ), + ), + _audioWave(isDarkMode), + _recordContent(isDarkMode), + _bottomButton(isDarkMode), + ], + ), + ); + } + + // 音波 + Widget _audioWave(bool isDarkMode) { + return Container( + width: double.infinity, + height: 70.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: Row( + children: [ + Expanded( + child: Lottie.asset( + 'assets/lottie/audio_wave.json', + controller: controller.lottieController, + fit: BoxFit.fill, + onLoaded: (composition) { + controller.lottieController.duration = composition.duration; + }, + ), + ), + Expanded( + child: Lottie.asset( + 'assets/lottie/audio_wave.json', + controller: controller.lottieController, + fit: BoxFit.fill, + ), + ), + Expanded( + child: Lottie.asset( + 'assets/lottie/audio_wave.json', + controller: controller.lottieController, + fit: BoxFit.fill, + ), + ), + ], + ), + ); + } + + // 记录内容 + Widget _recordContent(bool isDarkMode) { + return Expanded( + child: Container( + width: double.infinity, + padding: EdgeInsets.all(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: Align( + alignment: Alignment.topLeft, + child: SingleChildScrollView( + reverse: true, + child: Obx( + () => Text( + '${controller.finalContent.value}${controller.intermediateContent.value}', + ), + ), + ), + ), + ), + ); + } + + // 底部按钮 + Widget _bottomButton(bool isDarkMode) { + return Padding( + padding: EdgeInsets.only(top: 40.w, bottom: 30.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + onTap: controller.recorderHandle, + child: Container( + width: 130.w, + height: 60.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(60.w), + border: Border.all( + color: isDarkMode ? AppColors.primary : Colors.blue, + width: 2.w, + ), + ), + child: Obx( + () => Icon( + controller.isRecording.value ? Icons.pause : Icons.play_arrow, + color: isDarkMode ? AppColors.primary : Colors.blue, + size: 40.w, + ), + ), + ), + ), + GestureDetector( + onTap: () { + _showAlertDialog(isDarkMode); + }, + child: Container( + width: 130.w, + height: 60.w, + decoration: BoxDecoration( + color: isDarkMode ? AppColors.primary : Colors.blue, + borderRadius: BorderRadius.circular(60.w), + ), + child: Icon( + Icons.fiber_manual_record_outlined, + color: Colors.white, + size: 40.w, + ), + ), + ), + ], + ), + ); + } + + // 显示对话框 + void _showAlertDialog(bool isDarkMode) { + if (controller.seconds.value > 0) { + Get.dialog( + AlertDialog( + backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, + title: Text( + '注意', + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + ), + ), + content: Text( + '你想结束录音吗?', + style: TextStyle( + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text( + 'cancel'.tr, // 取消 + style: TextStyle( + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + ), + TextButton( + onPressed: () { + Get.back(); + controller.complete(); + }, + child: Text( + 'confirm'.tr, // 确定 + style: TextStyle( + color: isDarkMode ? Colors.red[300] : Colors.red[600], + ), + ), + ), + ], + ), + ); + } else { + Get.back(); + } + } +} diff --git a/lib/modules/meeting/views/meeting_view.dart b/lib/modules/meeting/views/meeting_view.dart index 3e77eff37..def4b3405 100644 --- a/lib/modules/meeting/views/meeting_view.dart +++ b/lib/modules/meeting/views/meeting_view.dart @@ -1,10 +1,7 @@ -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:lottie/lottie.dart'; -import '../../../core/theme/app_colors.dart'; import '../controllers/meeting_controller.dart'; class MeetingView extends GetView { @@ -14,17 +11,10 @@ class MeetingView extends GetView { Widget build(BuildContext context) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; - return PopScope( - canPop: false, - onPopInvokedWithResult: (didPop, result) { - if (didPop) return; - _showAlertDialog(isDarkMode); - }, - child: Scaffold( - backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], - appBar: _buildAppBar(isDarkMode), - body: _buildBody(isDarkMode), - ), + return Scaffold( + backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], + appBar: _buildAppBar(isDarkMode), + body: _buildBody(isDarkMode), ); } @@ -39,50 +29,7 @@ class MeetingView extends GetView { color: isDarkMode ? Colors.white : Colors.black, size: 20.sp, ), - onPressed: () { - _showAlertDialog(isDarkMode); - }, - ), - title: Padding( - padding: const EdgeInsets.only(right: 56), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Obx( - () => Icon( - Icons.keyboard_voice, - color: isDarkMode - ? controller.seconds.value > 0 - ? AppColors.primary - : Colors.white - : controller.seconds.value > 0 - ? Colors.blue - : Colors.black, - ), - ), - 5.horizontalSpace, - Obx( - () => Text( - DateUtil.formatDateMs( - controller.seconds.value * 1000, - format: 'HH:mm:ss', - isUtc: true, - ), - style: TextStyle( - fontSize: 17.sp, - fontWeight: FontWeight.w600, - color: isDarkMode - ? controller.seconds.value > 0 - ? AppColors.primary - : Colors.white - : controller.seconds.value > 0 - ? Colors.blue - : Colors.black, - ), - ), - ), - ], - ), + onPressed: () {}, ), centerTitle: true, ); @@ -90,231 +37,6 @@ class MeetingView extends GetView { // 构建主体内容 Widget _buildBody(bool isDarkMode) { - return Padding( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - '2025-03-03 录音', - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black87, - fontSize: 17.sp, - ), - ), - ), - 10.horizontalSpace, - Text( - '中文', - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black87, - fontSize: 15.sp, - ), - ), - ], - ), - Padding( - padding: EdgeInsets.symmetric(vertical: 5.w), - child: Text( - '2025-03-03 16:18', - style: TextStyle( - color: isDarkMode ? Colors.grey[400] : Colors.grey[500], - fontSize: 14.sp, - ), - ), - ), - _audioWave(isDarkMode), - _recordContent(isDarkMode), - _bottomButton(isDarkMode), - ], - ), - ); - } - - // 音波 - Widget _audioWave(bool isDarkMode) { - return Container( - width: double.infinity, - height: 70.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: Row( - children: [ - Expanded( - child: Lottie.asset( - 'assets/lottie/audio_wave.json', - controller: controller.lottieController, - fit: BoxFit.fill, - onLoaded: (composition) { - controller.lottieController.duration = composition.duration; - }, - ), - ), - Expanded( - child: Lottie.asset( - 'assets/lottie/audio_wave.json', - controller: controller.lottieController, - fit: BoxFit.fill, - ), - ), - Expanded( - child: Lottie.asset( - 'assets/lottie/audio_wave.json', - controller: controller.lottieController, - fit: BoxFit.fill, - ), - ), - ], - ), - ); - } - - // 记录内容 - Widget _recordContent(bool isDarkMode) { - return Expanded( - child: Container( - width: double.infinity, - padding: EdgeInsets.all(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: Align( - alignment: Alignment.topLeft, - child: SingleChildScrollView( - reverse: true, - child: Obx( - () => Text( - '${controller.finalContent.value}${controller.intermediateContent.value}', - ), - ), - ), - ), - ), - ); - } - - // 底部按钮 - Widget _bottomButton(bool isDarkMode) { - return Padding( - padding: EdgeInsets.only(top: 40.w, bottom: 30.w), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - onTap: controller.recorderHandle, - child: Container( - width: 130.w, - height: 60.w, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(60.w), - border: Border.all( - color: isDarkMode ? AppColors.primary : Colors.blue, - width: 2.w, - ), - ), - child: Obx( - () => Icon( - controller.isRecording.value ? Icons.pause : Icons.play_arrow, - color: isDarkMode ? AppColors.primary : Colors.blue, - size: 40.w, - ), - ), - ), - ), - GestureDetector( - onTap: () { - _showAlertDialog(isDarkMode); - }, - child: Container( - width: 130.w, - height: 60.w, - decoration: BoxDecoration( - color: isDarkMode ? AppColors.primary : Colors.blue, - borderRadius: BorderRadius.circular(60.w), - ), - child: Icon( - Icons.fiber_manual_record_outlined, - color: Colors.white, - size: 40.w, - ), - ), - ), - ], - ), - ); - } - - // 显示对话框 - void _showAlertDialog(bool isDarkMode) { - if (controller.seconds.value > 0) { - Get.dialog( - AlertDialog( - backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, - title: Text( - '注意', - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black87, - ), - ), - content: Text( - '你想结束录音吗?', - style: TextStyle( - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - ), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: Text( - 'cancel'.tr, // 取消 - style: TextStyle( - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - ), - ), - TextButton( - onPressed: () { - Get.back(); - controller.complete(); - }, - child: Text( - 'confirm'.tr, // 确定 - style: TextStyle( - color: isDarkMode ? Colors.red[300] : Colors.red[600], - ), - ), - ), - ], - ), - ); - } else { - Get.back(); - } + return Container(); } } diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index 04467cd53..0114183d9 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -4,6 +4,8 @@ import '../modules/home/bindings/home_binding.dart'; import '../modules/explore/views/explore_view.dart'; import '../modules/explore/bindings/explore_binding.dart'; import '../modules/meeting/bindings/meeting_binding.dart'; +import '../modules/meeting/bindings/meeting_record_binding.dart'; +import '../modules/meeting/views/meeting_record_view.dart'; import '../modules/meeting/views/meeting_view.dart'; import '../modules/profile/views/profile_view.dart'; import '../modules/profile/bindings/profile_binding.dart'; @@ -58,7 +60,7 @@ abstract class AppPages { ), GetPage( name: Routes.chat, - page: () => ChatView(), + page: () => const ChatView(), binding: ChatBinding(), ), GetPage( @@ -70,8 +72,7 @@ abstract class AppPages { name: Routes.editProfile, page: () => const EditProfileView(), binding: ProfileBinding(), - ), - + ), GetPage( name: Routes.translation, page: () => const TranslationView(), @@ -87,6 +88,11 @@ abstract class AppPages { page: () => const MeetingView(), binding: MeetingBinding(), ), + GetPage( + name: Routes.meetingRecord, + page: () => const MeetingRecordView(), + binding: MeetingRecordBinding(), + ), GetPage( name: Routes.settings, page: () => const SettingsView(), diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index b7e94af7e..0504f2c71 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -8,6 +8,7 @@ abstract class Routes { static const chat = '/chat'; static const profile = '/profile'; static const meeting = '/meeting'; + static const meetingRecord = '/meetingRecord'; static const settings = '/settings'; static const speechDemo = '/speech_demo'; static const TTS_TEST = '/tts_test'; @@ -19,5 +20,5 @@ abstract class Routes { static const translationHistory = '/translation_history'; static const FLUTTER_TTS_TEST = '/flutter_tts_test'; static const FLUTTER_ASR_TEST = '/flutter_asr_test'; - static const editProfile='/edit_profile'; -} \ No newline at end of file + static const editProfile = '/edit_profile'; +}