Browse Source

Merge branch 'new_dev' of https://github.com/deepcloud2048/deep_voice into new_dev

newdev_shunjiawei
liwei1dao 1 year ago
parent
commit
2276d7e346
  1. 4
      android/app/src/main/AndroidManifest.xml
  2. 20
      lib/core/translations/language/en_us.dart
  3. 15
      lib/core/translations/language/zh_cn.dart
  4. 2
      lib/data/services/music_manager.dart
  5. 2
      lib/data/services/novice_guide_service.dart
  6. 28
      lib/modules/agent/controllers/agent_controller.dart
  7. 19
      lib/modules/meeting/controllers/meeting_home_controller.dart
  8. 154
      lib/modules/meeting/controllers/meeting_record_controller.dart
  9. 806
      lib/modules/meeting/views/meeting_record_view.dart
  10. 9
      lib/modules/permissions/bindings/permissions_binding.dart
  11. 316
      lib/modules/permissions/controllers/permissions_controller.dart
  12. 67
      lib/modules/permissions/models/permission_group.dart
  13. 283
      lib/modules/permissions/views/permissions_view.dart
  14. 97
      lib/modules/settings/controllers/settings_controller.dart
  15. 402
      lib/modules/settings/views/permissions_view.dart
  16. 111
      lib/modules/splash/controllers/splash_controller.dart
  17. 5
      lib/routes/app_pages.dart
  18. 1
      pubspec.yaml
  19. 3
      windows/flutter/generated_plugin_registrant.cc
  20. 1
      windows/flutter/generated_plugins.cmake

4
android/app/src/main/AndroidManifest.xml

@ -28,6 +28,10 @@
<!-- 添加Android 11+的存储权限 --> <!-- 添加Android 11+的存储权限 -->
<!-- <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" <!-- <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" /> --> tools:ignore="ScopedStorage" /> -->
<!-- Android 13+ 媒体权限 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<!-- 添加悬浮窗权限,可能有助于解决前台服务启动问题 --> <!-- 添加悬浮窗权限,可能有助于解决前台服务启动问题 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

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

@ -487,11 +487,18 @@ const Map<String, String> enUS = {
// 权限管理页面 // 权限管理页面
'getPermissionStatusFailed': 'Failed to get permission status', 'getPermissionStatusFailed': 'Failed to get permission status',
'communicationPermissions': 'Communication Permissions', 'communicationPermissions': 'Communication Permissions',
'bluetoothPermissions': 'Bluetooth Permissions',
'notificationPermissions': 'Notification Permissions',
'storagePermissions': 'Storage Permissions',
'microphonePermissions': 'Microphone Permissions',
'alarmPermissions': 'Alarm Permissions', 'alarmPermissions': 'Alarm Permissions',
'calendarPermissions': 'Calendar Permissions', 'calendarPermissions': 'Calendar Permissions',
'locationPermissions': 'Location Permissions', 'locationPermissions': 'Location Permissions',
'phone': 'Phone', 'phone': 'Phone',
'sms': 'SMS', 'sms': 'SMS',
'contacts': 'Contacts',
//'bluetooth': 'Bluetooth',
'notification': 'Notification',
'alarm': 'Alarm', 'alarm': 'Alarm',
'calendarWrite': 'Calendar Write', 'calendarWrite': 'Calendar Write',
'calendarFullAccess': 'Calendar Full Access', 'calendarFullAccess': 'Calendar Full Access',
@ -521,6 +528,19 @@ const Map<String, String> enUS = {
'contactsPermissionDesc': 'Used for accessing contact information', 'contactsPermissionDesc': 'Used for accessing contact information',
'contactsPermissionDetailDesc': 'contactsPermissionDetailDesc':
'Phone call and SMS functionality requires contacts permission to select contacts', 'Phone call and SMS functionality requires contacts permission to select contacts',
'bluetoothPermissionDesc':
'Used for connecting and managing Bluetooth devices',
'bluetoothPermissionDetailDesc':
'Connecting to Bluetooth headphones and other devices requires Bluetooth permission',
'bluetoothScanPermissionDesc': 'Used for scanning nearby Bluetooth devices',
'bluetoothScanPermissionDetailDesc':
'Scanning for nearby Bluetooth devices requires Bluetooth scan permission',
'bluetoothConnectPermissionDesc': 'Used for connecting to Bluetooth devices',
'bluetoothConnectPermissionDetailDesc':
'Connecting to Bluetooth devices requires Bluetooth connect permission',
'notificationPermissionDesc': 'Used for sending notification messages',
'notificationPermissionDetailDesc':
'App needs notification permission to send you important alerts and messages',
'calendarWritePermissionDetailDesc': 'calendarWritePermissionDetailDesc':
'Creating calendar events requires calendar write permission', 'Creating calendar events requires calendar write permission',
'calendarFullAccessPermissionDetailDesc': 'calendarFullAccessPermissionDetailDesc':

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

@ -455,11 +455,18 @@ const Map<String, String> zhCN = {
// 权限管理页面 // 权限管理页面
'getPermissionStatusFailed': '获取权限状态失败', 'getPermissionStatusFailed': '获取权限状态失败',
'communicationPermissions': '通讯权限', 'communicationPermissions': '通讯权限',
'bluetoothPermissions': '蓝牙权限',
'notificationPermissions': '通知权限',
'storagePermissions': '存储权限',
'microphonePermissions': '麦克风权限',
'alarmPermissions': '闹钟权限', 'alarmPermissions': '闹钟权限',
'calendarPermissions': '日历权限', 'calendarPermissions': '日历权限',
'locationPermissions': '位置权限', 'locationPermissions': '位置权限',
'phone': '电话', 'phone': '电话',
'sms': '短信', 'sms': '短信',
'contacts': '联系人',
//'bluetooth': '蓝牙',
'notification': '通知',
'alarm': '闹钟', 'alarm': '闹钟',
'calendarWrite': '日历写入', 'calendarWrite': '日历写入',
'calendarFullAccess': '日历完整访问', 'calendarFullAccess': '日历完整访问',
@ -486,6 +493,14 @@ const Map<String, String> zhCN = {
'locationPermissionDetailDesc': '获取位置信息需要位置权限', 'locationPermissionDetailDesc': '获取位置信息需要位置权限',
'contactsPermissionDesc': '用于访问联系人信息', 'contactsPermissionDesc': '用于访问联系人信息',
'contactsPermissionDetailDesc': '拨打电话和发送短信功能需要联系人权限以选择联系人', 'contactsPermissionDetailDesc': '拨打电话和发送短信功能需要联系人权限以选择联系人',
'bluetoothPermissionDesc': '用于连接和管理蓝牙设备',
'bluetoothPermissionDetailDesc': '连接蓝牙耳机等设备需要蓝牙权限',
'bluetoothScanPermissionDesc': '用于扫描附近的蓝牙设备',
'bluetoothScanPermissionDetailDesc': '扫描附近蓝牙设备需要蓝牙扫描权限',
'bluetoothConnectPermissionDesc': '用于连接蓝牙设备',
'bluetoothConnectPermissionDetailDesc': '连接蓝牙设备需要蓝牙连接权限',
'notificationPermissionDesc': '用于发送通知消息',
'notificationPermissionDetailDesc': '应用需要通知权限以向您发送重要提醒和消息',
'appNeedsPermissionForFullFunction': '应用需要此权限以提供完整功能', 'appNeedsPermissionForFullFunction': '应用需要此权限以提供完整功能',
'permanentDenialText': '您已永久拒绝{permission}权限,请前往应用设置开启', 'permanentDenialText': '您已永久拒绝{permission}权限,请前往应用设置开启',
'goToSettings': '去设置', 'goToSettings': '去设置',

2
lib/data/services/music_manager.dart

@ -22,7 +22,7 @@ class MusicManager extends GetxService {
// Logger.error('无法获取 BuildContext'); // Logger.error('无法获取 BuildContext');
// return; // return;
// } // }
requestNotificationPermission(); //requestNotificationPermission();
Logger.info('初始化权限请求'); Logger.info('初始化权限请求');
} }

2
lib/data/services/novice_guide_service.dart

@ -148,7 +148,7 @@ class NoviceGuideService extends GetxService {
void startAIGuide() async { void startAIGuide() async {
if (!isAIGuide) { if (!isAIGuide) {
await Future.delayed(const Duration(milliseconds: 200)); await Future.delayed(const Duration(milliseconds: 300));
isAIGuide = true; isAIGuide = true;
_storage.write('ai_guide', true); _storage.write('ai_guide', true);
Intro.of(Get.context!).start(group: 'ai'); Intro.of(Get.context!).start(group: 'ai');

28
lib/modules/agent/controllers/agent_controller.dart

@ -1060,20 +1060,20 @@ class AgentController extends GetxController {
Future<void> startVoiceInput() async { Future<void> startVoiceInput() async {
if (isListening.value) return; if (isListening.value) return;
Logger.i(TAG, '开始语音输入'); // 开始语音输入 Logger.i(TAG, '开始语音输入'); // 开始语音输入
final hasPermission = await PermissionUtil.instance.requestPermission( // final hasPermission = await PermissionUtil.instance.requestPermission(
// context: Get.context!, // // context: Get.context!,
permissionType: Permission.microphone, // permissionType: Permission.microphone,
permissionName: 'microphonePermission'.tr, // 麦克风 // permissionName: 'microphonePermission'.tr, // 麦克风
explanationText: 'voiceTranslationPermissionText'.tr, // 语音识别与翻译功能需要访问麦克风。 // explanationText: 'voiceTranslationPermissionText'.tr, // 语音识别与翻译功能需要访问麦克风。
permanentDenialText: // permanentDenialText:
'permissionDeniedText'.tr, // 您已永久拒绝麦克风权限,请前往系统设置开启权限。 // 'permissionDeniedText'.tr, // 您已永久拒绝麦克风权限,请前往系统设置开启权限。
settingsButtonText: 'goToSettings'.tr, // 去设置 // settingsButtonText: 'goToSettings'.tr, // 去设置
cancelButtonText: 'cancel'.tr, // 取消 // cancelButtonText: 'cancel'.tr, // 取消
); // );
if (!hasPermission) { // if (!hasPermission) {
isTextInputMode.value = true; // 如果没有权限,切回文本模式 // isTextInputMode.value = true; // 如果没有权限,切回文本模式
return; // return;
} // }
try { try {
await AgentService.recognizeCallback(); await AgentService.recognizeCallback();

19
lib/modules/meeting/controllers/meeting_home_controller.dart

@ -1,5 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart';
@ -88,6 +89,24 @@ class MeetingHomeController extends GetxController {
// 请求存储权限 // 请求存储权限
void _requestStoragePermission() async { void _requestStoragePermission() async {
// 只在Android 10及以下版本请求存储权限
if (Platform.isAndroid) {
try {
final deviceInfo = DeviceInfoPlugin();
final androidInfo = await deviceInfo.androidInfo;
final sdkVersion = androidInfo.version.sdkInt;
// Android 11+ (API 30+) 不需要存储权限
if (sdkVersion >= 30) {
Logger.d("Permission", "Android 11+版本,无需请求存储权限");
return;
}
} catch (e) {
Logger.e("Permission", "获取Android版本信息失败: $e");
// 如果获取版本失败,为了兼容性还是请求权限
}
}
PermissionUtil.instance PermissionUtil.instance
.requestPermission( .requestPermission(
permissionType: Permission.storage, permissionType: Permission.storage,

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

@ -23,7 +23,7 @@ enum TimerStatus {
/// Controller for meeting recording screen /// Controller for meeting recording screen
/// Handles recording state, audio processing, transcription, and UI interactions /// Handles recording state, audio processing, transcription, and UI interactions
class MeetingRecordController extends GetxController class MeetingRecordController extends GetxController
with SingleGetTickerProviderMixin { with GetSingleTickerProviderStateMixin {
// Services // Services
final AsrService _asrService = Get.find<AsrService>(); final AsrService _asrService = Get.find<AsrService>();
final BleManager _bleManager = Get.find<BleManager>(); final BleManager _bleManager = Get.find<BleManager>();
@ -57,8 +57,7 @@ class MeetingRecordController extends GetxController
Timer? _timer; Timer? _timer;
int _elapsedMilliseconds = 0; int _elapsedMilliseconds = 0;
int? _lastStartTime; int? _lastStartTime;
bool _audioSourceType = false; // 移除了未使用的变量和TabController
late TabController tabController;
StreamSubscription<Uint8List>? _audioDataSubscription; StreamSubscription<Uint8List>? _audioDataSubscription;
// 添加波形放大倍数变量 // 添加波形放大倍数变量
@ -67,6 +66,10 @@ class MeetingRecordController extends GetxController
final RxInt _currentPosition = 0.obs; // 原始位置索引 final RxInt _currentPosition = 0.obs; // 原始位置索引
int maxPoints = 500; // 最大显示点数(根据屏幕宽度优化) int maxPoints = 500; // 最大显示点数(根据屏幕宽度优化)
int sampleRate = 16000; // 假设音频采样率(根据实际情况调整) int sampleRate = 16000; // 假设音频采样率(根据实际情况调整)
// 波形流动动画控制器
late AnimationController waveformAnimationController;
late Animation<double> waveformAnimation;
// 设置最大点数(根据屏幕宽度) // 设置最大点数(根据屏幕宽度)
void setMaxPoints(double screenWidth) { void setMaxPoints(double screenWidth) {
maxPoints = screenWidth ~/ 2; // 每2像素一个数据点 maxPoints = screenWidth ~/ 2; // 每2像素一个数据点
@ -77,12 +80,14 @@ class MeetingRecordController extends GetxController
super.onInit(); super.onInit();
_initializeComponents(); _initializeComponents();
_setupFromArguments(); _setupFromArguments();
generateWaveData(); _initializeWaveformAnimation();
//generateWaveData();
} }
@override @override
void onClose() { void onClose() {
_cleanupResources(); _cleanupResources();
waveformAnimationController.dispose();
super.onClose(); super.onClose();
} }
@ -111,12 +116,36 @@ class MeetingRecordController extends GetxController
/// Initializes UI controllers and focus nodes /// Initializes UI controllers and focus nodes
void _initializeComponents() { void _initializeComponents() {
tabController = TabController(length: 1, vsync: this);
titleEditingController = TextEditingController(text: fileName.value); titleEditingController = TextEditingController(text: fileName.value);
titleFocusNode = FocusNode(); titleFocusNode = FocusNode();
titleFocusNode.addListener(_handleTitleFocusChange); titleFocusNode.addListener(_handleTitleFocusChange);
} }
/// 初始化波形流动动画
void _initializeWaveformAnimation() {
// 设置动画持续时间,控制流动速度
waveformAnimationController = AnimationController(
duration: const Duration(milliseconds: 100), // 更快的流动速度
vsync: this,
);
waveformAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: waveformAnimationController,
curve: Curves.linear, // 线性动画确保匀速流动
));
// 监听动画完成,实现连续循环
waveformAnimationController.addStatusListener((status) {
if (status == AnimationStatus.completed && isRecording.value) {
waveformAnimationController.reset();
waveformAnimationController.forward();
}
});
}
/// Sets up initial state from navigation arguments /// Sets up initial state from navigation arguments
void _setupFromArguments() { void _setupFromArguments() {
final Map<String, dynamic> args = Get.arguments ?? {}; final Map<String, dynamic> args = Get.arguments ?? {};
@ -126,15 +155,12 @@ class MeetingRecordController extends GetxController
switch (audioType.value) { switch (audioType.value) {
case 0: case 0:
fileName.value = "现场录音"; fileName.value = "现场录音";
_audioSourceType = false;
break; break;
case 1: case 1:
fileName.value = "音、视频录音"; fileName.value = "音、视频录音";
_audioSourceType = true;
break; break;
case 2: case 2:
fileName.value = "通话录音"; fileName.value = "通话录音";
_audioSourceType = true;
break; break;
} }
newName.value = fileName.value; newName.value = fileName.value;
@ -145,6 +171,8 @@ class MeetingRecordController extends GetxController
_timer?.cancel(); _timer?.cancel();
_recognitionSubscription?.cancel(); _recognitionSubscription?.cancel();
_recognitionSubscription = null; _recognitionSubscription = null;
_audioDataSubscription?.cancel();
_audioDataSubscription = null;
titleEditingController.dispose(); titleEditingController.dispose();
titleFocusNode.dispose(); titleFocusNode.dispose();
if (audioType.value != 0) { if (audioType.value != 0) {
@ -242,7 +270,7 @@ class MeetingRecordController extends GetxController
// 1. 实时音频可视化 // 1. 实时音频可视化
// 2. 音频质量检测 // 2. 音频质量检测
// 3. 发送到其他服务 // 3. 发送到其他服务
print('接收到音频数据: ${audioData.length} 字节'); Logger.d("AudioData", "接收到音频数据: ${audioData.length} 字节");
// 处理音频数据的逻辑 // 处理音频数据的逻辑
_processAudioData(audioData); _processAudioData(audioData);
@ -254,6 +282,9 @@ class MeetingRecordController extends GetxController
newName.value = fullFileName; newName.value = fullFileName;
titleEditingController.text = fullFileName; titleEditingController.text = fullFileName;
// 启动波形动画
_startWaveformAnimation();
// Start hardware components // Start hardware components
_startHardwareServices(); _startHardwareServices();
startTimer(); startTimer();
@ -279,31 +310,17 @@ class MeetingRecordController extends GetxController
/// Pauses current recording session /// Pauses current recording session
Future<void> _pauseRecording() async { Future<void> _pauseRecording() async {
await _asrService.pauseRecord(); await _asrService.pauseRecord();
_stopWaveformAnimation(); // 暂停时停止动画
pauseTimer(); pauseTimer();
} }
/// Resumes paused recording session /// Resumes paused recording session
Future<void> _resumeRecording() async { Future<void> _resumeRecording() async {
await _asrService.resumeRecord(); await _asrService.resumeRecord();
_startWaveformAnimation(); // 恢复时启动动画
startTimer(); startTimer();
} }
/// Handles speech recognition events
void _handleRecognitionEvent(RecognitionEvent event) {
switch (event.type) {
case RecognitionEventType.intermediateResult:
intermediateContent.value = event.text;
break;
case RecognitionEventType.finalResult:
finalContent.value += _formatTranscript(event);
intermediateContent.value = '';
break;
default:
break;
}
}
/// 处理原始Uint8List音频数据 /// 处理原始Uint8List音频数据
void _processAudioData(Uint8List audioData) { void _processAudioData(Uint8List audioData) {
// 将Uint8List转换为Float32List(PCM16 -> 浮点) // 将Uint8List转换为Float32List(PCM16 -> 浮点)
@ -360,70 +377,55 @@ class MeetingRecordController extends GetxController
return _currentPosition.value / waveData.length; return _currentPosition.value / waveData.length;
} }
/// 添加处理后的音频数据 /// 添加处理后的音频数据 - 优化流动效果
void _addAudioData(Float32List newAudio) { void _addAudioData(Float32List newAudio) {
// 1. 降采样:只保留部分数据点 if (!isRecording.value) return;
final downsampled = _downsampleAudio(newAudio);
waveData.addAll(downsampled); // 1. 降采样处理
final downsampled = _downsampleAudio(newAudio);
if (downsampled.isEmpty) return;
// 3. 更新光标位置到最新点 // 2. 逐个添加数据点,确保平滑流动
_currentPosition.value = waveData.length; for (final sample in downsampled) {
waveData.add(sample);
// 4. 限制数据长度,移除旧数据 //维护合理的数据长度 - 保持滑动窗口效果
if (waveData.length > maxPoints * 2) { if (waveData.length > maxPoints) {
waveData.removeRange(0, waveData.length - maxPoints); waveData.removeAt(0); // 移除最旧的数据
_currentPosition.value = waveData.length; }
} }
// 3. 确保动画持续运行
_ensureAnimationRunning();
// 4. 更新位置信息
_currentPosition.value = waveData.length;
} }
/// Formats transcript with speaker identification /// 启动波形流动动画
String _formatTranscript(RecognitionEvent event) { void _startWaveformAnimation() {
final buffer = StringBuffer(); if (isRecording.value && !waveformAnimationController.isAnimating) {
waveformAnimationController.forward();
if (finalContent.value.isNotEmpty) {
if (event.role.isNotEmpty) {
// Add line break before new speaker
if (_isPunctuation(event.text[0])) {
buffer.write('${event.text[0]}\n');
buffer.write('说话人${event.role}:');
buffer.write(event.text.substring(1));
} else {
buffer.write('\n说话人${event.role}:');
buffer.write(event.text);
}
} else {
buffer.write(event.text);
}
} else {
buffer.write('说话人${event.role}:${event.text}');
} }
}
return buffer.toString(); /// 停止波形动画
void _stopWaveformAnimation() {
waveformAnimationController.stop();
waveformAnimationController.reset();
} }
/// Checks if character is Chinese/English punctuation /// 确保动画持续运行
bool _isPunctuation(String char) { void _ensureAnimationRunning() {
const punctuations = { if (isRecording.value && !waveformAnimationController.isAnimating) {
',', waveformAnimationController.forward();
'。', }
'!',
'?',
';',
':',
',',
'.',
':',
';',
'!',
'?'
};
return punctuations.contains(char);
} }
/// Saves recording and cleans up resources /// Saves recording and cleans up resources
Future<void> saveRecording() async { Future<void> saveRecording() async {
resetTimer(); resetTimer();
_stopWaveformAnimation(); // 停止波形动画
if (audioType.value != 0) { if (audioType.value != 0) {
_bleManager.closeCodec(); _bleManager.closeCodec();
} }
@ -436,11 +438,11 @@ class MeetingRecordController extends GetxController
final dir = Directory("${appDir.path}/MeetingAudio/"); final dir = Directory("${appDir.path}/MeetingAudio/");
if (!await dir.exists()) await dir.create(recursive: true); if (!await dir.exists()) await dir.create(recursive: true);
if (isMarked.value) { if (isMarked.value) {
final Newdir = Directory("${appDir.path}/DestAudio/"); final newDir = Directory("${appDir.path}/DestAudio/");
if (!await Newdir.exists()) await Newdir.create(recursive: true); if (!await newDir.exists()) await newDir.create(recursive: true);
await _asrService.moveFile( await _asrService.moveFile(
"${dir.path}${fileName.value}.wav", "${dir.path}${fileName.value}.wav",
"${Newdir.path}${fileName.value}.wav", "${newDir.path}${fileName.value}.wav",
); );
} else { } else {
await _asrService.renameFile( await _asrService.renameFile(

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

@ -3,7 +3,6 @@ import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:lottie/lottie.dart';
import '../controllers/meeting_record_controller.dart'; import '../controllers/meeting_record_controller.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
@ -14,17 +13,22 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark; final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return WillPopScope( return PopScope(
onWillPop: () async { onPopInvokedWithResult: (didPop, result) async {
// 在返回时执行保存操作 if (didPop) {
controller.saveTitleChange(); // 在返回时执行保存操作
await controller.saveRecording(); controller.saveTitleChange();
return true; // 允许退出 //await controller.saveRecording();
}
}, },
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back), icon: Icon(
Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black,
size: 20.sp,
),
onPressed: () async { onPressed: () async {
// 在返回按钮点击时执行保存操作 // 在返回按钮点击时执行保存操作
controller.saveTitleChange(); controller.saveTitleChange();
@ -48,7 +52,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
isDense: true, isDense: true,
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
border: InputBorder.none, border: InputBorder.none,
hintText: '输入文件名', hintText: 'enterFileName'.tr,
hintStyle: TextStyle( hintStyle: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54, color: isDarkMode ? Colors.white70 : Colors.black54,
), ),
@ -88,24 +92,9 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
), ),
body: Column( body: Column(
children: [ children: [
TabBar( // 简化为单一音频界面,移除不必要的TabBar
controller: controller.tabController,
indicatorColor: Colors.red,
labelColor: isDarkMode ? Colors.white : Colors.black,
unselectedLabelColor: Colors.grey,
tabs: const [
Tab(text: '音频'),
// Tab(text: '文本'),
],
),
Expanded( Expanded(
child: TabBarView( child: _buildAudioTab(context, isDarkMode),
controller: controller.tabController,
children: [
_buildAudioTab(context, isDarkMode),
// _buildTextTab(context, isDarkMode),
],
),
), ),
], ],
), ),
@ -114,339 +103,574 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
} }
Widget _buildAudioTab(BuildContext context, bool isDarkMode) { Widget _buildAudioTab(BuildContext context, bool isDarkMode) {
return Obx(() { return LayoutBuilder(
final screenWidth = MediaQuery.of(context).size.width; builder: (context, constraints) {
// 初始化最大点数 // 使用LayoutBuilder获取准确的屏幕宽度
if (controller.maxPoints == 0) { final screenWidth = constraints.maxWidth;
controller.setMaxPoints(screenWidth);
} // 初始化最大点数
final cursorPos = controller.cursorPositionValue; if (controller.maxPoints == 0) {
return Column( controller.setMaxPoints(screenWidth);
children: [ }
const SizedBox(height: 24),
SizedBox(
height: 120,
child: Stack(
alignment: Alignment.center,
children: [
// 波形绘制
CustomPaint(
painter: WaveformPainter(
controller.waveData,
cursorPosition: cursorPos,
// color: isDarkMode ? AppColors.darkBackground : Colors.grey[50],
),
size: Size(screenWidth, 120),
),
// 光标指示器
Positioned(
left: MediaQuery.of(context).size.width * cursorPos,
child: Container(
width: 2,
height: 120,
color: Colors.red,
),
),
],
),
),
const SizedBox(height: 16),
Text(
controller.formatTime(controller.seconds.value),
style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
),
const Spacer(),
_buildBottomBar(context, isDarkMode),
const SizedBox(height: 32),
],
);
});
}
Widget _buildTextTab(BuildContext context, bool isDarkMode) { return Column(
return Obx(() => Column(
children: [ children: [
const SizedBox(height: 48), SizedBox(height: 24.h),
Center( // 专业级实时流动波形显示区域
child: Column( Container(
children: [ height: 120.h,
const Icon(Icons.graphic_eq, color: Colors.red, size: 32), margin: EdgeInsets.symmetric(horizontal: 16.w),
const SizedBox(height: 8), decoration: BoxDecoration(
Text( gradient: LinearGradient(
controller.formatTime(controller.seconds.value), begin: Alignment.topCenter,
style: const TextStyle( end: Alignment.bottomCenter,
fontSize: 36, fontWeight: FontWeight.bold), colors: isDarkMode
? [
const Color(0xFF1A1A1A),
const Color(0xFF2A2A2A),
const Color(0xFF1A1A1A),
]
: [
const Color(0xFFF8F9FA),
const Color(0xFFFFFFFF),
const Color(0xFFF8F9FA),
],
),
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: isDarkMode
? Colors.white.withValues(alpha: 0.1)
: Colors.black.withValues(alpha: 0.05),
width: 1,
),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withValues(alpha: 0.3)
: Colors.black.withValues(alpha: 0.08),
blurRadius: 8,
offset: const Offset(0, 2),
), ),
], ],
), ),
), child: ClipRRect(
const SizedBox(height: 24), borderRadius: BorderRadius.circular(12.r),
// 新增:转文本提示区域 child: Stack(
if (!controller.showTextContent.value) ...[ children: [
const Text('无文本', // 实时流动波形绘制组件
style: TextStyle( _WaveformWidget(
color: Colors.grey, controller: controller,
fontSize: 16, isDarkMode: isDarkMode,
)), size: Size(screenWidth - 32.w, 120.h),
const SizedBox(height: 16),
ElevatedButton(
onPressed: controller.toggleTextDisplay,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 8,
),
),
child: const Text('转文本'),
),
],
// 文本内容区域(转文本后显示)
if (controller.showTextContent.value)
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: SingleChildScrollView(
child: Text(
'${controller.finalContent.value}${controller.intermediateContent.value}',
style: const TextStyle(fontSize: 16),
), ),
),
// 录音状态指示器
Obx(() {
if (controller.timerStatus.value == TimerStatus.running) {
return Positioned(
top: 8.h,
right: 8.w,
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 6.w,
vertical: 2.h,
),
decoration: BoxDecoration(
color: Colors.red.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(8.r),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 4.w,
height: 4.h,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
),
SizedBox(width: 3.w),
Text(
'REC',
style: TextStyle(
color: Colors.white,
fontSize: 8.sp,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
}
return const SizedBox.shrink();
}),
],
), ),
), ),
),
SizedBox(height: 24.h),
// 时间显示 - 只包装需要响应式更新的Text widget
Obx(() => Text(
controller.formatTime(controller.seconds.value),
style: TextStyle(
fontSize: 48.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black87,
),
)),
const Spacer(), const Spacer(),
_buildBottomBar(context, isDarkMode), _buildBottomBar(context, isDarkMode),
const SizedBox(height: 32), SizedBox(height: 32.h),
], ],
)); );
},
);
} }
// 底部操作栏 // 底部操作栏
Widget _buildBottomBar(BuildContext context, bool isDarkMode) { Widget _buildBottomBar(BuildContext context, bool isDarkMode) {
return Row( return Padding(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, padding: EdgeInsets.symmetric(horizontal: 20.w),
children: [ child: Row(
// 收藏按钮 mainAxisAlignment: MainAxisAlignment.spaceEvenly,
GestureDetector( children: [
onTap: controller.toggleMarked, // 收藏按钮
child: Obx(() => CircleAvatar( _buildActionButton(
radius: 32, onTap: controller.toggleMarked,
backgroundColor: Colors.white, child: Obx(() => Icon(
child: Icon(
controller.isMarked.value ? Icons.star : Icons.star_border, controller.isMarked.value ? Icons.star : Icons.star_border,
size: 36, size: 36.sp,
color: color:
controller.isMarked.value ? Colors.orange : Colors.grey, controller.isMarked.value ? Colors.orange : Colors.grey,
), )),
)), radius: 32.w,
), tooltip: controller.isMarked.value
// 录音按钮 ? 'unmarkFavorite'.tr
GestureDetector( : 'markFavorite'.tr,
onTap: controller.toggleRecording, ),
child: Obx(() => CircleAvatar( // 录音按钮
radius: 40, _buildActionButton(
backgroundColor: Colors.white, onTap: controller.toggleRecording,
child: Icon( child: Obx(() => Icon(
controller.timerStatus.value == TimerStatus.running controller.timerStatus.value == TimerStatus.running
? Icons.pause ? Icons.pause
: Icons.play_arrow, : Icons.play_arrow,
size: 44, size: 44.sp,
color: Colors.red, color: Colors.red,
), )),
)), radius: 40.w,
), tooltip: controller.timerStatus.value == TimerStatus.running
// 完成按钮 ? 'pauseRecording'.tr
GestureDetector( : 'startRecording'.tr,
onTap: () => _bottomSheet(context, isDarkMode), // 改为匿名函数 ),
child: const CircleAvatar( // 完成按钮
radius: 32, _buildActionButton(
backgroundColor: Colors.white, onTap: () => _bottomSheet(context, isDarkMode),
child: Icon( child: Icon(
Icons.check, Icons.check,
size: 36, size: 36.sp,
color: Colors.green, color: Colors.green,
), ),
radius: 32.w,
tooltip: 'finishRecording'.tr,
),
],
),
);
}
// 统一的操作按钮组件
Widget _buildActionButton({
required VoidCallback onTap,
required Widget child,
required double radius,
required String tooltip,
}) {
return Tooltip(
message: tooltip,
child: Material(
color: Colors.white,
shape: const CircleBorder(),
elevation: 4,
child: InkWell(
onTap: onTap,
customBorder: const CircleBorder(),
child: CircleAvatar(
radius: radius,
backgroundColor: Colors.white,
child: child,
), ),
), ),
], ),
); );
} }
// 底部编辑弹窗 // 底部编辑弹窗
void _bottomSheet(BuildContext context, bool isDarkMode) { void _bottomSheet(BuildContext context, bool isDarkMode) {
Get.bottomSheet( Get.bottomSheet(
Padding( Container(
padding: MediaQuery.of(context).viewInsets, padding: EdgeInsets.only(
child: Container( left: 20.w,
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 20.w), right: 20.w,
decoration: BoxDecoration( top: 20.w,
color: isDarkMode ? AppColors.darkBackground : Colors.grey[50], bottom: MediaQuery.of(context).viewInsets.bottom + 20.w,
borderRadius: BorderRadius.vertical(top: Radius.circular(20.w)), ),
), decoration: BoxDecoration(
child: Column( color: isDarkMode ? AppColors.darkBackground : Colors.white,
mainAxisSize: MainAxisSize.min, borderRadius: BorderRadius.vertical(top: Radius.circular(20.r)),
children: [ boxShadow: [
// 标题 BoxShadow(
Padding( color: Colors.black.withValues(alpha: 0.1),
padding: EdgeInsets.only(bottom: 20.w), blurRadius: 10,
child: Text( offset: const Offset(0, -2),
'editMeetingTitle'.tr, ),
style: TextStyle( ],
color: isDarkMode ? Colors.white : Colors.black87, ),
fontSize: 18.sp, child: Column(
fontWeight: FontWeight.bold, mainAxisSize: MainAxisSize.min,
), crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 拖拽指示器
Center(
child: Container(
width: 40.w,
height: 4.h,
margin: EdgeInsets.only(bottom: 20.h),
decoration: BoxDecoration(
color: Colors.grey[400],
borderRadius: BorderRadius.circular(2.r),
), ),
), ),
),
// 标题
Text(
'editMeetingTitle'.tr,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 20.sp,
fontWeight: FontWeight.bold,
),
),
// 输入框 SizedBox(height: 16.h),
Container(
decoration: BoxDecoration( // 输入框
color: isDarkMode ? Colors.white10 : Colors.white, Container(
borderRadius: BorderRadius.circular(12.w), decoration: BoxDecoration(
color: isDarkMode ? Colors.white10 : Colors.grey[100],
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: isDarkMode ? Colors.white24 : Colors.grey[300]!,
width: 1,
), ),
child: TextField( ),
controller: controller.titleEditingController, child: TextField(
autofocus: true, // 自动弹出键盘 controller: controller.titleEditingController,
maxLength: 50, // 添加字符限制 autofocus: true,
style: TextStyle( maxLength: 50,
color: isDarkMode ? Colors.white : Colors.black87, style: TextStyle(
fontSize: 16.sp, color: isDarkMode ? Colors.white : Colors.black87,
), fontSize: 16.sp,
decoration: InputDecoration( ),
hintText: 'maxCharacters'.tr, decoration: InputDecoration(
hintStyle: TextStyle( hintText: 'enterFileName'.tr,
color: isDarkMode ? Colors.white60 : Colors.black45, hintStyle: TextStyle(
), color: isDarkMode ? Colors.white60 : Colors.grey[600],
border: InputBorder.none,
contentPadding: EdgeInsets.all(16.w),
counterText: '', // 隐藏默认计数器
), ),
border: InputBorder.none,
contentPadding: EdgeInsets.all(16.w),
counterText: '',
), ),
textInputAction: TextInputAction.done,
onSubmitted: (_) async {
await controller.saveTitleChange();
await controller.saveRecording();
Get.back();
},
), ),
),
Padding( SizedBox(height: 24.h),
padding: EdgeInsets.only(top: 30.w),
child: Row( // 按钮组
children: [ Row(
// 取消按钮 children: [
Expanded( Expanded(
child: TextButton( child: OutlinedButton(
onPressed: Get.back, // onPressed: Get.back,
style: TextButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: foregroundColor:
isDarkMode ? AppColors.primary : Colors.blue, isDarkMode ? Colors.white70 : Colors.grey[700],
padding: EdgeInsets.symmetric(vertical: 12.w), side: BorderSide(
shape: RoundedRectangleBorder( color: isDarkMode ? Colors.white30 : Colors.grey[400]!,
borderRadius: BorderRadius.circular(12.w), ),
side: BorderSide( padding: EdgeInsets.symmetric(vertical: 14.h),
color: shape: RoundedRectangleBorder(
isDarkMode ? AppColors.primary : Colors.blue, borderRadius: BorderRadius.circular(12.r),
width: 2.w,
),
),
),
child: Text('cancel'.tr),
), ),
), ),
child: Text('cancel'.tr),
SizedBox(width: 20.w), ),
),
// 确认按钮 SizedBox(width: 16.w),
Expanded( Expanded(
child: TextButton( child: ElevatedButton(
onPressed: () async { onPressed: () async {
await controller.saveTitleChange(); await controller.saveTitleChange();
await controller.saveRecording(); await controller.saveRecording();
Get.back(); Get.back();
}, },
style: TextButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: backgroundColor:
isDarkMode ? AppColors.primary : Colors.blue, isDarkMode ? AppColors.primary : Colors.blue,
foregroundColor: Colors.white, foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 12.w), padding: EdgeInsets.symmetric(vertical: 14.h),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.w), borderRadius: BorderRadius.circular(12.r),
),
),
child: Text('confirm'.tr),
), ),
elevation: 2,
), ),
], child: Text('confirm'.tr),
),
), ),
), ],
], ),
), ],
), ),
), ),
isScrollControlled: true, // 防止键盘遮挡 isScrollControlled: true,
backgroundColor: Colors.transparent, // 移除默认背景 backgroundColor: Colors.transparent,
isDismissible: true,
enableDrag: true,
); );
} }
} }
class WaveformPainter extends CustomPainter { /// 实时流动波形绘制器
final List<double> data; class RealTimeWaveformPainter extends CustomPainter {
final double cursorPosition; final List<double> waveData;
final bool isDarkMode;
final bool isRecording;
final double animationValue; // 动画值,用于流动效果
WaveformPainter(this.data, {required this.cursorPosition}); RealTimeWaveformPainter({
required this.waveData,
required this.isDarkMode,
required this.isRecording,
this.animationValue = 0.0,
});
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
final paint = Paint() final centerY = size.height / 2;
..color = Colors.blue[300]! final centerX = size.width / 2;
..strokeWidth = 2
..strokeCap = StrokeCap.round; // 绘制中央基准线
_drawCenterLine(canvas, size, centerX, centerY);
// 只在录音状态下且有波形数据时才绘制波形
if (isRecording && waveData.isNotEmpty) {
_drawWaveform(canvas, size, centerX, centerY);
}
}
final cursorPaint = Paint() /// 绘制中央时间参考线
void _drawCenterLine(
Canvas canvas, Size size, double centerX, double centerY) {
// 绘制水平中线
final horizontalLinePaint = Paint()
..color =
(isDarkMode ? Colors.white : Colors.black).withValues(alpha: 0.15)
..strokeWidth = 1.0;
canvas.drawLine(
Offset(0, centerY),
Offset(size.width, centerY),
horizontalLinePaint,
);
// 绘制垂直时间参考线(红色,更醒目)
final centerLinePaint = Paint()
..color = Colors.red.withValues(alpha: 0.8)
..strokeWidth = 2.0
..style = PaintingStyle.stroke;
canvas.drawLine(
Offset(centerX, centerY - size.height * 0.4),
Offset(centerX, centerY + size.height * 0.4),
centerLinePaint,
);
// 在参考线顶部绘制小三角形指示器
final trianglePath = Path();
final triangleSize = 4.0;
trianglePath.moveTo(centerX, centerY - size.height * 0.4);
trianglePath.lineTo(
centerX - triangleSize, centerY - size.height * 0.4 - triangleSize);
trianglePath.lineTo(
centerX + triangleSize, centerY - size.height * 0.4 - triangleSize);
trianglePath.close();
final trianglePaint = Paint()
..color = Colors.red ..color = Colors.red
..strokeWidth = 2; ..style = PaintingStyle.fill;
final double barWidth = size.width / (data.length * 1.2); // 增加条宽 canvas.drawPath(trianglePath, trianglePaint);
final centerY = size.height / 2; }
// 改为绘制矩形条,增加视觉冲击力 /// 绘制实时流动波形
for (int i = 0; i < data.length; i++) { void _drawWaveform(Canvas canvas, Size size, double centerX, double centerY) {
final x = i * barWidth; final barWidth = 2.5; // 波形条宽度
final height = min(data[i], 50); // 限制最大高度 final barSpacing = 0.5; // 波形条间距
final rect = Rect.fromLTWH( final totalBarWidth = barWidth + barSpacing;
x,
centerY - height / 2, // 计算中央参考线左侧可以显示多少个波形条
barWidth * 0.8, // 条宽稍小于间隔 final maxBarsOnLeft = (centerX / totalBarWidth).floor();
height.toDouble(),
); // 绘制左侧流动波形(历史数据,从右向左流动)
_drawLeftWaveform(
canvas, centerX, centerY, barWidth, totalBarWidth, maxBarsOnLeft);
}
/// 绘制左侧历史波形(从右向左流动)
void _drawLeftWaveform(Canvas canvas, double centerX, double centerY,
double barWidth, double totalBarWidth, int maxBars) {
final dataLength = waveData.length;
if (dataLength == 0) return;
// 计算流动偏移量
final flowOffset = animationValue * totalBarWidth;
// 绘制最近的波形条(最多显示maxBars个)
final displayCount = min(dataLength, maxBars);
for (int i = 0; i < displayCount; i++) {
// 计算波形条的x位置:从参考线开始,向左排列,加上流动偏移
final x = centerX - (i + 1) * totalBarWidth + flowOffset;
// 如果波形条完全移出屏幕左侧,跳过绘制
if (x + barWidth < 0) continue;
// 获取对应的音频数据(最新的数据在数组末尾)
final dataIndex = dataLength - 1 - i;
if (dataIndex < 0) continue;
// 添加渐变效果增强视觉 final amplitude = waveData[dataIndex];
final gradient = LinearGradient(
colors: [Colors.blue[400]!, Colors.blue[200]!], // 计算透明度:越远的波形条越透明
begin: Alignment.topCenter, final opacity = (1.0 - (i / displayCount) * 0.6).clamp(0.2, 1.0);
end: Alignment.bottomCenter,
_drawWaveBar(canvas, x, centerY, barWidth, amplitude, opacity);
}
}
/// 绘制单个上下对称的波形条
void _drawWaveBar(Canvas canvas, double x, double centerY, double barWidth,
double amplitude, double opacity) {
// 将振幅转换为显示高度(上下对称)
final maxHeight = 50.0; // 最大高度
final barHeight = (amplitude / 100.0) * maxHeight;
// 创建上下对称的圆角矩形
final rect = RRect.fromRectAndRadius(
Rect.fromCenter(
center: Offset(x + barWidth / 2, centerY),
width: barWidth,
height: barHeight * 2, // 上下对称,总高度是barHeight的2倍
),
Radius.circular(barWidth / 2),
);
// 创建渐变效果
final gradient = LinearGradient(
colors: _getGradientColors(opacity),
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
);
final paint = Paint()
..shader = gradient.createShader(rect.outerRect)
..style = PaintingStyle.fill;
canvas.drawRRect(rect, paint);
// 可选:添加高光效果(仅对较新的波形条)
if (opacity > 0.7) {
final highlightRect = RRect.fromRectAndRadius(
Rect.fromCenter(
center: Offset(x + barWidth / 2, centerY),
width: barWidth * 0.4,
height: barHeight * 2 * 0.8,
),
Radius.circular(barWidth / 4),
); );
final rectPaint = Paint()
..shader = gradient.createShader(rect) final highlightPaint = Paint()
..color = Colors.white.withValues(alpha: 0.2 * opacity)
..style = PaintingStyle.fill; ..style = PaintingStyle.fill;
canvas.drawRRect( canvas.drawRRect(highlightRect, highlightPaint);
RRect.fromRectAndRadius(rect, const Radius.circular(4)),
rectPaint,
);
} }
}
final cursorX = size.width * cursorPosition; /// 获取渐变颜色
canvas.drawLine( List<Color> _getGradientColors(double opacity) {
Offset(cursorX, 0), return isDarkMode
Offset(cursorX, size.height), ? [
cursorPaint, Colors.blue[400]!.withValues(alpha: opacity),
); Colors.blue[200]!.withValues(alpha: opacity * 0.7),
Colors.blue[100]!.withValues(alpha: opacity * 0.4),
]
: [
Colors.blue[500]!.withValues(alpha: opacity),
Colors.blue[300]!.withValues(alpha: opacity * 0.7),
Colors.blue[100]!.withValues(alpha: opacity * 0.4),
];
} }
@override @override
bool shouldRepaint(covariant WaveformPainter oldDelegate) { bool shouldRepaint(covariant RealTimeWaveformPainter oldDelegate) {
return oldDelegate.data != data || return oldDelegate.waveData != waveData ||
oldDelegate.cursorPosition != cursorPosition; oldDelegate.isDarkMode != isDarkMode ||
oldDelegate.isRecording != isRecording ||
oldDelegate.animationValue != animationValue;
}
}
/// 专门的波形Widget,正确管理GetX响应式状态
class _WaveformWidget extends StatelessWidget {
final MeetingRecordController controller;
final bool isDarkMode;
final Size size;
const _WaveformWidget({
required this.controller,
required this.isDarkMode,
required this.size,
});
@override
Widget build(BuildContext context) {
// 使用AnimatedBuilder监听动画变化
return AnimatedBuilder(
animation: controller.waveformAnimation,
builder: (context, child) {
// 使用Obx只监听需要响应式更新的数据
return Obx(() {
return CustomPaint(
painter: RealTimeWaveformPainter(
waveData: controller.waveData,
isDarkMode: isDarkMode,
isRecording: controller.isRecording.value,
animationValue: controller.waveformAnimation.value,
),
size: size,
);
});
},
);
} }
} }

9
lib/modules/permissions/bindings/permissions_binding.dart

@ -0,0 +1,9 @@
import 'package:get/get.dart';
import '../controllers/permissions_controller.dart';
class PermissionsBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<PermissionsController>(() => PermissionsController());
}
}

316
lib/modules/permissions/controllers/permissions_controller.dart

@ -0,0 +1,316 @@
import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:flutter/material.dart';
import 'dart:io' show Platform;
import 'package:device_info_plus/device_info_plus.dart';
import '../../../core/utils/permission_util.dart';
class PermissionsController extends GetxController {
// 权限状态映射
final RxMap<Permission, PermissionStatus> permissionStatuses =
<Permission, PermissionStatus>{}.obs;
// 是否正在请求权限
final RxBool isRequestingPermission = false.obs;
// Android版本信息(用于权限组显示判断)
final RxInt androidSdkVersion = 0.obs;
@override
void onInit() {
super.onInit();
_initAndroidVersion();
checkAllPermissions();
}
/// 初始化Android版本信息
Future<void> _initAndroidVersion() async {
if (Platform.isAndroid) {
try {
final deviceInfo = DeviceInfoPlugin();
final androidInfo = await deviceInfo.androidInfo;
androidSdkVersion.value = androidInfo.version.sdkInt;
} catch (e) {
print('获取Android版本信息失败: $e');
androidSdkVersion.value = 0;
}
}
}
/// 检查是否应该显示蓝牙权限组
bool shouldShowBluetoothPermissions() {
if (Platform.isAndroid) {
// Android 12+ (API 31+) 才显示蓝牙权限组
return androidSdkVersion.value >= 31;
} else if (Platform.isIOS) {
// iOS显示蓝牙权限组
return true;
}
return false;
}
/// 检查是否应该显示存储权限组
bool shouldShowStoragePermissions() {
if (Platform.isAndroid) {
// Android 10及以下版本 (API 29及以下) 才显示存储权限组
return androidSdkVersion.value <= 29 && androidSdkVersion.value > 0;
}
// iOS系统不显示存储权限组
return false;
}
/// 检查所有权限状态
Future<Map<Permission, PermissionStatus>> checkAllPermissions() async {
final Map<Permission, PermissionStatus> statuses = {};
// 根据平台获取需要检查的权限列表
final permissions = await _getPlatformPermissions();
for (final permission in permissions) {
try {
statuses[permission] = await permission.status;
print('Permission: $permission, Status: ${statuses[permission]}');
} catch (e) {
// 某些权限在特定平台可能不支持,设置为拒绝状态
statuses[permission] = PermissionStatus.denied;
}
}
permissionStatuses.value = statuses;
return statuses;
}
/// 获取蓝牙权限列表(根据Android版本)
Future<List<Permission>> _getBluetoothPermissions() async {
if (Platform.isAndroid) {
try {
final deviceInfo = DeviceInfoPlugin();
final androidInfo = await deviceInfo.androidInfo;
// Android 12+ (API 31+) 使用新的蓝牙权限
if (androidInfo.version.sdkInt >= 31) {
return [
Permission.bluetoothScan,
Permission.bluetoothConnect,
];
}
// Android 12以下不显示蓝牙权限(因为会自动授予)
return [];
} catch (e) {
print('获取Android版本信息失败: $e');
return [];
}
} else if (Platform.isIOS) {
// iOS使用基本蓝牙权限
return [Permission.bluetooth];
}
return [];
}
/// 获取平台特定的权限列表
Future<List<Permission>> _getPlatformPermissions() async {
final bluetoothPermissions = await _getBluetoothPermissions();
if (Platform.isIOS) {
final List<Permission> permissions = [
Permission.calendarFullAccess,
Permission.location,
Permission.contacts,
...bluetoothPermissions,
Permission.notification,
Permission.microphone,
];
// iOS系统不添加存储权限(根据shouldShowStoragePermissions方法)
if (shouldShowStoragePermissions()) {
permissions.add(Permission.storage);
}
return permissions;
} else if (Platform.isAndroid) {
final List<Permission> permissions = [
Permission.calendarFullAccess,
Permission.location,
Permission.phone,
Permission.sms,
Permission.contacts,
...bluetoothPermissions,
Permission.notification,
Permission.scheduleExactAlarm,
Permission.microphone,
];
// 只在Android 10及以下版本添加存储权限
if (shouldShowStoragePermissions()) {
permissions.add(Permission.storage);
}
return permissions;
}
return [];
}
/// 请求单个权限
Future<bool> requestPermission(Permission permission) async {
if (isRequestingPermission.value) return false;
isRequestingPermission.value = true;
try {
final permissionName = getPermissionName(permission);
final explanationText = _getDetailedDescription(permission);
final hasPermission = await PermissionUtil.instance.requestPermission(
permissionType: permission,
permissionName: permissionName,
explanationText: explanationText,
permanentDenialText:
'permanentDenialText'.tr.replaceAll('{permission}', permissionName),
settingsButtonText: 'goToSettings'.tr,
cancelButtonText: 'cancel'.tr,
);
if (hasPermission) {
_showSuccessMessage();
}
// 刷新权限状态
await checkAllPermissions();
return hasPermission;
} catch (e) {
_showErrorMessage();
return false;
} finally {
isRequestingPermission.value = false;
}
}
/// 打开系统设置
void openSettings() {
openAppSettings();
}
/// 获取权限的用户友好名称
String getPermissionName(Permission permission) {
switch (permission) {
case Permission.phone:
return 'phone'.tr;
case Permission.sms:
return 'sms'.tr;
case Permission.contacts:
return 'contacts'.tr;
case Permission.bluetooth:
return 'bluetooth'.tr;
case Permission.bluetoothScan:
return 'bluetoothScan'.tr;
case Permission.bluetoothConnect:
return 'bluetoothConnect'.tr;
case Permission.notification:
return 'notification'.tr;
case Permission.calendarWriteOnly:
return 'calendarWrite'.tr;
case Permission.calendarFullAccess:
return 'calendarFullAccess'.tr;
case Permission.location:
return 'locationInfo'.tr;
case Permission.scheduleExactAlarm:
return 'alarm'.tr;
case Permission.storage:
return 'storagePermission'.tr;
case Permission.microphone:
return 'microphonePermission'.tr;
default:
return permission.toString();
}
}
/// 获取权限说明文本
String _getDetailedDescription(Permission permission) {
switch (permission) {
case Permission.phone:
return 'phonePermissionDetailDesc'.tr;
case Permission.sms:
return 'smsPermissionDetailDesc'.tr;
case Permission.contacts:
return 'contactsPermissionDetailDesc'.tr;
case Permission.bluetooth:
return 'bluetoothPermissionDetailDesc'.tr;
case Permission.bluetoothScan:
return 'bluetoothScanPermissionDetailDesc'.tr;
case Permission.bluetoothConnect:
return 'bluetoothConnectPermissionDetailDesc'.tr;
case Permission.notification:
return 'notificationPermissionDetailDesc'.tr;
case Permission.calendarWriteOnly:
return 'calendarWritePermissionDetailDesc'.tr;
case Permission.calendarFullAccess:
return 'calendarFullAccessPermissionDetailDesc'.tr;
case Permission.location:
return 'locationPermissionDetailDesc'.tr;
case Permission.scheduleExactAlarm:
return 'alarmPermissionDetailDesc'.tr;
case Permission.storage:
return 'storagePermissionExplanation'.tr;
case Permission.microphone:
return 'microphonePermissionExplanation'.tr;
default:
return 'appNeedsPermissionForFullFunction'.tr;
}
}
/// 获取权限描述
String getPermissionDescription(Permission permission) {
switch (permission) {
case Permission.phone:
return 'phonePermissionDesc'.tr;
case Permission.sms:
return 'smsPermissionDesc'.tr;
case Permission.contacts:
return 'contactsPermissionDesc'.tr;
case Permission.bluetooth:
return 'bluetoothPermissionDesc'.tr;
case Permission.bluetoothScan:
return 'bluetoothScanPermissionDesc'.tr;
case Permission.bluetoothConnect:
return 'bluetoothConnectPermissionDesc'.tr;
case Permission.notification:
return 'notificationPermissionDesc'.tr;
case Permission.calendarWriteOnly:
return 'calendarWritePermissionDesc'.tr;
case Permission.calendarFullAccess:
return 'calendarFullAccessPermissionDesc'.tr;
case Permission.location:
return 'locationPermissionDesc'.tr;
case Permission.scheduleExactAlarm:
return 'alarmPermissionDesc'.tr;
case Permission.storage:
return 'storagePermissionExplanation'.tr;
case Permission.microphone:
return 'microphonePermissionExplanation'.tr;
default:
return 'appFunctionNeeds'.tr;
}
}
/// 显示成功消息
void _showSuccessMessage() {
Get.snackbar(
'success'.tr,
'permissionGranted'.tr,
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
}
/// 显示错误消息
void _showErrorMessage() {
Get.snackbar(
'error'.tr,
'permissionRequestError'.tr,
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}

67
lib/modules/permissions/models/permission_group.dart

@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
class PermissionGroup {
final String titleKey;
final IconData icon;
final List<Permission> permissions;
const PermissionGroup({
required this.titleKey,
required this.icon,
required this.permissions,
});
static List<PermissionGroup> getPlatformGroups() {
return [
// 通讯权限组 - 仅Android(包含联系人权限)
const PermissionGroup(
titleKey: 'communicationPermissions',
icon: Icons.phone_android,
permissions: [Permission.phone, Permission.sms, Permission.contacts],
),
// 蓝牙权限组 - Android 12+
const PermissionGroup(
titleKey: 'bluetoothPermissions',
icon: Icons.bluetooth,
permissions: [Permission.bluetoothScan, Permission.bluetoothConnect],
),
// 通知权限组
const PermissionGroup(
titleKey: 'notificationPermissions',
icon: Icons.notifications,
permissions: [Permission.notification],
),
// 闹钟权限组 - 仅Android
const PermissionGroup(
titleKey: 'alarmPermissions',
icon: Icons.alarm,
permissions: [Permission.scheduleExactAlarm],
),
// 日历权限组
const PermissionGroup(
titleKey: 'calendarPermissions',
icon: Icons.calendar_today,
permissions: [Permission.calendarFullAccess],
),
// 位置权限组
const PermissionGroup(
titleKey: 'locationPermissions',
icon: Icons.location_on,
permissions: [Permission.location],
),
// 存储权限组
const PermissionGroup(
titleKey: 'storagePermissions',
icon: Icons.storage,
permissions: [Permission.storage],
),
// 麦克风权限组
const PermissionGroup(
titleKey: 'microphonePermissions',
icon: Icons.mic,
permissions: [Permission.microphone],
),
];
}
}

283
lib/modules/permissions/views/permissions_view.dart

@ -0,0 +1,283 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'dart:ui';
import 'dart:io' show Platform;
import 'package:device_info_plus/device_info_plus.dart';
import '../controllers/permissions_controller.dart';
import '../models/permission_group.dart';
class PermissionsView extends GetView<PermissionsController> {
const PermissionsView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: AppBar(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
elevation: 0.5,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
size: 20.w,
color: isDarkMode ? Colors.white : Colors.black87,
),
onPressed: () => Get.back(),
),
title: Text(
'permissionManagement'.tr,
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
centerTitle: true,
),
body: Obx(() {
if (controller.permissionStatuses.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
return RefreshIndicator(
onRefresh: controller.checkAllPermissions,
child: ListView(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
children: _buildPermissionGroups(isDarkMode),
),
);
}),
);
}
List<Widget> _buildPermissionGroups(bool isDarkMode) {
final groups = PermissionGroup.getPlatformGroups();
final List<Widget> widgets = [];
for (int i = 0; i < groups.length; i++) {
final group = groups[i];
// 过滤平台特定权限
if (_shouldShowGroup(group)) {
widgets.add(_buildPermissionSection(
group.titleKey,
group.icon,
group.permissions,
controller.permissionStatuses,
isDarkMode,
));
if (i < groups.length - 1) {
widgets.add(SizedBox(height: 20.h));
}
}
}
return widgets;
}
bool _shouldShowGroup(PermissionGroup group) {
// Android特定权限组
if (group.titleKey == 'communicationPermissions' ||
group.titleKey == 'alarmPermissions') {
return Platform.isAndroid;
}
// 蓝牙权限组:Android 12+或iOS才显示
if (group.titleKey == 'bluetoothPermissions') {
return controller.shouldShowBluetoothPermissions();
}
// 存储权限组:Android 10及以下版本或iOS才显示
if (group.titleKey == 'storagePermissions') {
return controller.shouldShowStoragePermissions();
}
// 通知权限和麦克风权限在所有平台显示
if (group.titleKey == 'notificationPermissions' ||
group.titleKey == 'microphonePermissions') {
return true;
}
return true;
}
Widget _buildPermissionSection(
String titleKey,
IconData icon,
List<Permission> permissions,
Map<Permission, PermissionStatus> statuses,
bool isDarkMode,
) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 组标题
Padding(
padding: EdgeInsets.only(left: 4.w, bottom: 8.h),
child: Text(
titleKey.tr,
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.blue[400] : Colors.blue[600],
letterSpacing: 1,
),
),
),
// 权限项容器
Container(
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16.r),
child: BackdropFilter(
filter: isDarkMode
? ImageFilter.blur(sigmaX: 5, sigmaY: 5)
: ImageFilter.blur(sigmaX: 0, sigmaY: 0),
child: Column(
children: _buildPermissionItems(permissions, statuses, isDarkMode),
),
),
),
),
],
);
}
List<Widget> _buildPermissionItems(
List<Permission> permissions,
Map<Permission, PermissionStatus> statuses,
bool isDarkMode,
) {
final List<Widget> items = [];
for (int i = 0; i < permissions.length; i++) {
final permission = permissions[i];
final status = statuses[permission] ?? PermissionStatus.denied;
items.add(
_buildPermissionItem(
permission: permission,
status: status,
isDarkMode: isDarkMode,
),
);
// 添加分隔线,最后一个不加
if (i < permissions.length - 1) {
items.add(Divider(
height: 1,
color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[200],
));
}
}
return items;
}
Widget _buildPermissionItem({
required Permission permission,
required PermissionStatus status,
required bool isDarkMode,
}) {
return Padding(
padding: EdgeInsets.all(16.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
controller.getPermissionName(permission),
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
SizedBox(height: 4.h),
Text(
controller.getPermissionDescription(permission),
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.grey[600],
),
),
SizedBox(height: 4.h),
_buildStatusText(status, isDarkMode),
],
),
),
Obx(() => Switch(
value: status.isGranted,
onChanged: controller.isRequestingPermission.value
? null
: (value) => _handlePermissionToggle(permission, status, value),
activeColor: Colors.blue[600],
)),
],
),
);
}
Widget _buildStatusText(PermissionStatus status, bool isDarkMode) {
String text;
Color color;
if (status.isGranted) {
text = 'authorized'.tr;
color = Colors.green;
} else if (status.isDenied) {
text = 'unauthorized'.tr;
color = Colors.red;
} else if (status.isPermanentlyDenied) {
text = 'needOpenInSystemSettings'.tr;
color = Colors.orange;
} else if (status.isLimited) {
text = 'partialAuthorization'.tr;
color = Colors.orange;
} else {
text = 'unknownStatus'.tr;
color = Colors.grey;
}
return Text(
text,
style: TextStyle(
color: color,
fontSize: 12.sp,
fontWeight: FontWeight.w500,
),
);
}
void _handlePermissionToggle(
Permission permission,
PermissionStatus status,
bool value,
) {
if (value) {
// 请求权限
controller.requestPermission(permission);
} else if (status.isGranted) {
// 已授权但用户想关闭,引导到系统设置
controller.openSettings();
}
}
}

