7 changed files with 447 additions and 375 deletions
@ -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>(() => MeetingRecordController()); |
|||
} |
|||
} |
|||
@ -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<AsrService>(); |
|||
} |
|||
|
|||
@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<void> _startRecorder() async { |
|||
final recognitionStream = await _asrService.startContinuousRecognition(); |
|||
_recognitionSubscription = recognitionStream.listen( |
|||
_handleRecognitionEvent, |
|||
); |
|||
} |
|||
|
|||
//结束录音 |
|||
Future<void> _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(); |
|||
} |
|||
} |
|||
|
|||
@ -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<AsrService>(); |
|||
} |
|||
|
|||
@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<void> _startRecorder() async { |
|||
final recognitionStream = await _asrService.startContinuousRecognition(); |
|||
_recognitionSubscription = recognitionStream.listen( |
|||
_handleRecognitionEvent, |
|||
); |
|||
} |
|||
|
|||
//结束录音 |
|||
Future<void> _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(); |
|||
} |
|||
} |
|||
@ -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<MeetingRecordController> { |
|||
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(); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue