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 'package:get/get.dart'; |
||||
|
|
||||
import '../../../data/services/asr_service.dart'; |
class MeetingController extends GetxController { |
||||
|
|
||||
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; |
|
||||
|
|
||||
@override |
@override |
||||
void onInit() { |
void onInit() { |
||||
super.onInit(); |
super.onInit(); |
||||
lottieController = AnimationController(vsync: this); |
|
||||
_asrService = Get.find<AsrService>(); |
|
||||
} |
} |
||||
|
|
||||
@override |
@override |
||||
void onClose() { |
void onClose() { |
||||
lottieController.dispose(); |
|
||||
timer?.cancel(); |
|
||||
_asrService.stopContinuousRecognition(); |
|
||||
_recognitionSubscription?.cancel(); |
|
||||
super.onClose(); |
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