From cb08a336c7c91167ac39bbae642e8b2f51b9bdb7 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sat, 10 Jan 2026 16:49:39 +0800 Subject: [PATCH 1/3] =?UTF-8?q?refactor(meeting):=20=E6=8F=90=E5=8F=96?= =?UTF-8?q?=E6=8E=A7=E5=88=B6=E5=99=A8=E5=88=A0=E9=99=A4=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E5=B9=B6=E6=9B=B4=E6=96=B0=E5=9B=BE=E6=A0=87=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将重复的控制器删除逻辑提取为独立方法_deleteControllerAfterPop 更新底部操作栏图标为圆角风格 调整底部间距从32.h改为42.h --- .../meeting/views/meeting_record_view.dart | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/lib/modules/meeting/views/meeting_record_view.dart b/lib/modules/meeting/views/meeting_record_view.dart index b5a285ecd..654bf1653 100644 --- a/lib/modules/meeting/views/meeting_record_view.dart +++ b/lib/modules/meeting/views/meeting_record_view.dart @@ -16,6 +16,14 @@ enum _MeetingRecordExitAction { class MeetingRecordView extends GetView { const MeetingRecordView({super.key}); + void _deleteControllerAfterPop() { + Future.delayed(const Duration(milliseconds: 400), () { + if (Get.isRegistered()) { + Get.delete(force: true); + } + }); + } + Future<_MeetingRecordExitAction?> _showExitConfirmDialog(bool isDarkMode) { return Get.dialog<_MeetingRecordExitAction>( Dialog( @@ -191,11 +199,7 @@ class MeetingRecordView extends GetView { if (!controller.isRecording.value) { Get.back(); - Future.microtask(() { - if (Get.isRegistered()) { - Get.delete(force: true); - } - }); + _deleteControllerAfterPop(); return; } @@ -209,20 +213,12 @@ class MeetingRecordView extends GetView { await controller.saveTitleChange(); await controller.saveRecording(); Get.back(result: 'meeting_record_saved'); - Future.microtask(() { - if (Get.isRegistered()) { - Get.delete(force: true); - } - }); + _deleteControllerAfterPop(); return; case _MeetingRecordExitAction.endWithoutSave: await controller.discardRecording(); Get.back(result: 'meeting_record_discarded'); - Future.microtask(() { - if (Get.isRegistered()) { - Get.delete(force: true); - } - }); + _deleteControllerAfterPop(); return; case _MeetingRecordExitAction.continueInBackground: await controller.saveTitleChange(); @@ -472,7 +468,7 @@ class MeetingRecordView extends GetView { )), const Spacer(), _buildBottomBar(context, isDarkMode), - SizedBox(height: 32.h), + SizedBox(height: 42.h), ], ); }, @@ -490,7 +486,9 @@ class MeetingRecordView extends GetView { _buildActionButton( onTap: controller.toggleMarked, child: Obx(() => Icon( - controller.isMarked.value ? Icons.star : Icons.star_border, + controller.isMarked.value + ? Icons.star_rounded + : Icons.star_border_rounded, size: 36.sp, color: controller.isMarked.value ? Colors.orange : Colors.grey, @@ -507,7 +505,7 @@ class MeetingRecordView extends GetView { child: Obx(() => Icon( controller.timerStatus.value == TimerStatus.running ? Icons.pause - : Icons.play_arrow, + : Icons.play_arrow_rounded, size: 44.sp, color: Colors.red, )), @@ -521,7 +519,7 @@ class MeetingRecordView extends GetView { _buildActionButton( onTap: () => _bottomSheet(context, isDarkMode), child: Icon( - Icons.check, + Icons.check_rounded, size: 36.sp, color: Colors.green, ), From 43ce0daa6791f9f3b7869654d70ac4f6f874e6cc Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sat, 10 Jan 2026 18:46:20 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat(=E6=9D=83=E9=99=90):=20=E6=94=B9?= =?UTF-8?q?=E8=BF=9B=E6=82=AC=E6=B5=AE=E7=AA=97=E6=9D=83=E9=99=90=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E6=B5=81=E7=A8=8B=E5=B9=B6=E6=B7=BB=E5=8A=A0=E5=9C=BA?= =?UTF-8?q?=E6=99=AF=E5=8C=BA=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为悬浮窗权限请求添加不同场景的说明文案,包括翻译悬浮窗和录音悬浮条场景 改进Android端权限请求逻辑,增加系统设置跳转的容错处理 添加相关多语言文案支持 --- .../voitrans/OverlayPermissionPlugin.kt | 45 ++++++++++++++++--- .../services/floating_window_manager.dart | 5 ++- lib/core/translations/language/en_us.dart | 7 +++ lib/core/translations/language/zh_cn.dart | 7 +++ lib/core/utils/overlay_permission_util.dart | 40 ++++++++++++++--- .../meeting_record_controller.dart | 4 +- 6 files changed, 92 insertions(+), 16 deletions(-) diff --git a/android/app/src/main/kotlin/com/saitong/voitrans/OverlayPermissionPlugin.kt b/android/app/src/main/kotlin/com/saitong/voitrans/OverlayPermissionPlugin.kt index 00ff6d139..e8496d57a 100644 --- a/android/app/src/main/kotlin/com/saitong/voitrans/OverlayPermissionPlugin.kt +++ b/android/app/src/main/kotlin/com/saitong/voitrans/OverlayPermissionPlugin.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Build +import android.content.pm.PackageManager import android.provider.Settings import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall @@ -46,12 +47,32 @@ class OverlayPermissionPlugin: FlutterPlugin, MethodCallHandler { private fun requestOverlayPermission(result: Result) { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION).apply { - data = Uri.parse("package:${context.packageName}") - flags = Intent.FLAG_ACTIVITY_NEW_TASK + if (hasOverlayPermission()) { + result.success(true) + return } - context.startActivity(intent) - result.success(true) + + val openedBySystem = startActivitySafe( + Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION).apply { + data = Uri.parse("package:${context.packageName}") + } + ) + if (openedBySystem) { + result.success(true) + return + } + + val openedByAppDetails = startActivitySafe( + Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.parse("package:${context.packageName}") + } + ) + if (openedByAppDetails) { + result.success(true) + return + } + + result.success(false) } else { result.success(true) } @@ -60,7 +81,19 @@ class OverlayPermissionPlugin: FlutterPlugin, MethodCallHandler { } } + private fun startActivitySafe(intent: Intent): Boolean { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + val pm: PackageManager = context.packageManager + if (intent.resolveActivity(pm) == null) return false + return try { + context.startActivity(intent) + true + } catch (_: Exception) { + false + } + } + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { channel.setMethodCallHandler(null) } -} \ No newline at end of file +} diff --git a/lib/core/services/floating_window_manager.dart b/lib/core/services/floating_window_manager.dart index ddbe999ee..955f7ff40 100644 --- a/lib/core/services/floating_window_manager.dart +++ b/lib/core/services/floating_window_manager.dart @@ -91,8 +91,9 @@ class FloatingWindowManager extends GetxController { /// 启用悬浮窗功能 Future enableFloatingWindow() async { if (!_hasPermission.value) { - final bool granted = - await OverlayPermissionUtil.requestOverlayPermission(); + final bool granted = await OverlayPermissionUtil.requestOverlayPermission( + scene: OverlayPermissionScene.translationFloatingWindow, + ); if (!granted) { Get.snackbar('权限不足'.tr, '需要悬浮窗权限才能启用此功能'.tr); return false; diff --git a/lib/core/translations/language/en_us.dart b/lib/core/translations/language/en_us.dart index 2a9a6cd8c..9297a6ab4 100644 --- a/lib/core/translations/language/en_us.dart +++ b/lib/core/translations/language/en_us.dart @@ -446,6 +446,13 @@ const Map enUS = { 'meetingRecordRecordingNotificationTitle': 'Recording in progress', // 录音进行中 'meetingRecordRecordingNotificationBody': 'Recording in background. Tap to return to the app.', // 正在后台录音,点击返回应用 + 'overlayPermissionTitle': 'Draw over other apps', // 悬浮窗权限 + 'overlayPermissionTranslationDesc': + 'To show translation results during calls or audio/video translation, we need the overlay permission. This lets you see real-time translations while using other apps.', // 为了在通话或音视频翻译时显示翻译结果,需要悬浮窗权限。这样您就可以在使用其他应用时看到实时翻译。 + 'overlayPermissionRecordingDesc': + 'To show the recording floating bar (timer/pause/resume) during recording, we need the overlay permission.', // 为了在录音过程中显示录音悬浮条(计时/暂停/继续),需要悬浮窗权限。 + 'overlayPermissionOpenSettingsFailed': + 'Unable to open system settings automatically. Please open Settings and enable the overlay permission manually.', // 无法自动跳转到系统设置,请手动前往系统设置开启悬浮窗权限。 'transcribing': 'Transcribing...', 'summarizing': 'Summarizing...', 'analyzing': 'Analyzing...', diff --git a/lib/core/translations/language/zh_cn.dart b/lib/core/translations/language/zh_cn.dart index 2bffe4762..3b1352f64 100644 --- a/lib/core/translations/language/zh_cn.dart +++ b/lib/core/translations/language/zh_cn.dart @@ -421,6 +421,13 @@ const Map zhCN = { 'meetingRecordContinueInBackground': '后台继续录音', // 后台继续录音 'meetingRecordRecordingNotificationTitle': '录音进行中', // 录音进行中 'meetingRecordRecordingNotificationBody': '正在后台录音,点击返回应用', // 正在后台录音,点击返回应用 + 'overlayPermissionTitle': '悬浮窗权限', // 悬浮窗权限 + 'overlayPermissionTranslationDesc': + '为了在通话或音视频翻译时显示翻译结果,需要悬浮窗权限。这样您就可以在使用其他应用时看到实时翻译。', // 为了在通话或音视频翻译时显示翻译结果,需要悬浮窗权限。这样您就可以在使用其他应用时看到实时翻译。 + 'overlayPermissionRecordingDesc': + '为了在录音过程中显示录音悬浮条(计时/暂停/继续),需要悬浮窗权限。', // 为了在录音过程中显示录音悬浮条(计时/暂停/继续),需要悬浮窗权限。 + 'overlayPermissionOpenSettingsFailed': + '无法自动跳转到系统设置,请手动前往系统设置开启悬浮窗权限。', // 无法自动跳转到系统设置,请手动前往系统设置开启悬浮窗权限。 'transcribing': '正在转写...', 'summarizing': '正在总结...', 'analyzing': '正在分析...', diff --git a/lib/core/utils/overlay_permission_util.dart b/lib/core/utils/overlay_permission_util.dart index 6a580621c..a4beb20e9 100644 --- a/lib/core/utils/overlay_permission_util.dart +++ b/lib/core/utils/overlay_permission_util.dart @@ -4,6 +4,12 @@ import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'logger.dart'; +/// 悬浮窗权限场景 +enum OverlayPermissionScene { + translationFloatingWindow, + recordingFloatingBar, +} + /// 悬浮窗权限管理工具类 class OverlayPermissionUtil { static const MethodChannel _channel = MethodChannel('overlay_permission'); @@ -12,7 +18,8 @@ class OverlayPermissionUtil { static Future hasOverlayPermission() async { if (Platform.isAndroid) { try { - final bool hasPermission = await _channel.invokeMethod('hasOverlayPermission'); + final bool hasPermission = + await _channel.invokeMethod('hasOverlayPermission'); Logger.info('检查悬浮窗权限: $hasPermission'); return hasPermission; } catch (e) { @@ -25,26 +32,36 @@ class OverlayPermissionUtil { } /// 请求悬浮窗权限 - static Future requestOverlayPermission() async { + static Future requestOverlayPermission({ + OverlayPermissionScene scene = + OverlayPermissionScene.translationFloatingWindow, + }) async { if (Platform.isAndroid) { // 首先检查是否已有权限 if (await hasOverlayPermission()) { return true; } + final String explanationKey = switch (scene) { + OverlayPermissionScene.translationFloatingWindow => + 'overlayPermissionTranslationDesc', // 为了在通话或音视频翻译时显示翻译结果,需要悬浮窗权限。这样您就可以在使用其他应用时看到实时翻译。 + OverlayPermissionScene.recordingFloatingBar => + 'overlayPermissionRecordingDesc', // 为了在录音过程中显示录音悬浮条(计时/暂停/继续),需要悬浮窗权限。 + }; + // 显示说明对话框 bool? userAgreed = await Get.dialog( AlertDialog( - title: Text('悬浮窗权限'.tr), - content: Text('为了在通话或音视频翻译时显示翻译结果,需要悬浮窗权限。这样您就可以在使用其他应用时看到实时翻译。'.tr), + title: Text('overlayPermissionTitle'.tr), // 悬浮窗权限 + content: Text(explanationKey.tr), // 按场景显示不同说明文案 actions: [ TextButton( onPressed: () => Get.back(result: false), - child: Text('取消'.tr), + child: Text('cancel'.tr), // 取消 ), ElevatedButton( onPressed: () => Get.back(result: true), - child: Text('去设置'.tr), + child: Text('goToSettings'.tr), // 去设置 ), ], ), @@ -54,7 +71,16 @@ class OverlayPermissionUtil { if (userAgreed == true) { try { // 跳转系统设置页 - await _channel.invokeMethod('requestOverlayPermission'); + final bool opened = + await _channel.invokeMethod('requestOverlayPermission'); + if (!opened) { + Get.snackbar( + 'permissionRequestError'.tr, + 'overlayPermissionOpenSettingsFailed' + .tr, // 无法自动跳转到系统设置,请手动前往系统设置开启悬浮窗权限。 + ); + return false; + } // 等待用户操作完成后重新检查权限 await Future.delayed(const Duration(seconds: 1)); return await hasOverlayPermission(); diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart index 635d39d41..a969b7bae 100644 --- a/lib/modules/meeting/controllers/meeting_record_controller.dart +++ b/lib/modules/meeting/controllers/meeting_record_controller.dart @@ -135,7 +135,9 @@ class MeetingRecordController extends GetxController final hasPermission = await OverlayPermissionUtil.hasOverlayPermission(); if (!hasPermission) { - final granted = await OverlayPermissionUtil.requestOverlayPermission(); + final granted = await OverlayPermissionUtil.requestOverlayPermission( + scene: OverlayPermissionScene.recordingFloatingBar, + ); if (!granted) return; } From 43c1b8f45783acc11d2387e9f5de07c8bd72d4a6 Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Sat, 10 Jan 2026 20:54:32 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E4=BF=AE=E6=94=B9cos=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/utils/upload_oss.dart | 40 +++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/lib/core/utils/upload_oss.dart b/lib/core/utils/upload_oss.dart index a2fb54bbf..b2f825090 100644 --- a/lib/core/utils/upload_oss.dart +++ b/lib/core/utils/upload_oss.dart @@ -3,25 +3,45 @@ import 'dart:math'; import 'package:common_utils/common_utils.dart'; import 'package:dio/dio.dart'; +import 'package:get_storage/get_storage.dart'; import 'package:tencentcloud_cos_sdk_plugin/cos.dart'; import 'package:tencentcloud_cos_sdk_plugin/cos_transfer_manger.dart'; import 'package:tencentcloud_cos_sdk_plugin/pigeon.dart'; +import '../../data/models/appconfig.dart'; + class UploadOss { static final UploadOss _shared = UploadOss._internal(); factory UploadOss() => _shared; - static const bucket = 'deepsound-1253517901'; - static const region = 'ap-guangzhou'; + static String bucket = ''; + static String region = ''; + static String secretId = ''; + static String secretKey = ''; + static String userId = ''; static CosTransferManger? _cosTransferManger; UploadOss._internal(); + static initOss() { + bucket = AppConfig.env('COS_BUCKET_NAME') ?? ''; + region = AppConfig.env('COS_REGION') ?? ''; + secretId = AppConfig.env('COS_SECRET_ID') ?? ''; + secretKey = AppConfig.env('COS_SECRET_KEY') ?? ''; + + final GetStorage storage = GetStorage(); + Map? userInfo = storage.read("user_info"); + if (userInfo != null) { + userId = userInfo['user']['uid']; + } else { + userId = ''; + } + } + static Future getTransferManger() async { if (_cosTransferManger == null) { - await Cos().initWithPlainSecret('AKID9F8qNiqlTzIMFwQ9rQxNcgYCdsukNmzA', - 'OYVGo8HEzWpvpfHgtfHfqpcDbnlY7Tdc'); + await Cos().initWithPlainSecret(secretId, secretKey); if (Cos().hasTransferManger(region)) { _cosTransferManger = Cos().getTransferManger(region); } else { @@ -43,8 +63,16 @@ class UploadOss { Function? callback, ProgressCallback? onSendProgress, }) async { - String pathName = - '$rootDir/${getDate()}/${getRandom(12)}.${fileType ?? getFileType(filepath!)}'; + if (bucket.isEmpty || + region.isEmpty || + secretId.isEmpty || + secretKey.isEmpty || + userId.isEmpty) { + initOss(); + } + String pathName = userId.isNotEmpty + ? 'User/$userId/$rootDir/${getDate()}/${getRandom(12)}.${fileType ?? getFileType(filepath!)}' + : 'User/$rootDir/${getDate()}/${getRandom(12)}.${fileType ?? getFileType(filepath!)}'; CosTransferManger cosTransferManger = await getTransferManger(); final completer = Completer(); await cosTransferManger.upload(