97
lib/modules/settings/controllers/settings_controller.dart

@ -12,7 +12,6 @@ import '../../../data/services/ble_manager.dart';
import '../../../data/services/language_manager.dart'; import '../../../data/services/language_manager.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:permission_handler/permission_handler.dart';
import 'dart:async'; import 'dart:async';
import '../../../data/services/version_update_service.dart'; import '../../../data/services/version_update_service.dart';
import '../../common/widgets/version_update_dialog.dart'; import '../../common/widgets/version_update_dialog.dart';
@ -129,103 +128,7 @@ class SettingsController extends GetxController {
bool isCheckingUpdate = false; //状态锁,防止重复检查 bool isCheckingUpdate = false; //状态锁,防止重复检查
DateTime? lastCheckTime; // 添加上次检查时间记录 DateTime? lastCheckTime; // 添加上次检查时间记录
// 权限相关
// 权限组,用于在UI中显示
final List<Map<String, dynamic>> permissionGroups = [
{
'name': '通讯权限',
'icon': Icons.phone_android,
'permissions': [
{'type': Permission.phone, 'name': '电话', 'description': '用于拨打电话功能'},
{'type': Permission.sms, 'name': '短信', 'description': '用于发送短信功能'},
{'type': Permission.contacts, 'name': '通讯录', 'description': '用于访问联系人'}
]
},
{
'name': '日程权限',
'icon': Icons.calendar_today,
'permissions': [
{
'type': Permission.calendarFullAccess,
'name': '日历写入',
'description': '用于创建日历事件和提醒'
},
{
'type': Permission.calendarFullAccess,
'name': '日历完全访问',
'description': '用于读取和修改日历事件'
}
]
},
{
'name': '存储权限',
'icon': Icons.storage,
'permissions': [
{'type': Permission.storage, 'name': '存储', 'description': '用于保存录音和文件'},
{'type': Permission.photos, 'name': '照片', 'description': '用于选择和保存照片'}
]
},
{
'name': '位置权限',
'icon': Icons.location_on,
'permissions': [
{
'type': Permission.location,
'name': '位置',
'description': '用于获取当前位置信息'
},
{
'type': Permission.locationAlways,
'name': '后台位置',
'description': '用于后台获取位置信息'
}
]
},
{
'name': '媒体权限',
'icon': Icons.mic,
'permissions': [
{
'type': Permission.microphone,
'name': '麦克风',
'description': '用于语音识别和录音'
},
{'type': Permission.bluetooth, 'name': '蓝牙', 'description': '用于连接蓝牙设备'}
]
},
{
'name': '闹钟权限',
'icon': Icons.alarm,
'permissions': [
{
'type': Permission.scheduleExactAlarm,
'name': '闹钟',
'description': '用于设置精确闹钟'
}
]
},
];
// 检查单个权限状态
// 打开应用设置
void openSettings() {
openAppSettings();
}
// 检查所有权限状态并返回结果
Future<Map<Permission, PermissionStatus>> checkAllPermissions() async {
Map<Permission, PermissionStatus> statuses = {};
for (var group in permissionGroups) {
for (var perm in group['permissions']) {
Permission permission = perm['type'];
statuses[permission] = await permission.status;
}
}
return statuses;
}
@override @override
void onInit() { void onInit() {

402
lib/modules/settings/views/permissions_view.dart

@ -1,402 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart';
import '../controllers/settings_controller.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'dart:ui';
import 'dart:io' show Platform;
import '../../../core/utils/permission_util.dart'; // 引入PermissionUtil
class PermissionsView extends GetView<SettingsController> {
const PermissionsView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: AppBar(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
elevation: 0.5,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
size: 20.w,
color: isDarkMode ? Colors.white : Colors.black87,
),
onPressed: () => Get.back(),
),
title: Text(
'permissionManagement'.tr, // 权限管理
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
centerTitle: true,
),
body: FutureBuilder<Map<Permission, PermissionStatus>>(
future: controller.checkAllPermissions(), //检查权限当前状态
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(
child: Text(
'${'getPermissionStatusFailed'.tr}: ${snapshot.error}')); // 获取权限状态失败
}
final permissionStatuses = snapshot.data ?? {};
return ListView(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
children: [
// 通讯权限组- iOS平台没有通讯权限
if (Platform.isAndroid) ...[
_buildPermissionSection(
'communicationPermissions'.tr, // 通讯权限
Icons.phone_android,
[Permission.phone, Permission.sms],
permissionStatuses,
isDarkMode),
SizedBox(height: 20.h),
],
// 闹钟权限组
if (Platform.isAndroid) ...[
_buildPermissionSection(
'alarmPermissions'.tr, // 闹钟权限
Icons.alarm,
[Permission.scheduleExactAlarm],
permissionStatuses,
isDarkMode,
),
SizedBox(height: 20.h),
],
// 日历权限组
_buildPermissionSection(
'calendarPermissions'.tr, // 日历权限
Icons.calendar_today,
[Permission.calendarFullAccess],
permissionStatuses,
isDarkMode),
SizedBox(height: 20.h),
// 位置权限组
_buildPermissionSection(
'locationPermissions'.tr, // 位置权限
Icons.location_on,
[Permission.location],
permissionStatuses,
isDarkMode),
],
);
},
),
);
}
// 构建权限组区域
Widget _buildPermissionSection(
String title,
IconData icon,
List<Permission> permissions,
Map<Permission, PermissionStatus> statuses,
bool isDarkMode) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 组标题
Padding(
padding: EdgeInsets.only(left: 4.w, bottom: 8.h),
child: Text(
title.tr,
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.blue[400] : Colors.blue[600],
letterSpacing: 1,
),
),
),
// 权限项容器
Container(
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16.r),
child: BackdropFilter(
filter: isDarkMode
? ImageFilter.blur(sigmaX: 5, sigmaY: 5)
: ImageFilter.blur(sigmaX: 0, sigmaY: 0),
child: Column(
children:
_buildPermissionItems(permissions, statuses, isDarkMode),
),
),
),
),
],
);
}
// 构建权限项列表
List<Widget> _buildPermissionItems(List<Permission> permissions,
Map<Permission, PermissionStatus> statuses, bool isDarkMode) {
final List<Widget> items = [];
// 获取名称
String getName(Permission permission) {
switch (permission) {
case Permission.phone:
return 'phone'.tr; // 电话
case Permission.sms:
return 'sms'.tr; // 短信
case Permission.calendarWriteOnly:
return 'calendarWrite'.tr; // 日历写入
case Permission.calendarFullAccess:
return 'calendarFullAccess'.tr; // 日历完整访问
case Permission.location:
return 'locationInfo'.tr; // 位置信息
case Permission.scheduleExactAlarm:
return 'alarm'.tr; // 闹钟
default:
return permission.toString();
}
}
// 获取描述文本
String getDescription(Permission permission) {
switch (permission) {
case Permission.phone:
return 'phonePermissionDesc'.tr; // 用于拨打电话功能
case Permission.sms:
return 'smsPermissionDesc'.tr; // 用于发送短信功能
case Permission.calendarWriteOnly:
return 'calendarWritePermissionDesc'.tr; // 用于创建日历事件和提醒
case Permission.calendarFullAccess:
return 'calendarFullAccessPermissionDesc'.tr; // 用于读取和管理日历事件及提醒
case Permission.location:
return 'locationPermissionDesc'.tr; // 用于获取当前位置信息
case Permission.scheduleExactAlarm:
return 'alarmPermissionDesc'.tr; // 设置精确闹钟需要此权限
default:
return 'appFunctionNeeds'.tr; // 应用功能需要
}
}
for (int i = 0; i < permissions.length; i++) {
final permission = permissions[i];
final status = statuses[permission] ?? PermissionStatus.denied;
items.add(
_buildPermissionItem(
name: getName(permission),
description: getDescription(permission),
status: status,
permission: permission,
isDarkMode: isDarkMode,
),
);
// 添加分隔线,最后一个不加
if (i < permissions.length - 1) {
items.add(Divider(
height: 1,
color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[200],
));
}
}
return items;
}
// 构建单个权限项
Widget _buildPermissionItem({
required String name,
required String description,
required PermissionStatus status,
required Permission permission,
required bool isDarkMode,
}) {
return Padding(
padding: EdgeInsets.all(16.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name.tr,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
SizedBox(height: 4.h),
Text(
description.tr,
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.grey[600],
),
),
SizedBox(height: 4.h),
_buildStatusText(status, isDarkMode),
],
),
),
Switch(
value: status.isGranted,
onChanged: (value) {
if (value) {
_handlePermissionAction(permission, status);
} else if (status.isGranted) {
controller.openSettings();
}
},
activeColor: Colors.blue[600],
),
],
),
);
}
// 状态文本
Widget _buildStatusText(PermissionStatus status, bool isDarkMode) {
String text;
Color color;
if (status.isGranted) {
text = 'authorized'.tr; // 已授权
color = Colors.green;
} else if (status.isDenied) {
text = 'unauthorized'.tr; // 未授权
color = Colors.red;
} else if (status.isPermanentlyDenied) {
text = 'needOpenInSystemSettings'.tr; // 需要在系统设置中开启
color = Colors.orange;
} else if (status.isLimited) {
text = 'partialAuthorization'.tr; // 部分授权
color = Colors.orange;
} else {
text = 'unknownStatus'.tr; // 未知状态
color = Colors.grey;
}
return Text(
text,
style: TextStyle(
color: color,
fontSize: 12.sp,
fontWeight: FontWeight.w500,
),
);
}
// 处理权限请求
Future<void> _handlePermissionAction(
Permission permission, PermissionStatus status) async {
try {
// final context = Get.context;
// if (context == null) return;
final permissionName = _getPermissionName(permission);
final explanationText = getDetailedDescription(permission);
// 使用 PermissionUtil 请求权限
final hasPermission = await PermissionUtil.instance.requestPermission(
// context: context,
permissionType: permission,
permissionName: permissionName,
explanationText: explanationText,
permanentDenialText: 'permanentDenialText'.tr.replaceAll(
'{permission}', permissionName), // 您已永久拒绝{permission}权限,请前往应用设置开启
settingsButtonText: 'goToSettings'.tr, // 去设置
cancelButtonText: 'cancel'.tr, // 取消
);
if (hasPermission) {
Get.snackbar(
'success'.tr, // 成功
'permissionGranted'.tr, // 权限已授予
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
}
// 刷新界面,重新获取权限状态
Get.forceAppUpdate();
} catch (e) {
Get.snackbar(
'error'.tr, // 错误
'permissionRequestError'.tr, // 请求权限时出错
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
// 获取权限的用户友好名称
String _getPermissionName(Permission permission) {
switch (permission) {
case Permission.phone:
return 'phone'.tr; // 电话
case Permission.sms:
return 'sms'.tr; // 短信
case Permission.calendarWriteOnly:
return 'calendarWrite'.tr; // 日历写入
case Permission.calendarFullAccess:
return 'calendarFullAccess'.tr; // 日历完整访问
case Permission.location:
return 'locationInfo'.tr; // 位置信息
case Permission.scheduleExactAlarm:
return 'alarm'.tr; // 闹钟
default:
return permission.toString();
}
}
// 获取权限说明文本
String getDetailedDescription(Permission permission) {
switch (permission) {
case Permission.phone:
return 'phonePermissionDetailDesc'.tr; // 拨打电话功能需要电话权限
case Permission.sms:
return 'smsPermissionDetailDesc'.tr; // 发送短信功能需要短信权限
case Permission.calendarWriteOnly:
return 'calendarWritePermissionDetailDesc'.tr; // 创建日历事件需要日历写入权限
case Permission.calendarFullAccess:
return 'calendarFullAccessPermissionDetailDesc'.tr; // 管理日历事件需要完整日历权限
case Permission.location:
return 'locationPermissionDetailDesc'.tr; // 获取位置信息需要位置权限
case Permission.scheduleExactAlarm:
return 'alarmPermissionDetailDesc'.tr; // 设置精确闹钟需要此权限
default:
return 'appNeedsPermissionForFullFunction'.tr; // 应用需要此权限以提供完整功能
}
}
}

111
lib/modules/splash/controllers/splash_controller.dart

@ -4,6 +4,8 @@ import 'package:app_tracking_transparency/app_tracking_transparency.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:http/http.dart' as http;
import '../../../data/models/appconfig.dart'; import '../../../data/models/appconfig.dart';
import '../../../data/models/appconfig_model.dart'; import '../../../data/models/appconfig_model.dart';
@ -44,14 +46,14 @@ class SplashController extends GetxController {
return; return;
} }
// 步骤 2: 并行执行所有启动任务,并等待它们全部完成 // 步骤 3: 并行执行所有启动任务,并等待它们全部完成
// Future.wait 会等待数组中所有的 Future 都完成后才继续 // Future.wait 会等待数组中所有的 Future 都完成后才继续
await Future.wait([ await Future.wait([
_startLoadingAnimation(), // 任务1: 最小动画时长 _startLoadingAnimation(), // 任务1: 最小动画时长
_performParallelTasks(), // 任务2: 其他所有后台任务 _performParallelTasks(), // 任务2: 其他所有后台任务
]); ]);
// 步骤 3: 所有任务(动画和后台)都已完成,触发导航 // 步骤 4: 所有任务(动画和后台)都已完成,触发导航
_canNavigate.value = true; _canNavigate.value = true;
} }
@ -81,6 +83,76 @@ class SplashController extends GetxController {
return completer.future; return completer.future;
} }
/// 网络连接检查
/// 网络连接检查 - 阻塞版本
Future<void> _checkNetworkStatus() async {
while (true) {
// 无限循环直到网络检查成功
try {
final connectivityResult = await Connectivity().checkConnectivity();
// 检查是否有网络连接
if (connectivityResult.contains(ConnectivityResult.none)) {
// 显示弹窗并等待用户操作
await _showNoNetworkDialogAndWait();
continue; // 重新检查
}
// 进一步检查网络连通性
final hasInternet = await _checkInternetConnection();
if (!hasInternet) {
// 显示弹窗并等待用户操作
await _showNoNetworkDialogAndWait();
continue; // 重新检查
}
// 网络检查成功,跳出循环
Logger.d(_tag, '网络检查成功,继续APP初始化流程');
break;
} catch (e) {
Logger.e(_tag, '网络检查失败: $e');
// 显示弹窗并等待用户操作
await _showNoNetworkDialogAndWait();
continue; // 重新检查
}
}
}
/// 检查实际的网络连通性
Future<bool> _checkInternetConnection() async {
final testUrls = [
'https://api.voitrans.net/ping', // 国内服
'https://hw.api.voitrans.net/ping', // 海外服
'https://www.baidu.com', // 百度
'https://www.google.com', // Google
];
for (final url in testUrls) {
try {
final response = await http.get(
Uri.parse(url.trim()), // 去除空格
headers: {'User-Agent': 'Voitrans-App'}, // 有助于后端识别
).timeout(const Duration(seconds: 5));
if (response.statusCode == 200) {
Logger.d(_tag, '网络连接检查成功: $url');
return true;
}
} on TimeoutException {
Logger.w(_tag, '网络连接超时: $url');
continue;
} on Exception catch (e) {
Logger.w(_tag, '网络连接失败: $url, 错误: $e');
continue;
}
}
Logger.e(_tag, '所有网络连接检查均失败');
return false;
}
/// 动画任务:返回一个在动画结束时完成的 Future /// 动画任务:返回一个在动画结束时完成的 Future
Future<void> _startLoadingAnimation() { Future<void> _startLoadingAnimation() {
const totalDuration = 1500; // 动画总时长1.5秒 const totalDuration = 1500; // 动画总时长1.5秒
@ -103,6 +175,8 @@ class SplashController extends GetxController {
/// 将所有并行的后台任务打包成一个 Future /// 将所有并行的后台任务打包成一个 Future
Future<void> _performParallelTasks() async { Future<void> _performParallelTasks() async {
//步骤2:检查网络连接状态
await _checkNetworkStatus();
// 同样使用 Future.wait,确保此函数在所有子任务完成后才结束 // 同样使用 Future.wait,确保此函数在所有子任务完成后才结束
await Future.wait([ await Future.wait([
_requestTrackingPermissionAsync(), _requestTrackingPermissionAsync(),
@ -211,3 +285,36 @@ class SplashController extends GetxController {
} }
} }
} }
/// 显示无网络连接对话框并等待用户操作(阻塞版本)
Future<void> _showNoNetworkDialogAndWait() async {
final completer = Completer<void>();
WidgetsBinding.instance.addPostFrameCallback((_) {
Get.dialog(
AlertDialog(
title: Row(
children: [
const Icon(Icons.wifi_off, color: Colors.orange),
const SizedBox(width: 8),
Text('networkError'.tr), // 无网络连接
],
),
content: Text('networkConnectionFailed'.tr), // 网络连接失败,请检查网络设置
actions: [
TextButton(
onPressed: () {
Get.back(); // 关闭弹窗
completer.complete(); // 完成Future,继续执行
},
child: Text('retry'.tr), // 重试
),
],
),
barrierDismissible: false, // 不允许点击外部关闭
);
});
// 等待用户点击重试按钮
return completer.future;
}

5
lib/routes/app_pages.dart

@ -50,8 +50,9 @@ import '../modules/pairing/views/pairing_view.dart';
import '../modules/pairing/bindings/pairing_binding.dart'; import '../modules/pairing/bindings/pairing_binding.dart';
import './app_routes.dart'; import './app_routes.dart';
import '../modules/settings/views/settings_view.dart'; import '../modules/settings/views/settings_view.dart';
import '../modules/settings/views/permissions_view.dart';
import '../modules/settings/bindings/settings_binding.dart'; import '../modules/settings/bindings/settings_binding.dart';
import '../modules/permissions/views/permissions_view.dart';
import '../modules/permissions/bindings/permissions_binding.dart';
import '../modules/speech_test/views/speech_test_view.dart'; import '../modules/speech_test/views/speech_test_view.dart';
import '../modules/speech_test/bindings/speech_test_binding.dart'; import '../modules/speech_test/bindings/speech_test_binding.dart';
import '../modules/agent/views/agent_view.dart'; import '../modules/agent/views/agent_view.dart';
@ -254,7 +255,7 @@ abstract class AppPages {
GetPage( GetPage(
name: Routes.permissions, name: Routes.permissions,
page: () => const PermissionsView(), page: () => const PermissionsView(),
binding: SettingsBinding(), binding: PermissionsBinding(),
transition: Transition.rightToLeft, transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250), transitionDuration: Duration(milliseconds: 250),
), ),

1
pubspec.yaml

@ -74,6 +74,7 @@ dependencies:
dio: ^5.8.0+1 dio: ^5.8.0+1
sqflite: ^2.4.2 sqflite: ^2.4.2
path: ^1.9.1 path: ^1.9.1
connectivity_plus: ^6.1.5 # 网络连接检测依赖
speech: speech:
path: local_plugins/speech path: local_plugins/speech
azure_speech: azure_speech:

3
windows/flutter/generated_plugin_registrant.cc

@ -6,6 +6,7 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <file_selector_windows/file_selector_windows.h> #include <file_selector_windows/file_selector_windows.h>
#include <firebase_auth/firebase_auth_plugin_c_api.h> #include <firebase_auth/firebase_auth_plugin_c_api.h>
#include <firebase_core/firebase_core_plugin_c_api.h> #include <firebase_core/firebase_core_plugin_c_api.h>
@ -18,6 +19,8 @@
#include <window_size/window_size_plugin.h> #include <window_size/window_size_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
FileSelectorWindowsRegisterWithRegistrar( FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows")); registry->GetRegistrarForPlugin("FileSelectorWindows"));
FirebaseAuthPluginCApiRegisterWithRegistrar( FirebaseAuthPluginCApiRegisterWithRegistrar(

1
windows/flutter/generated_plugins.cmake

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
connectivity_plus
file_selector_windows file_selector_windows
firebase_auth firebase_auth
firebase_core firebase_core

Loading…
Cancel
Save