Browse Source

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

weicu
liwei1dao 9 months ago
parent
commit
28115c0651
  1. 45
      android/app/src/main/kotlin/com/saitong/voitrans/OverlayPermissionPlugin.kt
  2. 5
      lib/core/services/floating_window_manager.dart
  3. 7
      lib/core/translations/language/en_us.dart
  4. 7
      lib/core/translations/language/zh_cn.dart
  5. 40
      lib/core/utils/overlay_permission_util.dart
  6. 40
      lib/core/utils/upload_oss.dart
  7. 4
      lib/modules/meeting/controllers/meeting_record_controller.dart
  8. 36
      lib/modules/meeting/views/meeting_record_view.dart

45
android/app/src/main/kotlin/com/saitong/voitrans/OverlayPermissionPlugin.kt

@ -4,6 +4,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.content.pm.PackageManager
import android.provider.Settings import android.provider.Settings
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
@ -46,12 +47,32 @@ class OverlayPermissionPlugin: FlutterPlugin, MethodCallHandler {
private fun requestOverlayPermission(result: Result) { private fun requestOverlayPermission(result: Result) {
try { try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION).apply { if (hasOverlayPermission()) {
data = Uri.parse("package:${context.packageName}") result.success(true)
flags = Intent.FLAG_ACTIVITY_NEW_TASK 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 { } else {
result.success(true) 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) { override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null) channel.setMethodCallHandler(null)
} }
} }

5
lib/core/services/floating_window_manager.dart

@ -91,8 +91,9 @@ class FloatingWindowManager extends GetxController {
/// 启用悬浮窗功能 /// 启用悬浮窗功能
Future<bool> enableFloatingWindow() async { Future<bool> enableFloatingWindow() async {
if (!_hasPermission.value) { if (!_hasPermission.value) {
final bool granted = final bool granted = await OverlayPermissionUtil.requestOverlayPermission(
await OverlayPermissionUtil.requestOverlayPermission(); scene: OverlayPermissionScene.translationFloatingWindow,
);
if (!granted) { if (!granted) {
Get.snackbar('权限不足'.tr, '需要悬浮窗权限才能启用此功能'.tr); Get.snackbar('权限不足'.tr, '需要悬浮窗权限才能启用此功能'.tr);
return false; return false;

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

@ -446,6 +446,13 @@ const Map<String, String> enUS = {
'meetingRecordRecordingNotificationTitle': 'Recording in progress', // 录音进行中 'meetingRecordRecordingNotificationTitle': 'Recording in progress', // 录音进行中
'meetingRecordRecordingNotificationBody': 'meetingRecordRecordingNotificationBody':
'Recording in background. Tap to return to the app.', // 正在后台录音,点击返回应用 '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...', 'transcribing': 'Transcribing...',
'summarizing': 'Summarizing...', 'summarizing': 'Summarizing...',
'analyzing': 'Analyzing...', 'analyzing': 'Analyzing...',

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

@ -421,6 +421,13 @@ const Map<String, String> zhCN = {
'meetingRecordContinueInBackground': '后台继续录音', // 后台继续录音 'meetingRecordContinueInBackground': '后台继续录音', // 后台继续录音
'meetingRecordRecordingNotificationTitle': '录音进行中', // 录音进行中 'meetingRecordRecordingNotificationTitle': '录音进行中', // 录音进行中
'meetingRecordRecordingNotificationBody': '正在后台录音,点击返回应用', // 正在后台录音,点击返回应用 'meetingRecordRecordingNotificationBody': '正在后台录音,点击返回应用', // 正在后台录音,点击返回应用
'overlayPermissionTitle': '悬浮窗权限', // 悬浮窗权限
'overlayPermissionTranslationDesc':
'为了在通话或音视频翻译时显示翻译结果,需要悬浮窗权限。这样您就可以在使用其他应用时看到实时翻译。', // 为了在通话或音视频翻译时显示翻译结果,需要悬浮窗权限。这样您就可以在使用其他应用时看到实时翻译。
'overlayPermissionRecordingDesc':
'为了在录音过程中显示录音悬浮条(计时/暂停/继续),需要悬浮窗权限。', // 为了在录音过程中显示录音悬浮条(计时/暂停/继续),需要悬浮窗权限。
'overlayPermissionOpenSettingsFailed':
'无法自动跳转到系统设置,请手动前往系统设置开启悬浮窗权限。', // 无法自动跳转到系统设置,请手动前往系统设置开启悬浮窗权限。
'transcribing': '正在转写...', 'transcribing': '正在转写...',
'summarizing': '正在总结...', 'summarizing': '正在总结...',
'analyzing': '正在分析...', 'analyzing': '正在分析...',

40
lib/core/utils/overlay_permission_util.dart

@ -4,6 +4,12 @@ import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'logger.dart'; import 'logger.dart';
/// 悬浮窗权限场景
enum OverlayPermissionScene {
translationFloatingWindow,
recordingFloatingBar,
}
/// 悬浮窗权限管理工具类 /// 悬浮窗权限管理工具类
class OverlayPermissionUtil { class OverlayPermissionUtil {
static const MethodChannel _channel = MethodChannel('overlay_permission'); static const MethodChannel _channel = MethodChannel('overlay_permission');
@ -12,7 +18,8 @@ class OverlayPermissionUtil {
static Future<bool> hasOverlayPermission() async { static Future<bool> hasOverlayPermission() async {
if (Platform.isAndroid) { if (Platform.isAndroid) {
try { try {
final bool hasPermission = await _channel.invokeMethod('hasOverlayPermission'); final bool hasPermission =
await _channel.invokeMethod('hasOverlayPermission');
Logger.info('检查悬浮窗权限: $hasPermission'); Logger.info('检查悬浮窗权限: $hasPermission');
return hasPermission; return hasPermission;
} catch (e) { } catch (e) {
@ -25,26 +32,36 @@ class OverlayPermissionUtil {
} }
/// 请求悬浮窗权限 /// 请求悬浮窗权限
static Future<bool> requestOverlayPermission() async { static Future<bool> requestOverlayPermission({
OverlayPermissionScene scene =
OverlayPermissionScene.translationFloatingWindow,
}) async {
if (Platform.isAndroid) { if (Platform.isAndroid) {
// 首先检查是否已有权限 // 首先检查是否已有权限
if (await hasOverlayPermission()) { if (await hasOverlayPermission()) {
return true; return true;
} }
final String explanationKey = switch (scene) {
OverlayPermissionScene.translationFloatingWindow =>
'overlayPermissionTranslationDesc', // 为了在通话或音视频翻译时显示翻译结果,需要悬浮窗权限。这样您就可以在使用其他应用时看到实时翻译。
OverlayPermissionScene.recordingFloatingBar =>
'overlayPermissionRecordingDesc', // 为了在录音过程中显示录音悬浮条(计时/暂停/继续),需要悬浮窗权限。
};
// 显示说明对话框 // 显示说明对话框
bool? userAgreed = await Get.dialog<bool>( bool? userAgreed = await Get.dialog<bool>(
AlertDialog( AlertDialog(
title: Text('悬浮窗权限'.tr), title: Text('overlayPermissionTitle'.tr), // 悬浮窗权限
content: Text('为了在通话或音视频翻译时显示翻译结果,需要悬浮窗权限。这样您就可以在使用其他应用时看到实时翻译。'.tr), content: Text(explanationKey.tr), // 按场景显示不同说明文案
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Get.back(result: false), onPressed: () => Get.back(result: false),
child: Text('取消'.tr), child: Text('cancel'.tr), // 取消
), ),
ElevatedButton( ElevatedButton(
onPressed: () => Get.back(result: true), onPressed: () => Get.back(result: true),
child: Text('去设置'.tr), child: Text('goToSettings'.tr), // 去设置
), ),
], ],
), ),
@ -54,7 +71,16 @@ class OverlayPermissionUtil {
if (userAgreed == true) { if (userAgreed == true) {
try { 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)); await Future.delayed(const Duration(seconds: 1));
return await hasOverlayPermission(); return await hasOverlayPermission();

40
lib/core/utils/upload_oss.dart

@ -3,25 +3,45 @@ import 'dart:math';
import 'package:common_utils/common_utils.dart'; import 'package:common_utils/common_utils.dart';
import 'package:dio/dio.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.dart';
import 'package:tencentcloud_cos_sdk_plugin/cos_transfer_manger.dart'; import 'package:tencentcloud_cos_sdk_plugin/cos_transfer_manger.dart';
import 'package:tencentcloud_cos_sdk_plugin/pigeon.dart'; import 'package:tencentcloud_cos_sdk_plugin/pigeon.dart';
import '../../data/models/appconfig.dart';
class UploadOss { class UploadOss {
static final UploadOss _shared = UploadOss._internal(); static final UploadOss _shared = UploadOss._internal();
factory UploadOss() => _shared; factory UploadOss() => _shared;
static const bucket = 'deepsound-1253517901'; static String bucket = '';
static const region = 'ap-guangzhou'; static String region = '';
static String secretId = '';
static String secretKey = '';
static String userId = '';
static CosTransferManger? _cosTransferManger; static CosTransferManger? _cosTransferManger;
UploadOss._internal(); 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<CosTransferManger> getTransferManger() async { static Future<CosTransferManger> getTransferManger() async {
if (_cosTransferManger == null) { if (_cosTransferManger == null) {
await Cos().initWithPlainSecret('AKID9F8qNiqlTzIMFwQ9rQxNcgYCdsukNmzA', await Cos().initWithPlainSecret(secretId, secretKey);
'OYVGo8HEzWpvpfHgtfHfqpcDbnlY7Tdc');
if (Cos().hasTransferManger(region)) { if (Cos().hasTransferManger(region)) {
_cosTransferManger = Cos().getTransferManger(region); _cosTransferManger = Cos().getTransferManger(region);
} else { } else {
@ -43,8 +63,16 @@ class UploadOss {
Function? callback, Function? callback,
ProgressCallback? onSendProgress, ProgressCallback? onSendProgress,
}) async { }) async {
String pathName = if (bucket.isEmpty ||
'$rootDir/${getDate()}/${getRandom(12)}.${fileType ?? getFileType(filepath!)}'; 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(); CosTransferManger cosTransferManger = await getTransferManger();
final completer = Completer<String>(); final completer = Completer<String>();
await cosTransferManger.upload( await cosTransferManger.upload(

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

@ -135,7 +135,9 @@ class MeetingRecordController extends GetxController
final hasPermission = await OverlayPermissionUtil.hasOverlayPermission(); final hasPermission = await OverlayPermissionUtil.hasOverlayPermission();
if (!hasPermission) { if (!hasPermission) {
final granted = await OverlayPermissionUtil.requestOverlayPermission(); final granted = await OverlayPermissionUtil.requestOverlayPermission(
scene: OverlayPermissionScene.recordingFloatingBar,
);
if (!granted) return; if (!granted) return;
} }

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

@ -16,6 +16,14 @@ enum _MeetingRecordExitAction {
class MeetingRecordView extends GetView<MeetingRecordController> { class MeetingRecordView extends GetView<MeetingRecordController> {
const MeetingRecordView({super.key}); const MeetingRecordView({super.key});
void _deleteControllerAfterPop() {
Future.delayed(const Duration(milliseconds: 400), () {
if (Get.isRegistered<MeetingRecordController>()) {
Get.delete<MeetingRecordController>(force: true);
}
});
}
Future<_MeetingRecordExitAction?> _showExitConfirmDialog(bool isDarkMode) { Future<_MeetingRecordExitAction?> _showExitConfirmDialog(bool isDarkMode) {
return Get.dialog<_MeetingRecordExitAction>( return Get.dialog<_MeetingRecordExitAction>(
Dialog( Dialog(
@ -191,11 +199,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
if (!controller.isRecording.value) { if (!controller.isRecording.value) {
Get.back(); Get.back();
Future.microtask(() { _deleteControllerAfterPop();
if (Get.isRegistered<MeetingRecordController>()) {
Get.delete<MeetingRecordController>(force: true);
}
});
return; return;
} }
@ -209,20 +213,12 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
await controller.saveTitleChange(); await controller.saveTitleChange();
await controller.saveRecording(); await controller.saveRecording();
Get.back(result: 'meeting_record_saved'); Get.back(result: 'meeting_record_saved');
Future.microtask(() { _deleteControllerAfterPop();
if (Get.isRegistered<MeetingRecordController>()) {
Get.delete<MeetingRecordController>(force: true);
}
});
return; return;
case _MeetingRecordExitAction.endWithoutSave: case _MeetingRecordExitAction.endWithoutSave:
await controller.discardRecording(); await controller.discardRecording();
Get.back(result: 'meeting_record_discarded'); Get.back(result: 'meeting_record_discarded');
Future.microtask(() { _deleteControllerAfterPop();
if (Get.isRegistered<MeetingRecordController>()) {
Get.delete<MeetingRecordController>(force: true);
}
});
return; return;
case _MeetingRecordExitAction.continueInBackground: case _MeetingRecordExitAction.continueInBackground:
await controller.saveTitleChange(); await controller.saveTitleChange();
@ -472,7 +468,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
)), )),
const Spacer(), const Spacer(),
_buildBottomBar(context, isDarkMode), _buildBottomBar(context, isDarkMode),
SizedBox(height: 32.h), SizedBox(height: 42.h),
], ],
); );
}, },
@ -490,7 +486,9 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
_buildActionButton( _buildActionButton(
onTap: controller.toggleMarked, onTap: controller.toggleMarked,
child: Obx(() => Icon( child: Obx(() => Icon(
controller.isMarked.value ? Icons.star : Icons.star_border, controller.isMarked.value
? Icons.star_rounded
: Icons.star_border_rounded,
size: 36.sp, size: 36.sp,
color: color:
controller.isMarked.value ? Colors.orange : Colors.grey, controller.isMarked.value ? Colors.orange : Colors.grey,
@ -507,7 +505,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
child: Obx(() => Icon( child: Obx(() => Icon(
controller.timerStatus.value == TimerStatus.running controller.timerStatus.value == TimerStatus.running
? Icons.pause ? Icons.pause
: Icons.play_arrow, : Icons.play_arrow_rounded,
size: 44.sp, size: 44.sp,
color: Colors.red, color: Colors.red,
)), )),
@ -521,7 +519,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
_buildActionButton( _buildActionButton(
onTap: () => _bottomSheet(context, isDarkMode), onTap: () => _bottomSheet(context, isDarkMode),
child: Icon( child: Icon(
Icons.check, Icons.check_rounded,
size: 36.sp, size: 36.sp,
color: Colors.green, color: Colors.green,
), ),

Loading…
Cancel
Save