Browse Source

feat(meeting): 添加会议录音退出确认对话框及后台录音功能

实现会议录音页面的退出确认对话框,提供保存并退出、不保存退出和后台继续录音选项
修改控制器逻辑以支持后台录音状态和不同的退出行为
添加多语言支持用于退出对话框的文本显示
禁用页面返回手势以避免误操作
newdev_shunjiawei
lxm 9 months ago
parent
commit
75089df6bd
  1. 6
      lib/core/translations/language/en_us.dart
  2. 5
      lib/core/translations/language/zh_cn.dart
  3. 4
      lib/modules/meeting/bindings/meeting_record_binding.dart
  4. 29
      lib/modules/meeting/controllers/meeting_record_controller.dart
  5. 18
      lib/modules/meeting/views/bottomSheet/navigation_bar_bottom_sheet.dart
  6. 223
      lib/modules/meeting/views/meeting_record_view.dart
  7. 1
      lib/routes/app_pages.dart

6
lib/core/translations/language/en_us.dart

@ -437,6 +437,12 @@ const Map<String, String> enUS = {
'audioVideoRecording': 'Audio/Video Recording',
'callRecording': 'Call Recording',
'importAudio': 'Import Audio',
'meetingRecordExitTitle': 'Exit Recording', // 退出录音
'meetingRecordExitMessage': 'Choose what to do before leaving', // 请选择离开后的操作
'meetingRecordEndAndSave': 'End recording and save', // 结束录音并保存
'meetingRecordEndWithoutSave': 'End recording without saving', // 结束录音不保存
'meetingRecordContinueInBackground':
'Continue recording in background', // 后台继续录音
'transcribing': 'Transcribing...',
'summarizing': 'Summarizing...',
'analyzing': 'Analyzing...',

5
lib/core/translations/language/zh_cn.dart

@ -414,6 +414,11 @@ const Map<String, String> zhCN = {
'audioVideoRecording': '音、视频录音',
'callRecording': '通话录音',
'importAudio': '导入音频',
'meetingRecordExitTitle': '退出录音', // 退出录音
'meetingRecordExitMessage': '请选择离开后的操作', // 请选择离开后的操作
'meetingRecordEndAndSave': '结束录音并保存', // 结束录音并保存
'meetingRecordEndWithoutSave': '结束录音不保存', // 结束录音不保存
'meetingRecordContinueInBackground': '后台继续录音', // 后台继续录音
'transcribing': '正在转写...',
'summarizing': '正在总结...',
'analyzing': '正在分析...',

4
lib/modules/meeting/bindings/meeting_record_binding.dart

@ -5,7 +5,7 @@ import '../controllers/meeting_record_controller.dart';
class MeetingRecordBinding extends Bindings {
@override
void dependencies() {
// 延迟注册 MeetingRecordController 控制器
Get.lazyPut<MeetingRecordController>(() => MeetingRecordController());
Get.put<MeetingRecordController>(MeetingRecordController(),
permanent: true);
}
}

29
lib/modules/meeting/controllers/meeting_record_controller.dart

@ -84,6 +84,14 @@ class MeetingRecordController extends GetxController
super.onClose();
}
void applyRouteArgumentsIfIdle() {
if (isEditingTitle.value) return;
if (isRecording.value) return;
if (timerStatus.value != TimerStatus.stopped) return;
_setupFromArguments();
titleEditingController.text = newName.value;
}
// 请求录音权限
Future<bool> _requestRecordPermission() async {
try {
@ -147,7 +155,9 @@ class MeetingRecordController extends GetxController
if (audioType.value != 0) {
_bleManager.closeCodec();
}
_asrService.stopRecord(true);
if (isRecording.value) {
_asrService.stopRecord(true);
}
isRecording.value = false;
rms.value = 0.0;
_smoothedRms = 0.0;
@ -408,6 +418,23 @@ class MeetingRecordController extends GetxController
_resetWaveform();
}
Future<void> discardRecording() async {
resetTimer();
_stopWaveformTimer();
if (audioType.value != 0) {
_bleManager.closeCodec();
}
if (isRecording.value) {
await _asrService.stopRecord(false);
}
_resetToDefaultState();
isRecording.value = false;
rms.value = 0.0;
_smoothedRms = 0.0;
_latestRms = 0.0;
_resetWaveform();
}
/// 将控制器重置到初始状态
void _resetToDefaultState() {
switch (audioType.value) {

18
lib/modules/meeting/views/bottomSheet/navigation_bar_bottom_sheet.dart

@ -30,10 +30,12 @@ class NavigationBarBottomSheet extends GetView<MeetingHomeController> {
Colors.red,
onTap: () async {
Get.back();
await Get.toNamed(Routes.meetingRecord, arguments: {
final result = await Get.toNamed(Routes.meetingRecord, arguments: {
'audioTypes': 0,
});
controller.addMeetingAudio();
if (result != 'meeting_record_background') {
controller.addMeetingAudio();
}
},
),
if (controller.meetingController.type == 1)
@ -45,10 +47,12 @@ class NavigationBarBottomSheet extends GetView<MeetingHomeController> {
onTap: controller.bleManager.isConnected
? () async {
Get.back();
await Get.toNamed(Routes.meetingRecord, arguments: {
final result = await Get.toNamed(Routes.meetingRecord, arguments: {
'audioTypes': 1,
});
controller.addMeetingAudio();
if (result != 'meeting_record_background') {
controller.addMeetingAudio();
}
}
: null,
),
@ -61,10 +65,12 @@ class NavigationBarBottomSheet extends GetView<MeetingHomeController> {
onTap: controller.bleManager.isConnected
? () async {
Get.back();
await Get.toNamed(Routes.meetingRecord, arguments: {
final result = await Get.toNamed(Routes.meetingRecord, arguments: {
'audioTypes': 2,
});
controller.addMeetingAudio();
if (result != 'meeting_record_background') {
controller.addMeetingAudio();
}
}
: null,
),

223
lib/modules/meeting/views/meeting_record_view.dart

@ -6,18 +6,231 @@ import 'package:get/get.dart';
import '../controllers/meeting_record_controller.dart';
import '../../../core/theme/app_colors.dart';
enum _MeetingRecordExitAction {
saveAndEnd,
endWithoutSave,
continueInBackground,
cancel,
}
class MeetingRecordView extends GetView<MeetingRecordController> {
const MeetingRecordView({super.key});
Future<_MeetingRecordExitAction?> _showExitConfirmDialog(bool isDarkMode) {
return Get.dialog<_MeetingRecordExitAction>(
Dialog(
insetPadding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 24.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white,
child: Container(
width: 320.w,
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
color: isDarkMode ? AppColors.cardBackground : Colors.white,
borderRadius: BorderRadius.circular(16.r),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 56.w,
height: 56.w,
decoration: BoxDecoration(
color: (isDarkMode ? Colors.white : Colors.blue)
.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(28.r),
),
child: Icon(
Icons.mic_rounded,
size: 28.sp,
color: isDarkMode ? Colors.white : Colors.blue,
),
),
SizedBox(height: 14.h),
Text(
'meetingRecordExitTitle'.tr, // 对应中文:退出录音
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
SizedBox(height: 8.h),
Text(
'meetingRecordExitMessage'.tr, // 对应中文:请选择离开后的操作
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.sp,
height: 1.4,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
SizedBox(height: 18.h),
SizedBox(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ElevatedButton(
onPressed: () => Get.back(
result: _MeetingRecordExitAction.saveAndEnd,
),
style: ElevatedButton.styleFrom(
backgroundColor:
isDarkMode ? AppColors.primary : Colors.blue,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
elevation: 0,
),
child: Text(
'meetingRecordEndAndSave'.tr, // 对应中文:结束录音并保存
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
),
),
),
SizedBox(height: 10.h),
OutlinedButton(
onPressed: () => Get.back(
result: _MeetingRecordExitAction.continueInBackground,
),
style: OutlinedButton.styleFrom(
foregroundColor:
isDarkMode ? Colors.white70 : Colors.grey[800],
side: BorderSide(
color:
isDarkMode ? Colors.white30 : Colors.grey[400]!,
),
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
),
child: Text(
'meetingRecordContinueInBackground'.tr, // 对应中文:后台继续录音
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
),
),
),
SizedBox(height: 10.h),
OutlinedButton(
onPressed: () => Get.back(
result: _MeetingRecordExitAction.endWithoutSave,
),
style: OutlinedButton.styleFrom(
foregroundColor:
isDarkMode ? Colors.red[300] : Colors.red[600],
side: BorderSide(
color:
isDarkMode ? Colors.red[300]! : Colors.red[300]!,
),
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
),
child: Text(
'meetingRecordEndWithoutSave'.tr, // 对应中文:结束录音不保存
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
),
),
),
SizedBox(height: 10.h),
TextButton(
onPressed: () =>
Get.back(result: _MeetingRecordExitAction.cancel),
style: TextButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
side: BorderSide(
color:
isDarkMode ? Colors.white24 : Colors.grey[300]!,
),
),
),
child: Text(
'cancel'.tr, // 对应中文:取消
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white70 : Colors.grey[700],
),
),
),
],
),
),
],
),
),
),
barrierDismissible: true,
);
}
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
controller.applyRouteArgumentsIfIdle();
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
if (!controller.isRecording.value) {
Get.back();
Future.microtask(() {
if (Get.isRegistered<MeetingRecordController>()) {
Get.delete<MeetingRecordController>(force: true);
}
});
return;
}
return WillPopScope(
onWillPop: () async {
await controller.saveTitleChange();
await controller.saveRecording();
return true;
final action = await _showExitConfirmDialog(isDarkMode);
if (action == null || action == _MeetingRecordExitAction.cancel) {
return;
}
switch (action) {
case _MeetingRecordExitAction.saveAndEnd:
await controller.saveTitleChange();
await controller.saveRecording();
Get.back(result: 'meeting_record_saved');
Future.microtask(() {
if (Get.isRegistered<MeetingRecordController>()) {
Get.delete<MeetingRecordController>(force: true);
}
});
return;
case _MeetingRecordExitAction.endWithoutSave:
await controller.discardRecording();
Get.back(result: 'meeting_record_discarded');
Future.microtask(() {
if (Get.isRegistered<MeetingRecordController>()) {
Get.delete<MeetingRecordController>(force: true);
}
});
return;
case _MeetingRecordExitAction.continueInBackground:
await controller.saveTitleChange();
Get.back(result: 'meeting_record_background');
return;
case _MeetingRecordExitAction.cancel:
return;
}
},
child: Scaffold(
appBar: AppBar(

1
lib/routes/app_pages.dart

@ -237,6 +237,7 @@ abstract class AppPages {
name: Routes.meetingRecord,
page: () => const MeetingRecordView(),
binding: MeetingRecordBinding(),
popGesture: false,
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),

Loading…
Cancel
Save