diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ceba71e9f..7d244c60a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -28,6 +28,10 @@ + + + + diff --git a/lib/core/translations/language/en_us.dart b/lib/core/translations/language/en_us.dart index 45071ebdd..1ea125f22 100644 --- a/lib/core/translations/language/en_us.dart +++ b/lib/core/translations/language/en_us.dart @@ -487,11 +487,18 @@ const Map enUS = { // 权限管理页面 'getPermissionStatusFailed': 'Failed to get permission status', 'communicationPermissions': 'Communication Permissions', + 'bluetoothPermissions': 'Bluetooth Permissions', + 'notificationPermissions': 'Notification Permissions', + 'storagePermissions': 'Storage Permissions', + 'microphonePermissions': 'Microphone Permissions', 'alarmPermissions': 'Alarm Permissions', 'calendarPermissions': 'Calendar Permissions', 'locationPermissions': 'Location Permissions', 'phone': 'Phone', 'sms': 'SMS', + 'contacts': 'Contacts', + //'bluetooth': 'Bluetooth', + 'notification': 'Notification', 'alarm': 'Alarm', 'calendarWrite': 'Calendar Write', 'calendarFullAccess': 'Calendar Full Access', @@ -521,6 +528,19 @@ const Map enUS = { 'contactsPermissionDesc': 'Used for accessing contact information', 'contactsPermissionDetailDesc': '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': 'Creating calendar events requires calendar write permission', 'calendarFullAccessPermissionDetailDesc': diff --git a/lib/core/translations/language/zh_cn.dart b/lib/core/translations/language/zh_cn.dart index 986e8cd14..f52ebd4ab 100644 --- a/lib/core/translations/language/zh_cn.dart +++ b/lib/core/translations/language/zh_cn.dart @@ -455,11 +455,18 @@ const Map zhCN = { // 权限管理页面 'getPermissionStatusFailed': '获取权限状态失败', 'communicationPermissions': '通讯权限', + 'bluetoothPermissions': '蓝牙权限', + 'notificationPermissions': '通知权限', + 'storagePermissions': '存储权限', + 'microphonePermissions': '麦克风权限', 'alarmPermissions': '闹钟权限', 'calendarPermissions': '日历权限', 'locationPermissions': '位置权限', 'phone': '电话', 'sms': '短信', + 'contacts': '联系人', + //'bluetooth': '蓝牙', + 'notification': '通知', 'alarm': '闹钟', 'calendarWrite': '日历写入', 'calendarFullAccess': '日历完整访问', @@ -486,6 +493,14 @@ const Map zhCN = { 'locationPermissionDetailDesc': '获取位置信息需要位置权限', 'contactsPermissionDesc': '用于访问联系人信息', 'contactsPermissionDetailDesc': '拨打电话和发送短信功能需要联系人权限以选择联系人', + 'bluetoothPermissionDesc': '用于连接和管理蓝牙设备', + 'bluetoothPermissionDetailDesc': '连接蓝牙耳机等设备需要蓝牙权限', + 'bluetoothScanPermissionDesc': '用于扫描附近的蓝牙设备', + 'bluetoothScanPermissionDetailDesc': '扫描附近蓝牙设备需要蓝牙扫描权限', + 'bluetoothConnectPermissionDesc': '用于连接蓝牙设备', + 'bluetoothConnectPermissionDetailDesc': '连接蓝牙设备需要蓝牙连接权限', + 'notificationPermissionDesc': '用于发送通知消息', + 'notificationPermissionDetailDesc': '应用需要通知权限以向您发送重要提醒和消息', 'appNeedsPermissionForFullFunction': '应用需要此权限以提供完整功能', 'permanentDenialText': '您已永久拒绝{permission}权限,请前往应用设置开启', 'goToSettings': '去设置', diff --git a/lib/data/services/music_manager.dart b/lib/data/services/music_manager.dart index e761e1a37..4fb1b7343 100644 --- a/lib/data/services/music_manager.dart +++ b/lib/data/services/music_manager.dart @@ -22,7 +22,7 @@ class MusicManager extends GetxService { // Logger.error('无法获取 BuildContext'); // return; // } - requestNotificationPermission(); + //requestNotificationPermission(); Logger.info('初始化权限请求'); } diff --git a/lib/data/services/novice_guide_service.dart b/lib/data/services/novice_guide_service.dart index c58b1a312..be154d551 100644 --- a/lib/data/services/novice_guide_service.dart +++ b/lib/data/services/novice_guide_service.dart @@ -148,7 +148,7 @@ class NoviceGuideService extends GetxService { void startAIGuide() async { if (!isAIGuide) { - await Future.delayed(const Duration(milliseconds: 200)); + await Future.delayed(const Duration(milliseconds: 300)); isAIGuide = true; _storage.write('ai_guide', true); Intro.of(Get.context!).start(group: 'ai'); diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 009bad72d..e786100ac 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -1060,20 +1060,20 @@ class AgentController extends GetxController { Future startVoiceInput() async { if (isListening.value) return; Logger.i(TAG, '开始语音输入'); // 开始语音输入 - final hasPermission = await PermissionUtil.instance.requestPermission( - // context: Get.context!, - permissionType: Permission.microphone, - permissionName: 'microphonePermission'.tr, // 麦克风 - explanationText: 'voiceTranslationPermissionText'.tr, // 语音识别与翻译功能需要访问麦克风。 - permanentDenialText: - 'permissionDeniedText'.tr, // 您已永久拒绝麦克风权限,请前往系统设置开启权限。 - settingsButtonText: 'goToSettings'.tr, // 去设置 - cancelButtonText: 'cancel'.tr, // 取消 - ); - if (!hasPermission) { - isTextInputMode.value = true; // 如果没有权限,切回文本模式 - return; - } + // final hasPermission = await PermissionUtil.instance.requestPermission( + // // context: Get.context!, + // permissionType: Permission.microphone, + // permissionName: 'microphonePermission'.tr, // 麦克风 + // explanationText: 'voiceTranslationPermissionText'.tr, // 语音识别与翻译功能需要访问麦克风。 + // permanentDenialText: + // 'permissionDeniedText'.tr, // 您已永久拒绝麦克风权限,请前往系统设置开启权限。 + // settingsButtonText: 'goToSettings'.tr, // 去设置 + // cancelButtonText: 'cancel'.tr, // 取消 + // ); + // if (!hasPermission) { + // isTextInputMode.value = true; // 如果没有权限,切回文本模式 + // return; + // } try { await AgentService.recognizeCallback(); diff --git a/lib/modules/meeting/controllers/meeting_home_controller.dart b/lib/modules/meeting/controllers/meeting_home_controller.dart index 1b5af2369..e994d5025 100644 --- a/lib/modules/meeting/controllers/meeting_home_controller.dart +++ b/lib/modules/meeting/controllers/meeting_home_controller.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; @@ -88,6 +89,24 @@ class MeetingHomeController extends GetxController { // 请求存储权限 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 .requestPermission( permissionType: Permission.storage, diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart index 11177bb1b..b30651e28 100644 --- a/lib/modules/meeting/controllers/meeting_record_controller.dart +++ b/lib/modules/meeting/controllers/meeting_record_controller.dart @@ -23,7 +23,7 @@ enum TimerStatus { /// Controller for meeting recording screen /// Handles recording state, audio processing, transcription, and UI interactions class MeetingRecordController extends GetxController - with SingleGetTickerProviderMixin { + with GetSingleTickerProviderStateMixin { // Services final AsrService _asrService = Get.find(); final BleManager _bleManager = Get.find(); @@ -57,8 +57,7 @@ class MeetingRecordController extends GetxController Timer? _timer; int _elapsedMilliseconds = 0; int? _lastStartTime; - bool _audioSourceType = false; - late TabController tabController; + // 移除了未使用的变量和TabController StreamSubscription? _audioDataSubscription; // 添加波形放大倍数变量 @@ -67,6 +66,10 @@ class MeetingRecordController extends GetxController final RxInt _currentPosition = 0.obs; // 原始位置索引 int maxPoints = 500; // 最大显示点数(根据屏幕宽度优化) int sampleRate = 16000; // 假设音频采样率(根据实际情况调整) + + // 波形流动动画控制器 + late AnimationController waveformAnimationController; + late Animation waveformAnimation; // 设置最大点数(根据屏幕宽度) void setMaxPoints(double screenWidth) { maxPoints = screenWidth ~/ 2; // 每2像素一个数据点 @@ -77,12 +80,14 @@ class MeetingRecordController extends GetxController super.onInit(); _initializeComponents(); _setupFromArguments(); - generateWaveData(); + _initializeWaveformAnimation(); + //generateWaveData(); } @override void onClose() { _cleanupResources(); + waveformAnimationController.dispose(); super.onClose(); } @@ -111,12 +116,36 @@ class MeetingRecordController extends GetxController /// Initializes UI controllers and focus nodes void _initializeComponents() { - tabController = TabController(length: 1, vsync: this); titleEditingController = TextEditingController(text: fileName.value); titleFocusNode = FocusNode(); titleFocusNode.addListener(_handleTitleFocusChange); } + /// 初始化波形流动动画 + void _initializeWaveformAnimation() { + // 设置动画持续时间,控制流动速度 + waveformAnimationController = AnimationController( + duration: const Duration(milliseconds: 100), // 更快的流动速度 + vsync: this, + ); + + waveformAnimation = Tween( + 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 void _setupFromArguments() { final Map args = Get.arguments ?? {}; @@ -126,15 +155,12 @@ class MeetingRecordController extends GetxController switch (audioType.value) { case 0: fileName.value = "现场录音"; - _audioSourceType = false; break; case 1: fileName.value = "音、视频录音"; - _audioSourceType = true; break; case 2: fileName.value = "通话录音"; - _audioSourceType = true; break; } newName.value = fileName.value; @@ -145,6 +171,8 @@ class MeetingRecordController extends GetxController _timer?.cancel(); _recognitionSubscription?.cancel(); _recognitionSubscription = null; + _audioDataSubscription?.cancel(); + _audioDataSubscription = null; titleEditingController.dispose(); titleFocusNode.dispose(); if (audioType.value != 0) { @@ -242,7 +270,7 @@ class MeetingRecordController extends GetxController // 1. 实时音频可视化 // 2. 音频质量检测 // 3. 发送到其他服务 - print('接收到音频数据: ${audioData.length} 字节'); + Logger.d("AudioData", "接收到音频数据: ${audioData.length} 字节"); // 处理音频数据的逻辑 _processAudioData(audioData); @@ -254,6 +282,9 @@ class MeetingRecordController extends GetxController newName.value = fullFileName; titleEditingController.text = fullFileName; + // 启动波形动画 + _startWaveformAnimation(); + // Start hardware components _startHardwareServices(); startTimer(); @@ -279,31 +310,17 @@ class MeetingRecordController extends GetxController /// Pauses current recording session Future _pauseRecording() async { await _asrService.pauseRecord(); + _stopWaveformAnimation(); // 暂停时停止动画 pauseTimer(); } /// Resumes paused recording session Future _resumeRecording() async { await _asrService.resumeRecord(); - + _startWaveformAnimation(); // 恢复时启动动画 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音频数据 void _processAudioData(Uint8List audioData) { // 将Uint8List转换为Float32List(PCM16 -> 浮点) @@ -360,70 +377,55 @@ class MeetingRecordController extends GetxController return _currentPosition.value / waveData.length; } - /// 添加处理后的音频数据 + /// 添加处理后的音频数据 - 优化流动效果 void _addAudioData(Float32List newAudio) { - // 1. 降采样:只保留部分数据点 - final downsampled = _downsampleAudio(newAudio); + if (!isRecording.value) return; - waveData.addAll(downsampled); + // 1. 降采样处理 + final downsampled = _downsampleAudio(newAudio); + if (downsampled.isEmpty) return; - // 3. 更新光标位置到最新点 - _currentPosition.value = waveData.length; + // 2. 逐个添加数据点,确保平滑流动 + for (final sample in downsampled) { + waveData.add(sample); - // 4. 限制数据长度,移除旧数据 - if (waveData.length > maxPoints * 2) { - waveData.removeRange(0, waveData.length - maxPoints); - _currentPosition.value = waveData.length; + //维护合理的数据长度 - 保持滑动窗口效果 + if (waveData.length > maxPoints) { + waveData.removeAt(0); // 移除最旧的数据 + } } + + // 3. 确保动画持续运行 + _ensureAnimationRunning(); + + // 4. 更新位置信息 + _currentPosition.value = waveData.length; } - /// Formats transcript with speaker identification - String _formatTranscript(RecognitionEvent event) { - final buffer = StringBuffer(); - - 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}'); + /// 启动波形流动动画 + void _startWaveformAnimation() { + if (isRecording.value && !waveformAnimationController.isAnimating) { + waveformAnimationController.forward(); } + } - return buffer.toString(); + /// 停止波形动画 + void _stopWaveformAnimation() { + waveformAnimationController.stop(); + waveformAnimationController.reset(); } - /// Checks if character is Chinese/English punctuation - bool _isPunctuation(String char) { - const punctuations = { - ',', - '。', - '!', - '?', - ';', - ':', - ',', - '.', - ':', - ';', - '!', - '?' - }; - return punctuations.contains(char); + /// 确保动画持续运行 + void _ensureAnimationRunning() { + if (isRecording.value && !waveformAnimationController.isAnimating) { + waveformAnimationController.forward(); + } } /// Saves recording and cleans up resources Future saveRecording() async { resetTimer(); + _stopWaveformAnimation(); // 停止波形动画 if (audioType.value != 0) { _bleManager.closeCodec(); } @@ -436,11 +438,11 @@ class MeetingRecordController extends GetxController 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); + 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", + "${newDir.path}${fileName.value}.wav", ); } else { await _asrService.renameFile( diff --git a/lib/modules/meeting/views/meeting_record_view.dart b/lib/modules/meeting/views/meeting_record_view.dart index 4de8a3320..3784e9673 100644 --- a/lib/modules/meeting/views/meeting_record_view.dart +++ b/lib/modules/meeting/views/meeting_record_view.dart @@ -3,7 +3,6 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; -import 'package:lottie/lottie.dart'; import '../controllers/meeting_record_controller.dart'; import '../../../core/theme/app_colors.dart'; @@ -14,17 +13,22 @@ class MeetingRecordView extends GetView { Widget build(BuildContext context) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; - return WillPopScope( - onWillPop: () async { - // 在返回时执行保存操作 - controller.saveTitleChange(); - await controller.saveRecording(); - return true; // 允许退出 + return PopScope( + onPopInvokedWithResult: (didPop, result) async { + if (didPop) { + // 在返回时执行保存操作 + controller.saveTitleChange(); + //await controller.saveRecording(); + } }, child: Scaffold( appBar: AppBar( 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 { // 在返回按钮点击时执行保存操作 controller.saveTitleChange(); @@ -48,7 +52,7 @@ class MeetingRecordView extends GetView { isDense: true, contentPadding: EdgeInsets.zero, border: InputBorder.none, - hintText: '输入文件名', + hintText: 'enterFileName'.tr, hintStyle: TextStyle( color: isDarkMode ? Colors.white70 : Colors.black54, ), @@ -88,24 +92,9 @@ class MeetingRecordView extends GetView { ), body: Column( children: [ - TabBar( - controller: controller.tabController, - indicatorColor: Colors.red, - labelColor: isDarkMode ? Colors.white : Colors.black, - unselectedLabelColor: Colors.grey, - tabs: const [ - Tab(text: '音频'), - // Tab(text: '文本'), - ], - ), + // 简化为单一音频界面,移除不必要的TabBar Expanded( - child: TabBarView( - controller: controller.tabController, - children: [ - _buildAudioTab(context, isDarkMode), - // _buildTextTab(context, isDarkMode), - ], - ), + child: _buildAudioTab(context, isDarkMode), ), ], ), @@ -114,339 +103,574 @@ class MeetingRecordView extends GetView { } Widget _buildAudioTab(BuildContext context, bool isDarkMode) { - return Obx(() { - final screenWidth = MediaQuery.of(context).size.width; - // 初始化最大点数 - if (controller.maxPoints == 0) { - controller.setMaxPoints(screenWidth); - } - final cursorPos = controller.cursorPositionValue; - return Column( - 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), - ], - ); - }); - } + return LayoutBuilder( + builder: (context, constraints) { + // 使用LayoutBuilder获取准确的屏幕宽度 + final screenWidth = constraints.maxWidth; + + // 初始化最大点数 + if (controller.maxPoints == 0) { + controller.setMaxPoints(screenWidth); + } - Widget _buildTextTab(BuildContext context, bool isDarkMode) { - return Obx(() => Column( + return Column( children: [ - const SizedBox(height: 48), - Center( - child: Column( - children: [ - const Icon(Icons.graphic_eq, color: Colors.red, size: 32), - const SizedBox(height: 8), - Text( - controller.formatTime(controller.seconds.value), - style: const TextStyle( - fontSize: 36, fontWeight: FontWeight.bold), + SizedBox(height: 24.h), + // 专业级实时流动波形显示区域 + Container( + height: 120.h, + margin: EdgeInsets.symmetric(horizontal: 16.w), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + 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), ), ], ), - ), - const SizedBox(height: 24), - // 新增:转文本提示区域 - if (!controller.showTextContent.value) ...[ - const Text('无文本', - style: TextStyle( - color: Colors.grey, - fontSize: 16, - )), - 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), + child: ClipRRect( + borderRadius: BorderRadius.circular(12.r), + child: Stack( + children: [ + // 实时流动波形绘制组件 + _WaveformWidget( + controller: controller, + isDarkMode: isDarkMode, + size: Size(screenWidth - 32.w, 120.h), ), - ), + + // 录音状态指示器 + 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(), _buildBottomBar(context, isDarkMode), - const SizedBox(height: 32), + SizedBox(height: 32.h), ], - )); + ); + }, + ); } // 底部操作栏 Widget _buildBottomBar(BuildContext context, bool isDarkMode) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - // 收藏按钮 - GestureDetector( - onTap: controller.toggleMarked, - child: Obx(() => CircleAvatar( - radius: 32, - backgroundColor: Colors.white, - child: Icon( + return Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // 收藏按钮 + _buildActionButton( + onTap: controller.toggleMarked, + child: Obx(() => Icon( controller.isMarked.value ? Icons.star : Icons.star_border, - size: 36, + size: 36.sp, color: controller.isMarked.value ? Colors.orange : Colors.grey, - ), - )), - ), - // 录音按钮 - GestureDetector( - onTap: controller.toggleRecording, - child: Obx(() => CircleAvatar( - radius: 40, - backgroundColor: Colors.white, - child: Icon( + )), + radius: 32.w, + tooltip: controller.isMarked.value + ? 'unmarkFavorite'.tr + : 'markFavorite'.tr, + ), + // 录音按钮 + _buildActionButton( + onTap: controller.toggleRecording, + child: Obx(() => Icon( controller.timerStatus.value == TimerStatus.running ? Icons.pause : Icons.play_arrow, - size: 44, + size: 44.sp, color: Colors.red, - ), - )), - ), - // 完成按钮 - GestureDetector( - onTap: () => _bottomSheet(context, isDarkMode), // 改为匿名函数 - child: const CircleAvatar( - radius: 32, - backgroundColor: Colors.white, + )), + radius: 40.w, + tooltip: controller.timerStatus.value == TimerStatus.running + ? 'pauseRecording'.tr + : 'startRecording'.tr, + ), + // 完成按钮 + _buildActionButton( + onTap: () => _bottomSheet(context, isDarkMode), child: Icon( Icons.check, - size: 36, + size: 36.sp, 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) { Get.bottomSheet( - Padding( - padding: MediaQuery.of(context).viewInsets, - child: Container( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 20.w), - decoration: BoxDecoration( - color: isDarkMode ? AppColors.darkBackground : Colors.grey[50], - borderRadius: BorderRadius.vertical(top: Radius.circular(20.w)), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // 标题 - Padding( - padding: EdgeInsets.only(bottom: 20.w), - child: Text( - 'editMeetingTitle'.tr, - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black87, - fontSize: 18.sp, - fontWeight: FontWeight.bold, - ), + Container( + padding: EdgeInsets.only( + left: 20.w, + right: 20.w, + top: 20.w, + bottom: MediaQuery.of(context).viewInsets.bottom + 20.w, + ), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.darkBackground : Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20.r)), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 10, + offset: const Offset(0, -2), + ), + ], + ), + child: Column( + 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, + ), + ), - // 输入框 - Container( - decoration: BoxDecoration( - color: isDarkMode ? Colors.white10 : Colors.white, - borderRadius: BorderRadius.circular(12.w), + SizedBox(height: 16.h), + + // 输入框 + Container( + 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, - autofocus: true, // 自动弹出键盘 - maxLength: 50, // 添加字符限制 - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black87, - fontSize: 16.sp, - ), - decoration: InputDecoration( - hintText: 'maxCharacters'.tr, - hintStyle: TextStyle( - color: isDarkMode ? Colors.white60 : Colors.black45, - ), - border: InputBorder.none, - contentPadding: EdgeInsets.all(16.w), - counterText: '', // 隐藏默认计数器 + ), + child: TextField( + controller: controller.titleEditingController, + autofocus: true, + maxLength: 50, + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 16.sp, + ), + decoration: InputDecoration( + hintText: 'enterFileName'.tr, + hintStyle: TextStyle( + color: isDarkMode ? Colors.white60 : Colors.grey[600], ), + border: InputBorder.none, + contentPadding: EdgeInsets.all(16.w), + counterText: '', ), + textInputAction: TextInputAction.done, + onSubmitted: (_) async { + await controller.saveTitleChange(); + await controller.saveRecording(); + Get.back(); + }, ), + ), - Padding( - padding: EdgeInsets.only(top: 30.w), - child: Row( - children: [ - // 取消按钮 - Expanded( - child: TextButton( - onPressed: Get.back, // - style: TextButton.styleFrom( - foregroundColor: - isDarkMode ? AppColors.primary : Colors.blue, - padding: EdgeInsets.symmetric(vertical: 12.w), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.w), - side: BorderSide( - color: - isDarkMode ? AppColors.primary : Colors.blue, - width: 2.w, - ), - ), - ), - child: Text('cancel'.tr), + SizedBox(height: 24.h), + + // 按钮组 + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: Get.back, + style: OutlinedButton.styleFrom( + foregroundColor: + isDarkMode ? Colors.white70 : Colors.grey[700], + side: BorderSide( + color: isDarkMode ? Colors.white30 : Colors.grey[400]!, + ), + padding: EdgeInsets.symmetric(vertical: 14.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), ), ), - - SizedBox(width: 20.w), - - // 确认按钮 - Expanded( - child: TextButton( - onPressed: () async { - await controller.saveTitleChange(); - await controller.saveRecording(); - Get.back(); - }, - style: TextButton.styleFrom( - backgroundColor: - isDarkMode ? AppColors.primary : Colors.blue, - foregroundColor: Colors.white, - padding: EdgeInsets.symmetric(vertical: 12.w), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.w), - ), - ), - child: Text('confirm'.tr), + child: Text('cancel'.tr), + ), + ), + SizedBox(width: 16.w), + Expanded( + child: ElevatedButton( + onPressed: () async { + await controller.saveTitleChange(); + await controller.saveRecording(); + Get.back(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: + isDarkMode ? AppColors.primary : Colors.blue, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(vertical: 14.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), ), + elevation: 2, ), - ], + child: Text('confirm'.tr), + ), ), - ), - ], - ), + ], + ), + ], ), ), - isScrollControlled: true, // 防止键盘遮挡 - backgroundColor: Colors.transparent, // 移除默认背景 + isScrollControlled: true, + backgroundColor: Colors.transparent, + isDismissible: true, + enableDrag: true, ); } } -class WaveformPainter extends CustomPainter { - final List data; - final double cursorPosition; +/// 实时流动波形绘制器 +class RealTimeWaveformPainter extends CustomPainter { + final List 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 void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = Colors.blue[300]! - ..strokeWidth = 2 - ..strokeCap = StrokeCap.round; + final centerY = size.height / 2; + final centerX = size.width / 2; + + // 绘制中央基准线 + _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 - ..strokeWidth = 2; + ..style = PaintingStyle.fill; - final double barWidth = size.width / (data.length * 1.2); // 增加条宽 - final centerY = size.height / 2; + canvas.drawPath(trianglePath, trianglePaint); + } - // 改为绘制矩形条,增加视觉冲击力 - for (int i = 0; i < data.length; i++) { - final x = i * barWidth; - final height = min(data[i], 50); // 限制最大高度 - final rect = Rect.fromLTWH( - x, - centerY - height / 2, - barWidth * 0.8, // 条宽稍小于间隔 - height.toDouble(), - ); + /// 绘制实时流动波形 + void _drawWaveform(Canvas canvas, Size size, double centerX, double centerY) { + final barWidth = 2.5; // 波形条宽度 + final barSpacing = 0.5; // 波形条间距 + final totalBarWidth = barWidth + barSpacing; + + // 计算中央参考线左侧可以显示多少个波形条 + final maxBarsOnLeft = (centerX / totalBarWidth).floor(); + + // 绘制左侧流动波形(历史数据,从右向左流动) + _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 gradient = LinearGradient( - colors: [Colors.blue[400]!, Colors.blue[200]!], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, + final amplitude = waveData[dataIndex]; + + // 计算透明度:越远的波形条越透明 + final opacity = (1.0 - (i / displayCount) * 0.6).clamp(0.2, 1.0); + + _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; - canvas.drawRRect( - RRect.fromRectAndRadius(rect, const Radius.circular(4)), - rectPaint, - ); + canvas.drawRRect(highlightRect, highlightPaint); } + } - final cursorX = size.width * cursorPosition; - canvas.drawLine( - Offset(cursorX, 0), - Offset(cursorX, size.height), - cursorPaint, - ); + /// 获取渐变颜色 + List _getGradientColors(double opacity) { + return isDarkMode + ? [ + 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 - bool shouldRepaint(covariant WaveformPainter oldDelegate) { - return oldDelegate.data != data || - oldDelegate.cursorPosition != cursorPosition; + bool shouldRepaint(covariant RealTimeWaveformPainter oldDelegate) { + return oldDelegate.waveData != waveData || + 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, + ); + }); + }, + ); } } diff --git a/lib/modules/permissions/bindings/permissions_binding.dart b/lib/modules/permissions/bindings/permissions_binding.dart new file mode 100644 index 000000000..2b0547984 --- /dev/null +++ b/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()); + } +} \ No newline at end of file diff --git a/lib/modules/permissions/controllers/permissions_controller.dart b/lib/modules/permissions/controllers/permissions_controller.dart new file mode 100644 index 000000000..4034cb772 --- /dev/null +++ b/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 permissionStatuses = + {}.obs; + + // 是否正在请求权限 + final RxBool isRequestingPermission = false.obs; + + // Android版本信息(用于权限组显示判断) + final RxInt androidSdkVersion = 0.obs; + + @override + void onInit() { + super.onInit(); + _initAndroidVersion(); + checkAllPermissions(); + } + + /// 初始化Android版本信息 + Future _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> checkAllPermissions() async { + final Map 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> _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> _getPlatformPermissions() async { + final bluetoothPermissions = await _getBluetoothPermissions(); + + if (Platform.isIOS) { + final List 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 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 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, + ); + } +} diff --git a/lib/modules/permissions/models/permission_group.dart b/lib/modules/permissions/models/permission_group.dart new file mode 100644 index 000000000..99a27ae43 --- /dev/null +++ b/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 permissions; + + const PermissionGroup({ + required this.titleKey, + required this.icon, + required this.permissions, + }); + + static List 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], + ), + ]; + } +} diff --git a/lib/modules/permissions/views/permissions_view.dart b/lib/modules/permissions/views/permissions_view.dart new file mode 100644 index 000000000..8022d8a2c --- /dev/null +++ b/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 { + 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 _buildPermissionGroups(bool isDarkMode) { + final groups = PermissionGroup.getPlatformGroups(); + final List 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 permissions, + Map 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 _buildPermissionItems( + List permissions, + Map statuses, + bool isDarkMode, + ) { + final List 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(); + } + } +} \ No newline at end of file diff --git a/lib/modules/settings/controllers/settings_controller.dart b/lib/modules/settings/controllers/settings_controller.dart index 496e405e3..ffa5c1b64 100644 --- a/lib/modules/settings/controllers/settings_controller.dart +++ b/lib/modules/settings/controllers/settings_controller.dart @@ -12,7 +12,6 @@ import '../../../data/services/ble_manager.dart'; import '../../../data/services/language_manager.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:permission_handler/permission_handler.dart'; import 'dart:async'; import '../../../data/services/version_update_service.dart'; import '../../common/widgets/version_update_dialog.dart'; @@ -129,103 +128,7 @@ class SettingsController extends GetxController { bool isCheckingUpdate = false; //状态锁,防止重复检查 DateTime? lastCheckTime; // 添加上次检查时间记录 - // 权限相关 - // 权限组,用于在UI中显示 - final List> 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> checkAllPermissions() async { - Map statuses = {}; - - for (var group in permissionGroups) { - for (var perm in group['permissions']) { - Permission permission = perm['type']; - statuses[permission] = await permission.status; - } - } - return statuses; - } @override void onInit() { diff --git a/lib/modules/settings/views/permissions_view.dart b/lib/modules/settings/views/permissions_view.dart deleted file mode 100644 index 2b0ceaf80..000000000 --- a/lib/modules/settings/views/permissions_view.dart +++ /dev/null @@ -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 { - 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>( - 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 permissions, - Map 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 _buildPermissionItems(List permissions, - Map statuses, bool isDarkMode) { - final List 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 _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; // 应用需要此权限以提供完整功能 - } - } -} diff --git a/lib/modules/splash/controllers/splash_controller.dart b/lib/modules/splash/controllers/splash_controller.dart index 5c90dacca..3bf9f9071 100644 --- a/lib/modules/splash/controllers/splash_controller.dart +++ b/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:get/get.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_model.dart'; @@ -44,14 +46,14 @@ class SplashController extends GetxController { return; } - // 步骤 2: 并行执行所有启动任务,并等待它们全部完成 + // 步骤 3: 并行执行所有启动任务,并等待它们全部完成 // Future.wait 会等待数组中所有的 Future 都完成后才继续 await Future.wait([ _startLoadingAnimation(), // 任务1: 最小动画时长 _performParallelTasks(), // 任务2: 其他所有后台任务 ]); - // 步骤 3: 所有任务(动画和后台)都已完成,触发导航 + // 步骤 4: 所有任务(动画和后台)都已完成,触发导航 _canNavigate.value = true; } @@ -81,6 +83,76 @@ class SplashController extends GetxController { return completer.future; } + /// 网络连接检查 + /// 网络连接检查 - 阻塞版本 + Future _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 _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 _startLoadingAnimation() { const totalDuration = 1500; // 动画总时长1.5秒 @@ -103,6 +175,8 @@ class SplashController extends GetxController { /// 将所有并行的后台任务打包成一个 Future Future _performParallelTasks() async { + //步骤2:检查网络连接状态 + await _checkNetworkStatus(); // 同样使用 Future.wait,确保此函数在所有子任务完成后才结束 await Future.wait([ _requestTrackingPermissionAsync(), @@ -211,3 +285,36 @@ class SplashController extends GetxController { } } } + +/// 显示无网络连接对话框并等待用户操作(阻塞版本) +Future _showNoNetworkDialogAndWait() async { + final completer = Completer(); + + 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; +} diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index b6df617fb..34f1fcc40 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -50,8 +50,9 @@ import '../modules/pairing/views/pairing_view.dart'; import '../modules/pairing/bindings/pairing_binding.dart'; import './app_routes.dart'; import '../modules/settings/views/settings_view.dart'; -import '../modules/settings/views/permissions_view.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/bindings/speech_test_binding.dart'; import '../modules/agent/views/agent_view.dart'; @@ -254,7 +255,7 @@ abstract class AppPages { GetPage( name: Routes.permissions, page: () => const PermissionsView(), - binding: SettingsBinding(), + binding: PermissionsBinding(), transition: Transition.rightToLeft, transitionDuration: Duration(milliseconds: 250), ), diff --git a/pubspec.yaml b/pubspec.yaml index ace7af29b..59d2d1b4d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -74,6 +74,7 @@ dependencies: dio: ^5.8.0+1 sqflite: ^2.4.2 path: ^1.9.1 + connectivity_plus: ^6.1.5 # 网络连接检测依赖 speech: path: local_plugins/speech azure_speech: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 74bd2623b..fae16f5da 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -18,6 +19,8 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + ConnectivityPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); FirebaseAuthPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index e548eecfe..44fbd12be 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + connectivity_plus file_selector_windows firebase_auth firebase_core