diff --git a/lib/core/translations/language/en_us.dart b/lib/core/translations/language/en_us.dart index 9297a6ab4..e64d06e47 100644 --- a/lib/core/translations/language/en_us.dart +++ b/lib/core/translations/language/en_us.dart @@ -443,6 +443,11 @@ const Map enUS = { 'meetingRecordEndWithoutSave': 'End recording without saving', // 结束录音不保存 'meetingRecordContinueInBackground': 'Continue recording in background', // 后台继续录音 + 'meetingRecordStopTimeout': + 'Stopping the recording timed out. Please try again.', // 结束录音超时,请重试 + 'meetingRecordStopFailed': + 'Failed to stop the recording. Please try again.', // 结束录音失败,请重试 + 'meetingRecordSaveFailed': 'Failed to save the recording. Please try again.', // 保存录音失败,请重试 'meetingRecordRecordingNotificationTitle': 'Recording in progress', // 录音进行中 'meetingRecordRecordingNotificationBody': 'Recording in background. Tap to return to the app.', // 正在后台录音,点击返回应用 diff --git a/lib/core/translations/language/zh_cn.dart b/lib/core/translations/language/zh_cn.dart index 3b1352f64..2c108c5f5 100644 --- a/lib/core/translations/language/zh_cn.dart +++ b/lib/core/translations/language/zh_cn.dart @@ -419,6 +419,9 @@ const Map zhCN = { 'meetingRecordEndAndSave': '结束录音并保存', // 结束录音并保存 'meetingRecordEndWithoutSave': '结束录音不保存', // 结束录音不保存 'meetingRecordContinueInBackground': '后台继续录音', // 后台继续录音 + 'meetingRecordStopTimeout': '结束录音超时,请重试', // 结束录音超时,请重试 + 'meetingRecordStopFailed': '结束录音失败,请重试', // 结束录音失败,请重试 + 'meetingRecordSaveFailed': '保存录音失败,请重试', // 保存录音失败,请重试 'meetingRecordRecordingNotificationTitle': '录音进行中', // 录音进行中 'meetingRecordRecordingNotificationBody': '正在后台录音,点击返回应用', // 正在后台录音,点击返回应用 'overlayPermissionTitle': '悬浮窗权限', // 悬浮窗权限 diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart index 48fd48925..405fcef1f 100644 --- a/lib/modules/meeting/controllers/meeting_record_controller.dart +++ b/lib/modules/meeting/controllers/meeting_record_controller.dart @@ -31,6 +31,8 @@ enum TimerStatus { class MeetingRecordController extends GetxController with GetTickerProviderStateMixin, WidgetsBindingObserver { static const int _backgroundRecordingNotificationId = 91001; + static const Duration _stopRecordTimeout = Duration(seconds: 12); + static const Duration _fileOpTimeout = Duration(seconds: 8); static FlutterLocalNotificationsPlugin? _localNotificationsPlugin; final NativePlugin _nativePlugin = NativePlugin(); @@ -160,11 +162,18 @@ class MeetingRecordController extends GetxController final typeText = _getRecordingTypeText(); final durationText = _formatTimeForFloatingBar(milliseconds.value); - await _nativePlugin.showRecordingFloatingBar( - duration: durationText, - recordingType: typeText, - isPaused: timerStatus.value == TimerStatus.paused, - ); + try { + await _nativePlugin + .showRecordingFloatingBar( + duration: durationText, + recordingType: typeText, + isPaused: timerStatus.value == TimerStatus.paused, + ) + .timeout(const Duration(seconds: 3)); + } catch (e) { + Logger.error('显示录音悬浮条失败: ${e.toString()}'); + return; + } _recordingFloatingBarTimer?.cancel(); _recordingFloatingBarTimer = @@ -185,7 +194,13 @@ class MeetingRecordController extends GetxController if (!Platform.isAndroid) return; _recordingFloatingBarTimer?.cancel(); _recordingFloatingBarTimer = null; - await _nativePlugin.hideRecordingFloatingBar(); + try { + await _nativePlugin + .hideRecordingFloatingBar() + .timeout(const Duration(seconds: 3)); + } catch (e) { + Logger.error('隐藏录音悬浮条失败: ${e.toString()}'); + } } Future togglePauseResumeFromFloatingBar() async { @@ -246,6 +261,7 @@ class MeetingRecordController extends GetxController final RxBool isMarked = false.obs; final RxBool showTextContent = false.obs; final RxBool isRecording = false.obs; + final RxBool isSaving = false.obs; //final RxBool isVoiceRecognitionActive = false.obs; final RxDouble rms = 0.0.obs; @@ -607,62 +623,152 @@ class MeetingRecordController extends GetxController } /// 保存录音并清理相关资源 - Future saveRecording() async { - resetTimer(); + Future saveRecording() async { + if (isSaving.value) return false; + if (!isRecording.value) return true; + isSaving.value = true; + + final prevStatus = timerStatus.value; + pauseTimer(); + final prevElapsedMs = _elapsedMilliseconds; _stopWaveformTimer(); - if (audioType.value != 0) { - _bleManager.closeCodec(); - } - if (!isRecording.value) return; - await _asrService.stopRecord(true); - await clearBackgroundRecordingIndicator(); - - // 如果标题被修改则重命名文件 - if (fileName.value != newName.value) { - final appDir = await appDocDir; - final dir = Directory("${appDir.path}/MeetingAudio/"); - if (!await dir.exists()) await dir.create(recursive: true); - if (isMarked.value) { - final newDir = Directory("${appDir.path}/DestAudio/"); - if (!await newDir.exists()) await newDir.create(recursive: true); - await _asrService.moveFile( - "${dir.path}${fileName.value}.wav", - "${newDir.path}${fileName.value}.wav", - ); - } else { - await _asrService.renameFile( - "${dir.path}${fileName.value}.wav", - newName.value, - ); + + try { + final bool stopped = await _asrService + .stopRecord(true) + .timeout(_stopRecordTimeout, onTimeout: () => false); + if (!stopped) { + _restoreRecordingUiAfterFailedStop(prevStatus, prevElapsedMs); + Get.snackbar('error'.tr, 'meetingRecordStopTimeout'.tr); + return false; } - } - _resetToDefaultState(); - isRecording.value = false; - rms.value = 0.0; - _smoothedRms = 0.0; - _latestRms = 0.0; - _resetWaveform(); - await _refreshRecordingIndicator(); + if (audioType.value != 0) { + _bleManager.closeCodec(); + } + + try { + await clearBackgroundRecordingIndicator() + .timeout(const Duration(seconds: 3)); + } catch (e) { + Logger.error('清理录音指示器失败: ${e.toString()}'); + } + + try { + await _applyTitleChangeAfterSave(); + } catch (e) { + Logger.error('保存录音后的文件处理失败: ${e.toString()}'); + } + + resetTimer(); + _resetToDefaultState(); + isRecording.value = false; + rms.value = 0.0; + _smoothedRms = 0.0; + _latestRms = 0.0; + _resetWaveform(); + await _refreshRecordingIndicator(); + return true; + } catch (e) { + Logger.error('保存录音失败: ${e.toString()}'); + _restoreRecordingUiAfterFailedStop(prevStatus, prevElapsedMs); + Get.snackbar('error'.tr, 'meetingRecordSaveFailed'.tr); + return false; + } finally { + isSaving.value = false; + } } - Future discardRecording() async { - resetTimer(); + Future discardRecording() async { + if (!isRecording.value) return true; + if (isSaving.value) return false; + isSaving.value = true; + + final prevStatus = timerStatus.value; + pauseTimer(); + final prevElapsedMs = _elapsedMilliseconds; _stopWaveformTimer(); - if (audioType.value != 0) { - _bleManager.closeCodec(); + + try { + final bool stopped = await _asrService + .stopRecord(false) + .timeout(_stopRecordTimeout, onTimeout: () => false); + if (!stopped) { + _restoreRecordingUiAfterFailedStop(prevStatus, prevElapsedMs); + Get.snackbar('error'.tr, 'meetingRecordStopTimeout'.tr); + return false; + } + + if (audioType.value != 0) { + _bleManager.closeCodec(); + } + + try { + await clearBackgroundRecordingIndicator() + .timeout(const Duration(seconds: 3)); + } catch (e) { + Logger.error('清理录音指示器失败: ${e.toString()}'); + } + + resetTimer(); + _resetToDefaultState(); + isRecording.value = false; + rms.value = 0.0; + _smoothedRms = 0.0; + _latestRms = 0.0; + _resetWaveform(); + await _refreshRecordingIndicator(); + return true; + } catch (e) { + Logger.error('结束录音不保存失败: ${e.toString()}'); + _restoreRecordingUiAfterFailedStop(prevStatus, prevElapsedMs); + Get.snackbar('error'.tr, 'meetingRecordStopFailed'.tr); + return false; + } finally { + isSaving.value = false; } - if (isRecording.value) { - await _asrService.stopRecord(false); + } + + void _restoreRecordingUiAfterFailedStop( + TimerStatus prevStatus, + int prevElapsedMs, + ) { + _elapsedMilliseconds = prevElapsedMs; + seconds.value = (prevElapsedMs / 1000).round(); + milliseconds.value = prevElapsedMs; + if (prevStatus == TimerStatus.running) { + timerStatus.value = TimerStatus.stopped; + startTimer(); + } else { + timerStatus.value = prevStatus; } - await clearBackgroundRecordingIndicator(); - _resetToDefaultState(); - isRecording.value = false; - rms.value = 0.0; - _smoothedRms = 0.0; - _latestRms = 0.0; - _resetWaveform(); - await _refreshRecordingIndicator(); + _startWaveformTimer(); + } + + Future _applyTitleChangeAfterSave() async { + if (fileName.value == newName.value) return; + final appDir = await appDocDir; + final dir = Directory("${appDir.path}/MeetingAudio/"); + if (!await dir.exists()) await dir.create(recursive: true); + + if (isMarked.value) { + final newDir = Directory("${appDir.path}/DestAudio/"); + if (!await newDir.exists()) await newDir.create(recursive: true); + await _asrService + .moveFile( + "${dir.path}${fileName.value}.wav", + "${newDir.path}${fileName.value}.wav", + ) + .timeout(_fileOpTimeout); + return; + } + + await _asrService + .renameFile( + "${dir.path}${fileName.value}.wav", + newName.value, + ) + .timeout(_fileOpTimeout); } /// 将控制器重置到初始状态 diff --git a/lib/modules/meeting/views/meeting_record_view.dart b/lib/modules/meeting/views/meeting_record_view.dart index 5950067da..7ce7fa7b7 100644 --- a/lib/modules/meeting/views/meeting_record_view.dart +++ b/lib/modules/meeting/views/meeting_record_view.dart @@ -214,14 +214,18 @@ class MeetingRecordView extends GetView { switch (action) { case _MeetingRecordExitAction.saveAndEnd: await controller.saveTitleChange(); - await controller.saveRecording(); - Get.back(result: 'meeting_record_saved'); - _deleteControllerAfterPop(); + final saved = await controller.saveRecording(); + if (saved) { + Get.back(result: 'meeting_record_saved'); + _deleteControllerAfterPop(); + } return; case _MeetingRecordExitAction.endWithoutSave: - await controller.discardRecording(); - Get.back(result: 'meeting_record_discarded'); - _deleteControllerAfterPop(); + final discarded = await controller.discardRecording(); + if (discarded) { + Get.back(result: 'meeting_record_discarded'); + _deleteControllerAfterPop(); + } return; case _MeetingRecordExitAction.continueInBackground: await controller.saveTitleChange(); @@ -695,8 +699,10 @@ class MeetingRecordView extends GetView { textInputAction: TextInputAction.done, onSubmitted: (_) async { await controller.saveTitleChange(); - await controller.saveRecording(); - Get.back(); + final saved = await controller.saveRecording(); + if (saved) { + Get.back(); + } }, ), ), @@ -728,9 +734,11 @@ class MeetingRecordView extends GetView { child: ElevatedButton( onPressed: () async { await controller.saveTitleChange(); - await controller.saveRecording(); - Get.back(); //关闭弹窗 - Get.back(); //回退到会议记录页面 + final saved = await controller.saveRecording(); + if (saved) { + Get.back(); //关闭弹窗 + Get.back(); //回退到会议记录页面 + } }, style: ElevatedButton.styleFrom( backgroundColor: