diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 20d5ead25..cbb9afd88 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -69,8 +69,8 @@ android {
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 26 // 更新为26以满足ble_service库的要求
targetSdk = 34 // 设置为 Android 13 (API 33),以匹配 FOREGROUND_SERVICE_MEDIA_PLAYBACK 权限的要求
- versionCode = flutter.versionCode
- versionName = flutter.versionName
+ versionCode = flutter.versionCode // 对应构建号,自动同步flutter构建号
+ versionName = flutter.versionName // 对应版本号,自动同步flutter版本号
}
signingConfigs {
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index bc3f4a793..5be33da0e 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -39,7 +39,7 @@
- CFBundleVersion
+ CFBundleVersion
$(FLUTTER_BUILD_NUMBER)
FacebookAdvertiserIDCollectionEnabled
diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart
index 08bb41500..c372b985a 100644
--- a/lib/core/bindings/initial_binding.dart
+++ b/lib/core/bindings/initial_binding.dart
@@ -10,6 +10,7 @@ import '../../core/utils/logger.dart';
import '../../data/services/bluetooth_manager.dart';
import '../../data/services/ble_manager.dart';
import '../../data/services/account_service.dart';
+import '../../data/services/version_update_service.dart';
/// 初始绑定,用于管理全局依赖
class InitialBinding extends Bindings {
@@ -46,6 +47,9 @@ class InitialBinding extends Bindings {
// 账号管理服务
Get.lazyPut(() => AccountService(), fenix: true);
+ // 版本更新服务
+ Get.lazyPut(() => VersionUpdateService(), fenix: true);
+
// 杰理蓝牙服务
// // 语言管理器(需要最先初始化)
diff --git a/lib/core/utils/synchrodata.dart b/lib/core/utils/synchrodata.dart
new file mode 100644
index 000000000..4d8d2e3af
--- /dev/null
+++ b/lib/core/utils/synchrodata.dart
@@ -0,0 +1,103 @@
+import 'dart:convert';
+
+import 'package:get_storage/get_storage.dart';
+
+import '../../data/services/db/sqflite_api.dart';
+import '../../data/services/network/api.dart';
+
+class Synchrodata {
+ static final GetStorage _storage = GetStorage();
+
+ static List _records = _storage.read("operation_records") ?? [];
+ static List _delids = _storage.read("operation_delids") ?? [];
+ static bool _isSynchro = false;
+
+ // 保存操作记录
+ static Future saveOperationRecord(
+ String method,
+ dynamic params,
+ ) async {
+ String userId = '';
+ Map? userInfo = _storage.read("user_info");
+ if (userInfo != null) {
+ userId = userInfo['user']['uid'];
+ }
+ String id = '${userId}_${params['id']}';
+ if (params['type'] != null) {
+ params['rtype'] = params['type'];
+ }
+
+ switch (method) {
+ case 'insert':
+ _records.add({
+ ...params,
+ 'id': id,
+ 'number': params['id'],
+ 'uid': userId,
+ });
+ _storage.write("operation_records", _records);
+ break;
+ case 'update':
+ Map? foundElement = _records.firstWhere(
+ (element) => element['id'] == id,
+ orElse: () => null,
+ );
+ if (foundElement != null) {
+ foundElement.addAll({
+ ...params,
+ 'id': id,
+ 'number': params['id'],
+ });
+ } else {
+ _records.add({
+ ...params,
+ 'id': id,
+ 'number': params['id'],
+ 'uid': userId,
+ });
+ }
+ _storage.write("operation_records", _records);
+ break;
+ case 'delete':
+ _delids.add(id);
+ _storage.write("operation_delids", _delids);
+ break;
+ default:
+ break;
+ }
+ }
+
+ // 获取操作记录
+ static Future startSynchro() async {
+ if (_isSynchro) return;
+ _isSynchro = true;
+ if (_records.isNotEmpty || _delids.isNotEmpty) {
+ // 获取需要修改的全部数据
+ List putRecords = [];
+ for (var element in _records) {
+ final result = await SqfliteApi.getMeetingDetails(element['number']);
+ final speakerList =
+ await SqfliteApi.getMeetingSpeaker(element['number']);
+ putRecords.add({
+ ...result,
+ ...result['details'],
+ 'rtype': result['type'],
+ 'original': jsonEncode(speakerList),
+ ...element,
+ });
+ }
+ var result = await Api.putOperationRecord({
+ 'delids': _delids,
+ 'records': putRecords,
+ });
+ if (result != null) {
+ _records = [];
+ _delids = [];
+ _storage.write("operation_records", _records);
+ _storage.write("operation_delids", _delids);
+ }
+ }
+ var result = await Api.getOperationRecord();
+ _isSynchro = false;
+ }
+}
diff --git a/lib/data/models/channel_app_model.dart b/lib/data/models/channel_app_model.dart
new file mode 100644
index 000000000..bddbf9f39
--- /dev/null
+++ b/lib/data/models/channel_app_model.dart
@@ -0,0 +1,151 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'channel_app_model.g.dart';
+
+/// 渠道应用信息响应模型
+@JsonSerializable()
+class ChannelAppResponse {
+ @JsonKey(name: 'code')
+ final int code;
+
+ @JsonKey(name: 'data')
+ final ChannelAppData? data;
+
+ @JsonKey(name: 'msg')
+ final String msg;
+
+ const ChannelAppResponse({
+ required this.code,
+ this.data,
+ required this.msg,
+ });
+
+ factory ChannelAppResponse.fromJson(Map json) =>
+ _$ChannelAppResponseFromJson(json);
+
+ Map toJson() => _$ChannelAppResponseToJson(this);
+}
+
+/// 渠道应用数据模型
+@JsonSerializable()
+class ChannelAppData {
+ @JsonKey(name: 'app')
+ final AppInfo app;
+
+ const ChannelAppData({
+ required this.app,
+ });
+
+ factory ChannelAppData.fromJson(Map json) =>
+ _$ChannelAppDataFromJson(json);
+
+ Map toJson() => _$ChannelAppDataToJson(this);
+}
+
+/// 应用信息模型
+@JsonSerializable()
+class AppInfo {
+ /// 下载地址
+ @JsonKey(name: 'address')
+ final String address;
+
+ /// 渠道标识
+ @JsonKey(name: 'channel')
+ final String channel;
+
+ /// 应用描述
+ @JsonKey(name: 'description')
+ final String description;
+
+ /// 最新版本号
+ @JsonKey(name: 'version')
+ final String version;
+
+ const AppInfo({
+ required this.address,
+ required this.channel,
+ required this.description,
+ required this.version,
+ });
+
+ factory AppInfo.fromJson(Map json) =>
+ _$AppInfoFromJson(json);
+
+ Map toJson() => _$AppInfoToJson(this);
+}
+
+/// 版本更新信息模型
+class VersionUpdateInfo {
+ /// 是否有新版本
+ final bool hasUpdate;
+
+ /// 当前版本
+ final String currentVersion;
+
+ /// 最新版本
+ final String latestVersion;
+
+ /// 应用信息
+ final AppInfo? appInfo;
+
+ /// 更新类型
+ final UpdateType updateType;
+
+ const VersionUpdateInfo({
+ required this.hasUpdate,
+ required this.currentVersion,
+ required this.latestVersion,
+ this.appInfo,
+ required this.updateType,
+ });
+}
+
+/// 更新类型枚举
+enum UpdateType {
+ /// 无需更新
+ none,
+ /// 可选更新
+ optional,
+ /// 强制更新
+ force,
+}
+
+/// 应用商店类型枚举
+enum AppStoreType {
+ /// iOS App Store
+ appStore,
+ /// Google Play
+ googlePlay,
+ /// 华为应用市场
+ huawei,
+ /// 小米应用商店
+ xiaomi,
+ /// OPPO软件商店
+ oppo,
+ /// vivo应用商店
+ vivo,
+ /// 荣耀应用市场
+ honor,
+ /// 官网下载
+ official,
+ /// 未知
+ unknown,
+}
+
+/// 平台信息模型
+class PlatformInfo {
+ /// 平台类型
+ final String platform;
+
+ /// 应用商店类型
+ final AppStoreType storeType;
+
+ /// 渠道标识
+ final String channel;
+
+ const PlatformInfo({
+ required this.platform,
+ required this.storeType,
+ required this.channel,
+ });
+}
diff --git a/lib/data/models/channel_app_model.g.dart b/lib/data/models/channel_app_model.g.dart
new file mode 100644
index 000000000..7d5e6aa6b
--- /dev/null
+++ b/lib/data/models/channel_app_model.g.dart
@@ -0,0 +1,47 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'channel_app_model.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+ChannelAppResponse _$ChannelAppResponseFromJson(Map json) =>
+ ChannelAppResponse(
+ code: (json['code'] as num).toInt(),
+ data: json['data'] == null
+ ? null
+ : ChannelAppData.fromJson(json['data'] as Map),
+ msg: json['msg'] as String,
+ );
+
+Map _$ChannelAppResponseToJson(ChannelAppResponse instance) =>
+ {
+ 'code': instance.code,
+ 'data': instance.data,
+ 'msg': instance.msg,
+ };
+
+ChannelAppData _$ChannelAppDataFromJson(Map json) =>
+ ChannelAppData(
+ app: AppInfo.fromJson(json['app'] as Map),
+ );
+
+Map _$ChannelAppDataToJson(ChannelAppData instance) =>
+ {
+ 'app': instance.app,
+ };
+
+AppInfo _$AppInfoFromJson(Map json) => AppInfo(
+ address: json['address'] as String,
+ channel: json['channel'] as String,
+ description: json['description'] as String,
+ version: json['version'] as String,
+ );
+
+Map _$AppInfoToJson(AppInfo instance) => {
+ 'address': instance.address,
+ 'channel': instance.channel,
+ 'description': instance.description,
+ 'version': instance.version,
+ };
diff --git a/lib/data/services/account_service.dart b/lib/data/services/account_service.dart
index 6c01ac65c..cbbe49de1 100644
--- a/lib/data/services/account_service.dart
+++ b/lib/data/services/account_service.dart
@@ -25,7 +25,7 @@ class AccountService extends GetxService {
Future clearUserData() async {
try {
// 清理所有存储
- await _storage.erase();
+ //await _storage.erase();
// 或者逐个清理关键存储项
await _storage.write("logintoken", null);
await _storage.write("user_info", null);
@@ -38,6 +38,18 @@ class AccountService extends GetxService {
}
}
+ Future clearAllUserData() async {
+ // 清理所有用户数据
+ try {
+ await _storage.erase();
+ logger.d('所有用户数据清理成功');
+ return true;
+ } catch (e) {
+ logger.e('用户数据清理失败: $e');
+ return false;
+ }
+ }
+
/// 退出第三方登录
///
/// 返回操作是否成功
@@ -105,7 +117,8 @@ class AccountService extends GetxService {
}
// 清理本地数据
- final dataCleaned = await clearUserData();
+ final dataCleaned = await clearAllUserData(); // 一次性清除所有数据
+
if (!dataCleaned) {
return AccountOperationResult(
success: false,
diff --git a/lib/data/services/db/database_helper.dart b/lib/data/services/db/database_helper.dart
index 0773d9c12..0d6c3aa54 100644
--- a/lib/data/services/db/database_helper.dart
+++ b/lib/data/services/db/database_helper.dart
@@ -39,14 +39,6 @@ class DatabaseHelper {
synctime INTEGER
)
''');
- // 操作记录
- batch.execute('''
- CREATE TABLE IF NOT EXISTS operationrecord (
- id INTEGER PRIMARY KEY,
- method TEXT,
- data TEXT
- )
- ''');
// 商务会议助手列表
batch.execute('''
CREATE TABLE IF NOT EXISTS meeting (
diff --git a/lib/data/services/db/sqflite_api.dart b/lib/data/services/db/sqflite_api.dart
index a5f0f1cb3..5857b3a7a 100644
--- a/lib/data/services/db/sqflite_api.dart
+++ b/lib/data/services/db/sqflite_api.dart
@@ -1,49 +1,10 @@
-import 'dart:convert';
-
import 'package:flutter/material.dart';
import 'package:get/get.dart';
-import 'package:get_storage/get_storage.dart';
+import '../../../core/utils/synchrodata.dart';
import 'database_helper.dart';
class SqfliteApi {
- // 保存操作记录
- static Future saveMeetingOperationRecord(
- String method,
- dynamic params,
- ) async {
- GetStorage storage = GetStorage();
- String userId = '';
- Map? userInfo = storage.read("user_info");
- if (userInfo != null) {
- userId = userInfo['user']['uid'];
- }
-
- final db = await DatabaseHelper().database;
- String data = '';
- if (method == 'records') {
- Map records = {
- ...params,
- 'id': '${userId}_${params['id']}',
- 'number': params['id'],
- 'uid': userId,
- };
- data = jsonEncode(records);
- } else {
- data = params;
- }
- db.insert('operationrecord', {
- 'method': method,
- 'data': data,
- });
- }
-
- // 操作记录列表
- static Future getMeetingOperationRecord() async {
- final db = await DatabaseHelper().database;
- return await db.query('operationrecord');
- }
-
// 获取商务会议助手列表
static Future getMeetingList(int offset) async {
final db = await DatabaseHelper().database;
@@ -112,6 +73,10 @@ class SqfliteApi {
'summary': '',
'mindmap': '',
});
+ Synchrodata.saveOperationRecord('insert', {
+ ...meeting,
+ 'id': id,
+ });
});
return id;
} catch (e) {
@@ -136,6 +101,10 @@ class SqfliteApi {
where: 'meetingid = ?',
whereArgs: [id],
);
+ Synchrodata.saveOperationRecord('update', {
+ ...meeting,
+ 'id': id,
+ });
return true;
} catch (e) {
Get.snackbar(
@@ -205,6 +174,10 @@ class SqfliteApi {
static Future editMeetingTitle(id, Map meeting) async {
final db = await DatabaseHelper().database;
await db.update('meeting', meeting, where: 'id = ?', whereArgs: [id]);
+ Synchrodata.saveOperationRecord('update', {
+ ...meeting,
+ 'id': id,
+ });
}
// 删除商务会议助手
@@ -216,6 +189,9 @@ class SqfliteApi {
.delete('meetingdetails', where: 'meetingid = ?', whereArgs: [id]);
await txn
.delete('meetingspeaker', where: 'meetingid = ?', whereArgs: [id]);
+ Synchrodata.saveOperationRecord('delete', {
+ 'id': id,
+ });
});
}
diff --git a/lib/data/services/network/api.dart b/lib/data/services/network/api.dart
index b2f43ac10..07e265b4e 100644
--- a/lib/data/services/network/api.dart
+++ b/lib/data/services/network/api.dart
@@ -44,14 +44,6 @@ class Api {
'/api/home/user_setting',
params: params,
);
- // options: params is dio.FormData
- // ? Options(
- // headers: {
- // 'Content-Type': 'multipart/form-data',
- // },
- // )
- // : null,
- // );
}
//注销账号
@@ -107,4 +99,31 @@ class Api {
params: params,
);
}
+
+ // 上传会议助手操作记录
+ static putOperationRecord([params]) {
+ return DioManager().request(
+ NWMethod.post,
+ '/api/home/record_put',
+ params: params,
+ );
+ }
+
+ // 获取会议助手数据
+ static getOperationRecord([params]) {
+ return DioManager().request(
+ NWMethod.post,
+ '/api/home/record_get',
+ params: params,
+ );
+ }
+
+ //获取渠道包信息
+ static getChannelApp([params]) {
+ return DioManager().request(
+ NWMethod.post,
+ '/api/home/user_getchannelapp',
+ params: params,
+ );
+ }
}
diff --git a/lib/data/services/version_update_service.dart b/lib/data/services/version_update_service.dart
new file mode 100644
index 000000000..f0bf31f05
--- /dev/null
+++ b/lib/data/services/version_update_service.dart
@@ -0,0 +1,315 @@
+import 'dart:io';
+import 'package:flutter/foundation.dart';
+import 'package:get/get.dart';
+import 'package:package_info_plus/package_info_plus.dart';
+import 'package:url_launcher/url_launcher.dart';
+import 'package:logger/logger.dart';
+import 'package:device_info_plus/device_info_plus.dart';
+import '../models/channel_app_model.dart';
+import 'network/api.dart';
+
+/// 版本更新服务类
+class VersionUpdateService extends GetxService {
+ static VersionUpdateService get to => Get.find();
+
+ final Logger _logger = Logger();
+
+ /// 获取当前应用版本信息
+ Future getCurrentVersion() async {
+ try {
+ final packageInfo = await PackageInfo.fromPlatform();
+ return '${packageInfo.version}+${packageInfo.buildNumber}';
+ } catch (e) {
+ _logger.e('获取当前版本信息失败: $e');
+ return '1.0.0';
+ }
+ }
+
+ /// 获取当前平台信息
+ Future getCurrentPlatformInfo() async {
+ if (kIsWeb) {
+ return const PlatformInfo(
+ platform: 'web',
+ storeType: AppStoreType.official,
+ channel: '官方网站',
+ );
+ }
+
+ if (Platform.isIOS) {
+ return const PlatformInfo(
+ platform: 'ios',
+ storeType: AppStoreType.appStore,
+ channel: 'App Store',
+ );
+ }
+
+ if (Platform.isAndroid) {
+ // 根据设备品牌判断应用商店类型
+ return await getAndroidStoreInfoByBrand();
+ }
+
+ return const PlatformInfo(
+ platform: 'unknown',
+ storeType: AppStoreType.unknown,
+ channel: '未知',
+ );
+ }
+
+ /// 根据设备品牌获取应用商店信息
+ Future getAndroidStoreInfoByBrand() async {
+ try {
+ final deviceInfo = DeviceInfoPlugin();
+ final androidInfo = await deviceInfo.androidInfo;
+ final brand = androidInfo.brand.toLowerCase();
+
+ return const PlatformInfo(
+ platform: 'android',
+ storeType: AppStoreType.official,
+ channel: '官方内测',
+ );
+
+ // switch (brand) {
+ // case 'huawei':
+ // case 'honor':
+ // return PlatformInfo(
+ // platform: 'android',
+ // storeType:
+ // brand == 'huawei' ? AppStoreType.huawei : AppStoreType.honor,
+ // channel: brand == 'huawei' ? '华为应用市场' : '荣耀应用市场',
+ // );
+ // case 'xiaomi':
+ // case 'redmi':
+ // return const PlatformInfo(
+ // platform: 'android',
+ // storeType: AppStoreType.xiaomi,
+ // channel: '小米应用商店',
+ // );
+ // case 'oppo':
+ // case 'oneplus':
+ // return const PlatformInfo(
+ // platform: 'android',
+ // storeType: AppStoreType.oppo,
+ // channel: 'OPPO软件商店',
+ // );
+ // case 'vivo':
+ // case 'iqoo':
+ // return const PlatformInfo(
+ // platform: 'android',
+ // storeType: AppStoreType.vivo,
+ // channel: 'vivo应用商店',
+ // );
+ // default:
+ // return const PlatformInfo(
+ // platform: 'android',
+ // storeType: AppStoreType.googlePlay,
+ // channel: 'Google Play',
+ // );
+ // }
+ } catch (e) {
+ _logger.e('获取Android设备信息失败: $e');
+ return const PlatformInfo(
+ platform: 'android',
+ storeType: AppStoreType.googlePlay,
+ channel: 'Google Play',
+ );
+ }
+ }
+
+ /// 检查版本更新
+ Future checkForUpdate() async {
+ try {
+ final currentVersion = await getCurrentVersion();
+ final platformInfo = await getCurrentPlatformInfo();
+
+ // 调用后端接口获取最新版本信息
+ final response =
+ await Api.getChannelApp({'channel': platformInfo.channel});
+
+ print('获取渠道包信息:$response');
+
+ if (response == null || !response.containsKey('app')) {
+ return VersionUpdateInfo(
+ hasUpdate: false,
+ currentVersion: currentVersion,
+ latestVersion: currentVersion,
+ updateType: UpdateType.none,
+ );
+ }
+
+ //final channelAppResponse = ChannelAppResponse.fromJson(response);
+ // 直接从response中获取app信息
+ final appInfo = AppInfo.fromJson(response['app']);
+
+ // if (channelAppResponse.code != 0 || channelAppResponse.data == null) {
+ // _logger.w('获取版本信息失败: ${channelAppResponse.msg}');
+ // return VersionUpdateInfo(
+ // hasUpdate: false,
+ // currentVersion: currentVersion,
+ // latestVersion: currentVersion,
+ // updateType: UpdateType.none,
+ // );
+ // }
+
+ // final appInfo = channelAppResponse.data!.app;
+ final hasUpdate = _compareVersions(currentVersion, appInfo.version);
+
+ return VersionUpdateInfo(
+ hasUpdate: hasUpdate,
+ currentVersion: currentVersion,
+ latestVersion: appInfo.version,
+ appInfo: appInfo,
+ updateType: hasUpdate ? UpdateType.optional : UpdateType.none,
+ );
+ } catch (e) {
+ _logger.e('检查版本更新失败: $e');
+ final currentVersion = await getCurrentVersion();
+ return VersionUpdateInfo(
+ hasUpdate: false,
+ currentVersion: currentVersion,
+ latestVersion: currentVersion,
+ updateType: UpdateType.none,
+ );
+ }
+ }
+
+ /// 比较版本号
+ /// 返回true表示需要更新(远程版本更高)
+ bool _compareVersions(String current, String latest) {
+ // 拆分版本号与构建号
+ final currentParts = current.split('+');
+ final latestParts = latest.split('+');
+
+ // 解析版本号 (major.minor.patch)
+ final currentVersions = currentParts[0].split('.').map(int.parse).toList();
+ final latestVersions = latestParts[0].split('.').map(int.parse).toList();
+
+ // 逐级比较版本号
+ for (var i = 0; i < currentVersions.length; i++) {
+ if (latestVersions[i] > currentVersions[i]) return true;
+ if (latestVersions[i] < currentVersions[i]) return false;
+ }
+
+ // 版本号相同则比较构建号
+ if (latestParts.length > 1 && currentParts.length > 1) {
+ return int.parse(latestParts[1]) > int.parse(currentParts[1]);
+ }
+ return false;
+ }
+
+ /// 解析版本号字符串为数字数组
+ List _parseVersion(String version) {
+ final parts = version.split('.');
+ final result = [];
+
+ for (int i = 0; i < 3; i++) {
+ if (i < parts.length) {
+ result.add(int.tryParse(parts[i]) ?? 0);
+ } else {
+ result.add(0);
+ }
+ }
+
+ return result;
+ }
+
+ /// 执行更新操作
+ Future performUpdate(VersionUpdateInfo updateInfo) async {
+ if (!updateInfo.hasUpdate || updateInfo.appInfo == null) {
+ return false;
+ }
+
+ final platformInfo = await getCurrentPlatformInfo();
+
+ try {
+ switch (platformInfo.storeType) {
+ case AppStoreType.appStore:
+ return await _openAppStore();
+ case AppStoreType.googlePlay:
+ return await _openGooglePlay();
+ case AppStoreType.huawei:
+ return await _openHuaweiStore();
+ case AppStoreType.xiaomi:
+ return await _openXiaomiStore();
+ case AppStoreType.oppo:
+ return await _openOppoStore();
+ case AppStoreType.vivo:
+ return await _openVivoStore();
+ case AppStoreType.honor:
+ return await _openHonorStore();
+ case AppStoreType.official:
+ return await _openOfficialDownload(updateInfo.appInfo!.address);
+ default:
+ return await _openOfficialDownload(updateInfo.appInfo!.address);
+ }
+ } catch (e) {
+ _logger.e('执行更新失败: $e');
+ return false;
+ }
+ }
+
+ /// 打开iOS App Store
+ Future _openAppStore() async {
+ // 这里需要替换为实际的App Store ID
+ const appStoreUrl = 'https://apps.apple.com/app/id你的应用ID';
+ return await _launchUrl(appStoreUrl);
+ }
+
+ /// 打开Google Play
+ Future _openGooglePlay() async {
+ // 这里需要替换为实际的包名
+ const packageName = 'com.yourcompany.deepvoice';
+ const playStoreUrl =
+ 'https://play.google.com/store/apps/details?id=$packageName';
+ return await _launchUrl(playStoreUrl);
+ }
+
+ /// 打开华为应用市场
+ Future _openHuaweiStore() async {
+ const huaweiStoreUrl = 'appmarket://details?id=com.yourcompany.deepvoice';
+ return await _launchUrl(huaweiStoreUrl);
+ }
+
+ /// 打开小米应用商店
+ Future _openXiaomiStore() async {
+ const xiaomiStoreUrl = 'mimarket://details?id=com.yourcompany.deepvoice';
+ return await _launchUrl(xiaomiStoreUrl);
+ }
+
+ /// 打开OPPO软件商店
+ Future _openOppoStore() async {
+ const oppoStoreUrl =
+ 'oppomarket://details?packagename=com.yourcompany.deepvoice';
+ return await _launchUrl(oppoStoreUrl);
+ }
+
+ /// 打开vivo应用商店
+ Future _openVivoStore() async {
+ const vivoStoreUrl = 'vivomarket://details?id=com.yourcompany.deepvoice';
+ return await _launchUrl(vivoStoreUrl);
+ }
+
+ /// 打开荣耀应用市场
+ Future _openHonorStore() async {
+ const honorStoreUrl = 'honormarket://details?id=com.yourcompany.deepvoice';
+ return await _launchUrl(honorStoreUrl);
+ }
+
+ /// 打开官网下载
+ Future _openOfficialDownload(String downloadUrl) async {
+ return await _launchUrl(downloadUrl);
+ }
+
+ /// 启动URL
+ Future _launchUrl(String url) async {
+ try {
+ final uri = Uri.parse(url);
+ if (await canLaunchUrl(uri)) {
+ return await launchUrl(uri, mode: LaunchMode.externalApplication);
+ }
+ return false;
+ } catch (e) {
+ _logger.e('启动URL失败: $e');
+ return false;
+ }
+ }
+}
diff --git a/lib/modules/FTFTranslation/README.md b/lib/modules/FTFTranslation/README.md
new file mode 100644
index 000000000..c41004a52
--- /dev/null
+++ b/lib/modules/FTFTranslation/README.md
@@ -0,0 +1,160 @@
+# Realtime 实时语音聊天模块
+
+## ⚠️ 当前状态
+
+**此模块为功能预览版本,提供完整的实时语音对话功能。**
+
+通过原生插件实现语音识别、AI对话和语音合成的完整语音交互体验。
+
+## 概述
+
+Realtime模块实现了类似ChatGPT app的实时语音对话功能,当前版本提供:
+
+- ✅ **完整语音交互** - 实时语音识别、AI对话和语音合成
+- ✅ **状态管理** - 聆听、思考、回答状态的可视化
+- ✅ **动画效果** - 基于Shader的流动云效果,支持音频振幅驱动
+- ✅ **字幕显示** - 可选择开启/关闭的字幕模式
+- ✅ **智能UI** - 根据连接状态自动调整按钮可用性
+
+## 界面设计
+
+### 布局结构
+- **顶部状态区域** - 显示当前状态和字幕模式切换按钮
+- **中间可视化区域** - 圆形Shader动画,根据音频振幅呈现呼吸效果
+- **底部控制区域** - 主控制按钮和状态指示器
+
+### 状态指示
+- 🔵 **聆听状态** - 蓝色动画,接收用户语音输入
+- 🟠 **思考状态** - 橙色动画,AI处理中
+- 🟢 **回答状态** - 绿色动画,AI语音输出
+- ⚪ **空闲状态** - 灰色静态,等待交互
+
+### 字幕模式
+- **默认关闭** - 初始状态下不显示任何字幕内容
+- **手动切换** - 通过右上角按钮手动开启/关闭字幕
+- **完整隐藏** - 关闭时字幕区域完全不显示,不占用界面空间
+- **智能布局** - 开启时圆形动画自动上移,字幕区域占据下半屏
+
+### 交互控制
+- **录音按钮** - 仅在成功连接AI服务器且开始录音后可用
+- **连接状态** - 未连接时录音按钮显示不可用状态
+- **状态反馈** - 按钮颜色和可点击性根据连接状态动态调整
+
+## 使用方法
+
+### 路由导航
+
+```dart
+// 跳转到实时语音对话页面
+Get.toNamed(Routes.realtime);
+```
+
+### 交互流程
+
+1. **自动连接** - 页面加载后自动尝试连接AI服务器
+2. **等待连接** - 录音按钮在连接成功前保持不可用状态
+3. **开始录音** - 连接成功后自动开始录音,录音按钮变为可用
+4. **语音对话** - 用户说话 → AI处理 → 语音回复
+5. **字幕控制** - 可随时通过右上角按钮切换字幕显示
+
+## 技术架构
+
+### 控制器 (RealtimeController)
+- **连接管理**: WebSocket连接到AI服务器
+- **状态同步**: `isConnected`, `isListening`, `isSpeaking`
+- **字幕控制**: `isSubtitleMode` 默认为false,用户手动控制
+- **消息管理**: 智能去重,避免流式响应重复显示
+- **音频处理**: 原生层计算RMS值,Flutter层接收并控制动画
+
+### 视图组件
+- **RealtimeView** - 主视图容器,响应式布局
+- **ShaderMicFlow** - 基于GLSL Shader的流动云动画组件
+- **AutoScrollList** - 自动滚动的字幕列表
+
+### 原生集成
+- **RealtimeService** - 原生音频处理和WebSocket通信
+- **音频格式** - 16kHz, 1声道, 16位PCM
+- **实时传输** - 低延迟音频流处理
+- **RMS计算** - 原生层实时计算音频振幅
+
+## 默认行为
+
+### 字幕模式
+- **初始状态**: 关闭 (`isSubtitleMode.value = false`)
+- **显示逻辑**: 仅在用户手动开启时显示字幕内容
+- **空间占用**: 关闭时字幕区域高度为0,不影响其他UI元素
+- **切换效果**: 平滑动画过渡,圆形动画位置和大小同步调整
+
+### 按钮状态
+- **录音按钮**: 默认不可用,连接成功且开始录音后才可点击
+- **状态指示**: 通过颜色和图标变化反映当前可用性
+- **交互反馈**: 不可用时点击无效果,避免误操作
+
+## 开发计划
+
+### 已完成功能 ✅
+- [x] 完整语音交互流程
+- [x] 实时WebSocket通信
+- [x] Shader流动云动画
+- [x] RMS音频振幅计算
+- [x] 智能UI状态管理
+- [x] 字幕模式控制
+- [x] 消息去重优化
+
+### 计划增强 ⏳
+- [ ] 多语言语音识别
+- [ ] 语音中断检测
+- [ ] 音频质量自适应
+- [ ] 离线降级处理
+- [ ] 语音情感识别
+
+## 注意事项
+
+1. **网络依赖** - 需要连接到AI服务器 (ws://192.168.1.11:8000/ws)
+2. **麦克风权限** - 需要获取麦克风使用权限
+3. **音频会话** - 自动管理音频会话,支持蓝牙设备
+4. **资源管理** - 页面关闭时自动清理音频资源和网络连接
+
+## 自定义配置
+
+### 修改服务器地址
+
+```dart
+// 在RealtimeController中修改连接URL
+await _realtimeService.initialize(
+ serverUrl: 'ws://your-server:port/ws',
+ // ... 其他参数
+);
+```
+
+### 调整UI布局
+
+```dart
+// 修改字幕模式下圆形动画位置
+alignment: controller.isSubtitleMode.value
+ ? const Alignment(0, -0.9) // 调整Y轴位置
+ : const Alignment(0, -0.25),
+```
+
+### 音频参数调优
+
+```dart
+// 在初始化时调整音频参数
+sampleRate: 16000, // 采样率
+channels: 1, // 声道数
+bitsPerSample: 16, // 位深度
+```
+
+## 技术栈
+
+- **Flutter SDK**: >= 3.0.0
+- **GetX**: >= 4.6.5 (状态管理)
+- **Flutter Shaders**: >= 0.0.6 (Shader动画)
+- **原生插件**: Realtime服务 (音频处理)
+- **WebSocket**: 实时通信协议
+
+## 版本信息
+
+- **当前版本**: v1.0.0 (Production)
+- **最后更新**: 2024年
+- **状态**: 完整功能实现
\ No newline at end of file
diff --git a/lib/modules/FTFTranslation/bindings/FTFTranslation_binding.dart b/lib/modules/FTFTranslation/bindings/FTFTranslation_binding.dart
new file mode 100644
index 000000000..85e44791c
--- /dev/null
+++ b/lib/modules/FTFTranslation/bindings/FTFTranslation_binding.dart
@@ -0,0 +1,10 @@
+import 'package:get/get.dart';
+import '../controllers/FTFTranslation_controller.dart';
+
+class FTFTranslationBinding extends Bindings {
+ @override
+ void dependencies() {
+ // 注册实时语音聊天控制器
+ Get.lazyPut(() => FTFTranslationController());
+ }
+}
diff --git a/lib/modules/FTFTranslation/controllers/FTFTranslation_controller.dart b/lib/modules/FTFTranslation/controllers/FTFTranslation_controller.dart
new file mode 100644
index 000000000..53cffe68d
--- /dev/null
+++ b/lib/modules/FTFTranslation/controllers/FTFTranslation_controller.dart
@@ -0,0 +1,241 @@
+import 'dart:async';
+import 'dart:typed_data';
+
+import 'package:get/get.dart';
+import '../../../core/utils/logger.dart';
+import '../../../data/services/asr_service.dart';
+import '../../../data/services/speech_impl/xunfei_asr_service.dart';
+import '../../../data/services/tts_service.dart';
+import '../../../data/services/language_manager.dart';
+import '../../../data/services/volcano_translation_service.dart';
+
+class FTFTranslationController extends GetxController
+ with GetSingleTickerProviderStateMixin {
+ final messages = [].obs;
+ final isChineseInput = true.obs;
+ final leftLanguage = '中文'.obs;
+ final rightLanguage = 'English'.obs;
+ final isLeftInput = true.obs;
+
+ final supportedLanguages = const [
+ '中文',
+ 'English',
+ '日本語',
+ '한국어',
+ 'Español',
+ ];
+
+ late final AsrService _asrService;
+
+ StreamSubscription? _recognitionSubscription;
+
+ RxString intermediateContent = ''.obs;
+ RxString finalContent = ''.obs;
+ BytesBuilder _originaBytes = BytesBuilder(); //原始音频
+
+ final VolcanoTranslationService _translationService =
+ Get.find();
+ final LanguageManager _languageManager = Get.find();
+ // 当前使用的TTS服务
+ late TtsService _ttsService;
+ final isTranslating = false.obs;
+ final sourceLanguageCode = 'zh-CN'.obs;
+ final targetLanguageCode = 'en-US'.obs;
+ final isTtsEnabled = true.obs;
+
+ @override
+ void onInit() {
+ super.onInit();
+ Get.put(XunfeiAsrService());
+ final TtsService _ttsService = Get.find();
+ _asrService = Get.find();
+ }
+
+ @override
+ void onClose() {
+ _asrService.dispose();
+ _recognitionSubscription?.cancel();
+ super.onClose();
+ }
+
+ //开始录音
+ Future _startRecorder() async {
+ final recognitionStream =
+ await _asrService.startContinuousRecognition(false);
+ _recognitionSubscription = recognitionStream.listen(
+ _handleRecognitionEvent,
+ );
+ }
+
+ //结束录音
+ Future _stopRecorder() async {
+ await _asrService.stopContinuousRecognition();
+ }
+
+ // 处理语音识别事件
+ void _handleRecognitionEvent(RecognitionEvent event) {
+ switch (event.type) {
+ case RecognitionEventType.intermediateResult:
+ intermediateContent.value = event.text;
+ handleFinalResult(event.text);
+ break;
+ case RecognitionEventType.finalResult:
+ finalContent.value += _spliceText(event);
+ intermediateContent.value = '';
+ _originaBytes.add(event.audio!);
+ handleIntermediateResult(event.text);
+ break;
+ default:
+ // 不做任何处理
+ break;
+ }
+ }
+
+ // 翻译文本
+ Future translateText(String sourceText,
+ {bool isFinal = false}) async {
+ if (sourceText.isEmpty) return null;
+
+ isTranslating.value = true;
+ String? translationResult;
+
+ try {
+ // 根据检测到的语言确定源语言和目标语言
+ final bool shouldSwap =
+ intermediateContent.value == sourceLanguageCode.value;
+ final detectedSourceLanguageCode =
+ shouldSwap ? sourceLanguageCode.value : targetLanguageCode.value;
+ final detectedTargetLanguageCode =
+ shouldSwap ? targetLanguageCode.value : sourceLanguageCode.value;
+
+ // 调用翻译服务
+ translationResult = await _translationService.translateText(
+ text: sourceText,
+ sourceLanguageCode: detectedSourceLanguageCode,
+ targetLanguageCode: detectedTargetLanguageCode,
+ );
+
+ if (translationResult != null) {
+ // 查找匹配的项目 - 优先使用timestamp查找
+ int index = -1;
+
+ // 播放TTS - 只在最终结果时播放
+ if (isTtsEnabled.value && isFinal) {
+ // Logger.info('播放翻译文本: ${translationResult}');
+ await playTranslatedText(translationResult);
+ }
+ }
+ } catch (e) {
+ Logger.error('翻译失败: ${e.toString()}');
+ } finally {
+ isTranslating.value = false;
+ }
+
+ return translationResult;
+ }
+
+ // 处理中间识别结果
+ void handleIntermediateResult(String text) {
+ if (text.isEmpty) return;
+
+ translateText(text, isFinal: false);
+ }
+
+ // 处理最终识别结果
+ Future handleFinalResult(String text) async {
+ if (text.isEmpty) return;
+
+ // 使用项目的时间戳进行翻译
+ translateText(text, isFinal: true);
+ }
+
+ // 播放翻译文本
+ Future playTranslatedText(String text) async {
+ if (text.isEmpty) return;
+
+ try {
+ final bool shouldSwap =
+ intermediateContent.value == targetLanguageCode.value;
+ final detectedTargetLanguageCode =
+ shouldSwap ? sourceLanguageCode.value : targetLanguageCode.value;
+ final voiceName = _languageManager
+ .getTtsVoiceNameByAsrCode(detectedTargetLanguageCode) ??
+ 'en-US-AriaNeural';
+
+ await _ttsService.setVoice(voiceName);
+ await _ttsService.speakOnce(text);
+ } catch (e) {
+ Logger.error('播放翻译失败: ${e.toString()}');
+ }
+ }
+
+ String _spliceText(RecognitionEvent event) {
+ String eventText = event.text;
+ String text = '';
+ if (finalContent.value.isNotEmpty) {
+ if (event.role != '') {
+ final punctuations = {
+ ',',
+ '。',
+ '!',
+ '?',
+ ';',
+ ':',
+ ',',
+ '.',
+ ':',
+ ';',
+ '!',
+ '?'
+ };
+ final firstChar = eventText[0];
+ if (punctuations.contains(firstChar)) {
+ text += '$firstChar\n';
+ eventText = eventText.substring(1);
+ } else {
+ text += '\n';
+ }
+ text += '说话人${event.role}:';
+ }
+ } else {
+ text += '说话人${event.role}:';
+ }
+ text += eventText;
+ return text;
+ }
+
+ void addMessage(String text) {
+ final fromLang =
+ isLeftInput.value ? leftLanguage.value : rightLanguage.value;
+ final toLang = isLeftInput.value ? rightLanguage.value : leftLanguage.value;
+
+ messages.add(
+ FTFTranslationMessage(
+ original: text,
+ translated: '[$fromLang → $toLang] $text (翻译模拟)',
+ isChineseSpeaker: isLeftInput.value,
+ ),
+ );
+ }
+
+ void swapLanguages() {
+ final temp = leftLanguage.value;
+ leftLanguage.value = rightLanguage.value;
+ rightLanguage.value = temp;
+ isLeftInput.value = !isLeftInput.value;
+ }
+
+ void toggleInputSide(bool isLeft) => isLeftInput.value = isLeft;
+}
+
+class FTFTranslationMessage {
+ final String original;
+ final String translated;
+ final bool isChineseSpeaker;
+
+ FTFTranslationMessage({
+ required this.original,
+ required this.translated,
+ required this.isChineseSpeaker,
+ });
+}
diff --git a/lib/modules/FTFTranslation/views/FTFTranslation_view.dart b/lib/modules/FTFTranslation/views/FTFTranslation_view.dart
new file mode 100644
index 000000000..84d8c1c6a
--- /dev/null
+++ b/lib/modules/FTFTranslation/views/FTFTranslation_view.dart
@@ -0,0 +1,212 @@
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import '../controllers/FTFTranslation_controller.dart';
+
+class FTFTranslationView extends GetView {
+ const FTFTranslationView({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ final TextEditingController inputController = TextEditingController();
+
+ return Scaffold(
+ backgroundColor: const Color(0xFF0E0F12),
+ appBar: AppBar(
+ backgroundColor: Colors.black,
+ title: const Text('面对面翻译'),
+ centerTitle: true,
+ ),
+ body: Column(
+ children: [
+ Expanded(
+ child: Obx(() => ListView.builder(
+ padding: const EdgeInsets.all(16),
+ itemCount: controller.messages.length,
+ itemBuilder: (context, index) {
+ final msg = controller.messages[index];
+ final isChinese = msg.isChineseSpeaker;
+ return Align(
+ alignment: isChinese
+ ? Alignment.centerLeft
+ : Alignment.centerRight,
+ child: Container(
+ margin: const EdgeInsets.symmetric(vertical: 6),
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: isChinese ? Colors.blue : Colors.grey[800],
+ borderRadius: BorderRadius.circular(12),
+ ),
+ constraints: const BoxConstraints(maxWidth: 300),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ msg.original,
+ style: const TextStyle(
+ color: Colors.white,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ msg.translated,
+ style: TextStyle(
+ color: Colors.white.withOpacity(0.9),
+ fontSize: 14,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ },
+ )),
+ ),
+ _buildBottomInput(inputController),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildBottomInput(TextEditingController inputController) {
+ return Obx(() {
+ final isLeftInput = controller.isLeftInput.value;
+ final inputLang = isLeftInput
+ ? controller.leftLanguage.value
+ : controller.rightLanguage.value;
+
+ return Container(
+ color: Colors.black,
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ child: Column(
+ children: [
+ Row(
+ children: [
+ Expanded(
+ child: TextField(
+ controller: inputController,
+ style: const TextStyle(color: Colors.white),
+ decoration: InputDecoration(
+ filled: true,
+ fillColor: Colors.grey[900],
+ hintText: '请输入 $inputLang 内容',
+ hintStyle: const TextStyle(color: Colors.grey),
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8),
+ borderSide: BorderSide.none,
+ ),
+ contentPadding:
+ const EdgeInsets.symmetric(horizontal: 12),
+ ),
+ ),
+ ),
+ IconButton(
+ onPressed: () {
+ final text = inputController.text.trim();
+ if (text.isNotEmpty) {
+ controller.addMessage(text);
+ inputController.clear();
+ }
+ },
+ icon: const Icon(Icons.send, color: Colors.white),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ Row(
+ children: [
+ Expanded(
+ child: _buildLanguageDropdown(
+ selected: controller.leftLanguage.value,
+ items: controller.supportedLanguages,
+ onChanged: (value) {
+ if (value != null) controller.leftLanguage.value = value;
+ },
+ icon: Icons.headphones,
+ isSelected: isLeftInput,
+ label: '左方语言',
+ onTapInputSide: () => controller.toggleInputSide(true),
+ ),
+ ),
+ const SizedBox(width: 12),
+ GestureDetector(
+ onTap: controller.swapLanguages,
+ child: Container(
+ padding: const EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ color: Colors.white10,
+ shape: BoxShape.circle,
+ ),
+ child:
+ const Icon(Icons.compare_arrows, color: Colors.white),
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: _buildLanguageDropdown(
+ selected: controller.rightLanguage.value,
+ items: controller.supportedLanguages,
+ onChanged: (value) {
+ if (value != null) controller.rightLanguage.value = value;
+ },
+ icon: Icons.smartphone,
+ isSelected: !isLeftInput,
+ label: '右方语言',
+ onTapInputSide: () => controller.toggleInputSide(false),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ );
+ });
+ }
+
+ Widget _buildLanguageDropdown({
+ required String selected,
+ required List items,
+ required void Function(String?) onChanged,
+ required IconData icon,
+ required bool isSelected,
+ required String label,
+ required VoidCallback onTapInputSide,
+ }) {
+ return GestureDetector(
+ onTap: onTapInputSide,
+ child: Container(
+ padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
+ decoration: BoxDecoration(
+ border: Border.all(
+ color: isSelected ? Colors.white : Colors.grey,
+ ),
+ borderRadius: BorderRadius.circular(24),
+ color: isSelected ? Colors.white10 : Colors.transparent,
+ ),
+ child: Row(
+ children: [
+ Icon(icon, color: Colors.white),
+ const SizedBox(width: 8),
+ Expanded(
+ child: DropdownButton(
+ dropdownColor: Colors.grey[900],
+ value: selected,
+ isExpanded: true,
+ underline: const SizedBox(),
+ iconEnabledColor: Colors.white,
+ style: const TextStyle(color: Colors.white),
+ items: items.map((lang) {
+ return DropdownMenuItem(
+ value: lang,
+ child: Text(lang),
+ );
+ }).toList(),
+ onChanged: onChanged,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart
index 4a77fbefc..2bbaa3d78 100644
--- a/lib/modules/agent/controllers/agent_controller.dart
+++ b/lib/modules/agent/controllers/agent_controller.dart
@@ -91,6 +91,17 @@ class AgentController extends GetxController {
_loadTtsPreference(); //加载TTS偏好设置
_loadChatHistory(); //加载聊天历史记录
_subscribeToEvents(); //订阅代理服务事件
+ _initMusicStatus(); //初始化音乐状态
+ }
+
+ // 初始化音乐播放状态
+ Future _initMusicStatus() async {
+ try {
+ await musicStatus();
+ Logger.d('AgentController', '音乐状态初始化完成');
+ } catch (e) {
+ Logger.e('AgentController', '音乐状态初始化失败: $e');
+ }
}
@override
@@ -818,85 +829,161 @@ class AgentController extends GetxController {
useImagePrompt('请翻译这张图片中的文字');
}
- // 播放音乐
- void musicStatus() async {
- var status = await _musiceManager.getStatus();
- // debugPrint('获取当前音乐播放状态 $status');
- isPlaying.value = status['isPlaying'];
- currentId.value = status['id'] ?? 0;
+ // 获取音乐播放状态
+ Future musicStatus() async {
+ try {
+ var status = await _musiceManager.getStatus();
+ isPlaying.value = status['isPlaying'] ?? false;
+ currentId.value = status['id'] ?? 0;
+ Logger.d('AgentController',
+ '音乐状态更新: playing=${isPlaying.value}, url=${currentId.value}');
+ } catch (e) {
+ Logger.e('AgentController', '获取音乐状态失败: $e');
+ }
}
// 播放音乐
- void musicPlay(Map song) async {
- final id = song['id'] ?? "";
- final timestampMs = song['ts'] ?? 0;
- // 当前时间
- final now = DateTime.now();
- // 将时间戳转换为 DateTime
- final tsTime = DateTime.fromMillisecondsSinceEpoch(timestampMs).add(
- const Duration(minutes: 10), // 转换为北京时间
- );
- // 判断是否过期
- if (tsTime.isBefore(now)) {
- //获取新的连接
- var response = await Api.getMusicUrl({
- "ids": [id]
- });
- // 2. 将响应数据转换为User对象
- try {
- print('getMusicUrl 响应数据: ${response.toString()}'); // 调试输出原始数据
- final urldata = response['urls'][0] as Map;
- song["url"] = urldata['url'] ?? '';
- } catch (e) {
- Logger.e(TAG, '转换响应数据失败: $e');
- return;
+ Future musicPlay(Map song) async {
+ try {
+ Logger.d('AgentController', '开始播放音乐: ${song['title'] ?? song['name']}');
+
+ // 立即更新UI状态,提供即时反馈
+ isPlaying.value = true;
+ currentId.value = song['id'] ?? 0;
+
+ // 重置进度状态
+ currentPosition.value = 0;
+ duration.value = 0;
+ progress.value = 0.0;
+
+ final id = song['id'] ?? "";
+ final timestampMs = song['ts'] ?? 0;
+
+ // 检查URL是否需要刷新(只有当有时间戳时才检查)
+ if (timestampMs > 0) {
+ final now = DateTime.now();
+ final tsTime = DateTime.fromMillisecondsSinceEpoch(timestampMs).add(
+ const Duration(minutes: 10), // 转换为北京时间
+ );
+
+ // 判断是否过期
+ if (tsTime.isBefore(now)) {
+ Logger.d('AgentController', '音乐URL已过期,正在获取新URL...');
+ try {
+ var response = await Api.getMusicUrl({
+ "ids": [id]
+ });
+ print('getMusicUrl 响应数据: ${response.toString()}'); // 调试输出原始数据
+ final urldata = response['urls'][0] as Map;
+ song["url"] = urldata['url'] ?? '';
+ currentId.value = song['id'] ?? 0;
+ Logger.d('AgentController', '获取新URL成功');
+ } catch (e) {
+ Logger.e('AgentController', '获取新URL失败: $e');
+ // 如果获取新URL失败,尝试使用原URL
+ }
+ }
}
- }
- _musiceManager.play(song);
- isPlaying.value = true;
- currentId.value = song['id'] ?? 0;
- _startProgressTimer();
+ // 转换为MusicManager期望的格式
+ final musicData = {
+ 'id': song['id']?.toString() ?? '',
+ 'url': song['url']?.toString() ?? '',
+ 'title': song['title']?.toString() ?? song['name']?.toString() ?? '',
+ 'artist':
+ song['artist']?.toString() ?? song['sgener']?.toString() ?? '',
+ 'coverUrl':
+ song['coverUrl']?.toString() ?? song['image']?.toString() ?? '',
+ };
+
+ // 调用音乐管理器播放
+ await _musiceManager.play(musicData);
+
+ // 启动进度定时器
+ _startProgressTimer();
+
+ Logger.d('AgentController', '音乐播放启动成功');
+ } catch (e) {
+ Logger.e('AgentController', '播放音乐失败: $e');
+ // 发生错误时重置状态
+ isPlaying.value = false;
+ currentId.value = 0;
+ }
}
- // 播放音乐
- void musicPause() {
- _musiceManager.pause();
- isPlaying.value = false;
- _stopProgressTimer();
+ // 暂停音乐
+ Future musicPause() async {
+ try {
+ Logger.d('AgentController', '暂停音乐播放');
+ await _musiceManager.pause();
+ isPlaying.value = false;
+ _stopProgressTimer();
+ } catch (e) {
+ Logger.e('AgentController', '暂停音乐失败: $e');
+ }
}
- void musicResume() {
- _musiceManager.resume();
- isPlaying.value = true;
- _startProgressTimer();
+ // 恢复音乐播放
+ Future musicResume() async {
+ try {
+ Logger.d('AgentController', '恢复音乐播放');
+ await _musiceManager.resume();
+ isPlaying.value = true;
+ _startProgressTimer();
+ } catch (e) {
+ Logger.e('AgentController', '恢复音乐失败: $e');
+ }
}
// 启动进度更新定时器
void _startProgressTimer() {
_stopProgressTimer(); // 先停止现有定时器
- _progressTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
- if (isPlaying.value) {
+
+ Logger.d('AgentController', '启动播放进度定时器');
+ _progressTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) {
+ if (isPlaying.value && currentId.value != 0) {
updateMusicProgress();
- } else {
+ } else if (!isPlaying.value) {
+ // 如果不在播放状态,停止定时器
timer.cancel();
+ _progressTimer = null;
+ Logger.d('AgentController', '播放已停止,取消进度定时器');
}
});
}
// 停止进度更新定时器
void _stopProgressTimer() {
- _progressTimer?.cancel();
- _progressTimer = null;
+ if (_progressTimer != null) {
+ Logger.d('AgentController', '停止播放进度定时器');
+ _progressTimer?.cancel();
+ _progressTimer = null;
+ }
}
// 获取播放进度
Future updateMusicProgress() async {
try {
+ if (!isPlaying.value || currentId.value == 0) {
+ return; // 如果没有在播放,直接返回
+ }
+
var progressInfo = await _musiceManager.getProgress();
- currentPosition.value = progressInfo['currentPosition'] ?? 0;
- duration.value = progressInfo['duration'] ?? 0;
- progress.value = progressInfo['progress'] ?? 0.0;
+ final newPosition = progressInfo['currentPosition'] ?? 0;
+ final newDuration = progressInfo['duration'] ?? 0;
+ final newProgress = progressInfo['progress'] ?? 0.0;
+
+ // 只有当值发生变化时才更新,避免不必要的UI刷新
+ if (currentPosition.value != newPosition) {
+ currentPosition.value = newPosition;
+ }
+ if (duration.value != newDuration) {
+ duration.value = newDuration;
+ }
+ if ((progress.value - newProgress).abs() > 0.001) {
+ // 避免浮点数精度问题
+ progress.value = newProgress;
+ }
} catch (e) {
Logger.e('AgentController', '获取播放进度失败: $e');
}
diff --git a/lib/modules/agent/views/music_card.dart b/lib/modules/agent/views/music_card.dart
index ecdc7bbcd..f72af95be 100644
--- a/lib/modules/agent/views/music_card.dart
+++ b/lib/modules/agent/views/music_card.dart
@@ -95,19 +95,27 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url,
backgroundColor: Colors.grey,
size: 36.0,
strokeWidth: 2.0,
- onPlayPause: () {
- if (controller.isPlaying.value) {
- if (controller.currentId.value == id) {
- controller.musicPause();
- } else {
- controller.musicPlay(song);
- }
- } else {
- if (controller.currentId.value == id) {
- controller.musicResume();
+ onPlayPause: () async {
+ try {
+ if (controller.isPlaying.value) {
+ if (controller.currentId.value == id) {
+ // 当前歌曲正在播放,暂停它
+ await controller.musicPause();
+ } else {
+ // 其他歌曲正在播放,播放这首歌
+ await controller.musicPlay(song);
+ }
} else {
- controller.musicPlay(song);
+ if (controller.currentId.value == id) {
+ // 当前歌曲已暂停,恢复播放
+ await controller.musicResume();
+ } else {
+ // 播放新歌曲
+ await controller.musicPlay(song);
+ }
}
+ } catch (e) {
+ // 静默处理错误,避免影响用户体验
}
},
onSeek: isThisPlaying
diff --git a/lib/modules/common/widgets/version_update_dialog.dart b/lib/modules/common/widgets/version_update_dialog.dart
new file mode 100644
index 000000000..cb0a59f03
--- /dev/null
+++ b/lib/modules/common/widgets/version_update_dialog.dart
@@ -0,0 +1,248 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+import '../../../data/models/channel_app_model.dart';
+import '../../../data/services/version_update_service.dart';
+
+/// 版本更新提示对话框 - 禁止外部关闭
+class VersionUpdateDialog extends StatelessWidget {
+ final VersionUpdateInfo updateInfo;
+ final VoidCallback? onCancel;
+ final VoidCallback? onUpdate;
+
+ const VersionUpdateDialog({
+ super.key,
+ required this.updateInfo,
+ this.onCancel,
+ this.onUpdate,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final isDarkMode = Theme.of(context).brightness == Brightness.dark;
+
+ // 在强制更新时添加拦截层
+ Widget dialogContent = _buildDialogContent(isDarkMode);
+ //if (updateInfo.updateType == UpdateType.force) {
+ return PopScope(
+ canPop: false, // 完全禁用返回键关闭
+ child: dialogContent,
+ );
+ //}
+
+ //return dialogContent;
+ }
+
+ /// 构建对话框内容
+ Widget _buildDialogContent(bool isDarkMode) {
+ return Dialog(
+ // 关键修改:禁用外部点击关闭
+ //barrierDismissible: false,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(16.r),
+ ),
+ child: Container(
+ width: 320.w,
+ padding: EdgeInsets.all(24.w),
+ decoration: BoxDecoration(
+ color: isDarkMode ? Colors.grey[900] : Colors.white,
+ borderRadius: BorderRadius.circular(16.r),
+ ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ // 标题图标
+ Container(
+ width: 64.w,
+ height: 64.w,
+ decoration: BoxDecoration(
+ color: Colors.blue.withOpacity(0.1),
+ borderRadius: BorderRadius.circular(32.r),
+ ),
+ child: Icon(
+ Icons.system_update,
+ size: 32.sp,
+ color: Colors.blue,
+ ),
+ ),
+
+ SizedBox(height: 16.h),
+
+ // 标题
+ Text(
+ '发现新版本',
+ style: TextStyle(
+ fontSize: 20.sp,
+ fontWeight: FontWeight.bold,
+ color: isDarkMode ? Colors.white : Colors.black87,
+ ),
+ ),
+
+ SizedBox(height: 8.h),
+
+ // 版本信息
+ Text(
+ '${updateInfo.currentVersion} → ${updateInfo.latestVersion}',
+ style: TextStyle(
+ fontSize: 14.sp,
+ color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
+ ),
+ ),
+
+ SizedBox(height: 16.h),
+
+ // 更新描述
+ if (updateInfo.appInfo?.description.isNotEmpty == true) ...[
+ Container(
+ width: double.infinity,
+ padding: EdgeInsets.all(12.w),
+ decoration: BoxDecoration(
+ color: isDarkMode ? Colors.grey[800] : Colors.grey[100],
+ borderRadius: BorderRadius.circular(8.r),
+ ),
+ child: Text(
+ updateInfo.appInfo!.description,
+ style: TextStyle(
+ fontSize: 14.sp,
+ color: isDarkMode ? Colors.grey[300] : Colors.grey[700],
+ height: 1.4,
+ ),
+ ),
+ ),
+ SizedBox(height: 20.h),
+ ],
+
+ // 按钮区域
+ Row(
+ children: [
+ // 取消按钮(仅在非强制更新时显示)
+ if (updateInfo.updateType != UpdateType.force) ...[
+ Expanded(
+ child: TextButton(
+ onPressed: () {
+ Get.back();
+ onCancel?.call();
+ },
+ style: TextButton.styleFrom(
+ padding: EdgeInsets.symmetric(vertical: 12.h),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8.r),
+ side: BorderSide(
+ color: isDarkMode
+ ? Colors.grey[600]!
+ : Colors.grey[300]!,
+ ),
+ ),
+ ),
+ child: Text(
+ '稍后提醒',
+ style: TextStyle(
+ fontSize: 16.sp,
+ color:
+ isDarkMode ? Colors.grey[400] : Colors.grey[600],
+ ),
+ ),
+ ),
+ ),
+ SizedBox(width: 12.w),
+ ],
+
+ // 更新按钮
+ Expanded(
+ child: ElevatedButton(
+ onPressed: () async {
+ Get.back();
+ await _handleUpdate();
+ onUpdate?.call();
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.blue,
+ foregroundColor: Colors.white,
+ padding: EdgeInsets.symmetric(vertical: 12.h),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8.r),
+ ),
+ elevation: 0,
+ ),
+ child: Text(
+ _getUpdateButtonText(),
+ style: TextStyle(
+ fontSize: 16.sp,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ /// 获取更新按钮文本
+ String _getUpdateButtonText() {
+ switch (updateInfo.updateType) {
+ case UpdateType.force:
+ return '立即更新';
+ case UpdateType.optional:
+ return '立即更新';
+ default:
+ return '检查更新';
+ }
+ }
+
+ /// 处理更新操作
+ Future _handleUpdate() async {
+ try {
+ // 显示加载提示
+ Get.dialog(
+ const Center(
+ child: CircularProgressIndicator(),
+ ),
+ barrierDismissible: false,
+ );
+
+ final versionService = VersionUpdateService.to;
+ final success = await versionService.performUpdate(updateInfo);
+
+ // 关闭加载提示
+ Get.back();
+
+ if (!success) {
+ Get.snackbar(
+ '更新失败',
+ '无法打开应用商店,请手动前往应用商店更新',
+ snackPosition: SnackPosition.TOP,
+ backgroundColor: Colors.red.withOpacity(0.8),
+ colorText: Colors.white,
+ );
+ }
+ } catch (e) {
+ // 关闭加载提示
+ if (Get.isDialogOpen == true) {
+ Get.back();
+ }
+
+ Get.snackbar(
+ '更新失败',
+ '更新过程中发生错误,请稍后重试',
+ snackPosition: SnackPosition.TOP,
+ backgroundColor: Colors.red.withOpacity(0.8),
+ colorText: Colors.white,
+ );
+ }
+ }
+
+ /// 显示版本更新对话框
+ static Future show(VersionUpdateInfo updateInfo) async {
+ await Get.dialog(
+ VersionUpdateDialog(updateInfo: updateInfo),
+ // 关键修改:禁用外部点击关闭
+ barrierDismissible: false,
+ // 防止返回键关闭(在强制更新模式下)
+ //useRootNavigator: true,
+ );
+ }
+}
diff --git a/lib/modules/meeting/controllers/meeting_controller.dart b/lib/modules/meeting/controllers/meeting_controller.dart
index 13e88d22e..0058b31d8 100644
--- a/lib/modules/meeting/controllers/meeting_controller.dart
+++ b/lib/modules/meeting/controllers/meeting_controller.dart
@@ -12,7 +12,9 @@ import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../../core/utils/logger.dart';
+import '../../../core/utils/synchrodata.dart';
import '../../../core/utils/upload_oss.dart';
+import '../../../data/services/ble_manager.dart';
import '../../../data/services/db/sqflite_api.dart';
import '../../../core/utils/permission_util.dart';
@@ -32,14 +34,12 @@ class MeetingController extends GetxController {
List selected = [];
final appDocDir = getApplicationDocumentsDirectory();
- //final appopus = getExternalStorageDirectories();
- // final directory = Directory(
- // '/storage/emulated/0/Android/data/com.yunqiinnovation.deepsound/files');
List files = [];
TextEditingController titleController = TextEditingController(); // 录音标题
RxList dataList = [].obs;
-
+ // 蓝牙服务
+ final bleManager = Get.find();
List dataListTask = [];
bool isDisposed = false;
String filePath = '';
@@ -55,7 +55,6 @@ class MeetingController extends GetxController {
super.onInit();
listController.addListener(_onScroll);
_requestStoragePermission();
- //Permission.storage.request();
_headers = {
'Authorization': 'Bearer; $_token',
'Content-Type': 'application/json',
@@ -64,7 +63,7 @@ class MeetingController extends GetxController {
addMeetingAudio();
final Map args = Get.arguments ?? {};
// 检查参数是否存在且包含有效路径
- if (args != null &&
+ if (args.isNotEmpty &&
args.containsKey('filePath') &&
args['filePath'] is String &&
args['filePath'].isNotEmpty) {
@@ -82,6 +81,10 @@ class MeetingController extends GetxController {
super.onClose();
}
+ void onPopInvokedWithResult(didPop, result) {
+ Synchrodata.startSynchro();
+ }
+
void _onScroll() {
if (isGetMore &&
!isLoading &&
@@ -109,11 +112,8 @@ class MeetingController extends GetxController {
// 请求存储权限
void _requestStoragePermission() async {
- // final BuildContext context = Get.overlayContext!;
-
PermissionUtil.instance
.requestPermission(
- // context: context,
permissionType: Permission.storage,
permissionName: "存储",
explanationText: "我们需要访问存储权限,以便您可以选择并上传音频文件。",
@@ -124,18 +124,14 @@ class MeetingController extends GetxController {
Logger.d("Permission", "用户授予了存储权限");
} else {
Logger.d("Permission", "用户未授予存储权限");
- // 可以提示用户无法进行文件操作
}
});
}
void loadTranslatopus() async {
- // final appDir = await appopus; // 先 await 获取目录
final dir =
Directory("/data/user/0/com.yunqiinnovation.deepsound/files/OpusAudio");
- print("appDocDir=${dir?.path}");
-
files = dir
.listSync()
.where((f) => f is File && (f.path.endsWith('.opus')))
@@ -147,15 +143,12 @@ class MeetingController extends GetxController {
final appDir = await appDocDir; // 先 await 获取目录
final dir = Directory("${appDir.path}/TranslatAudio");
- print("appDocDir=${dir?.path}");
- // Create directory if it doesn't exist
if (!await dir.exists()) {
- await dir.create(recursive: true); // Create directory tree if needed
- files = []; // No files exist in new directory
+ await dir.create(recursive: true);
+ files = [];
return;
}
- // Directory exis
files = dir
.listSync()
.where((f) =>
@@ -168,20 +161,12 @@ class MeetingController extends GetxController {
f.path.endsWith('.mov')))
.toList();
} catch (e) {
- print("Error loading files: $e");
- files = []; // Return empty list on error
+ files = [];
}
}
-// 导入音频文件
Future shareAllTranslatAudio() async {
final filePath = selected[0];
- // for (final filePath in selected) {
- // if (filePath.isEmpty) {
- // EasyLoading.showToast('没有可分享的音频文件');
- // return;
- // }
- // }
try {
await Share.shareXFiles(
[XFile(filePath)],
@@ -216,7 +201,7 @@ class MeetingController extends GetxController {
final files =
await sourceDir.list().where((entity) => entity is File).toList();
- int addQuantity = 0;
+ int addUp = 0;
for (final fileEntity in files) {
try {
final file = File(fileEntity.path);
@@ -225,8 +210,6 @@ class MeetingController extends GetxController {
// 设置音频源获取时长
await _audioPlayer.setAudioSource(AudioSource.file(filePath));
- print(
- "_audioPlayer$_audioPlayer,filePath=$filePath,_audioPlayer.duration?.inSeconds${_audioPlayer.duration?.inSeconds}");
int seconds = _audioPlayer.duration?.inSeconds ?? 0;
// 获取文件修改时间
@@ -251,7 +234,6 @@ class MeetingController extends GetxController {
'tasktype': 0,
'creationtime': creationTime,
};
- print("导入数据: $data");
// 插入数据库
final meetingId = await SqfliteApi.insertMeeting(data);
@@ -268,12 +250,13 @@ class MeetingController extends GetxController {
await file.delete();
addQuantity++;
+ addUp++;
}
} catch (e) {
print("文件处理失败: ${fileEntity.path}, 错误: $e");
}
}
- if (addQuantity > 0) EasyLoading.showSuccess('成功导入本地 $addQuantity 条录音');
+ if (addUp > 0) EasyLoading.showSuccess('成功导入本地 $addUp 条录音');
} catch (e) {
EasyLoading.showError('导入失败: $e');
} finally {
@@ -292,7 +275,6 @@ class MeetingController extends GetxController {
filepath: filePath, rootDir: 'TranslatAudio');
int seconds = _audioPlayer.duration?.inSeconds ?? 0;
final fileName = filePath.split('/').last;
- // int creationTime = DateTime.now().millisecondsSinceEpoch;
Map data = {
'title': fileName,
'type': 'TRANSLAT',
@@ -302,7 +284,6 @@ class MeetingController extends GetxController {
'tasktype': 0,
'creationtime': creationTime,
};
- print("data=$data");
final meetingId = await SqfliteApi.insertMeeting(data);
if (meetingId != null) {
dataList.insert(0, {
@@ -314,7 +295,6 @@ class MeetingController extends GetxController {
}
} catch (e) {
Logger.error('导入文件失败: $filePath, error: $e');
- // 可选:继续导入下一个文件
}
}
EasyLoading.dismiss();
@@ -335,6 +315,9 @@ class MeetingController extends GetxController {
PlatformFile file = result.files.first;
filePath = await _saveFile(file);
fileName = file.name;
+ } else {
+ EasyLoading.dismiss();
+ return;
}
}
diff --git a/lib/modules/meeting/controllers/meeting_details_controller.dart b/lib/modules/meeting/controllers/meeting_details_controller.dart
index 8577525ba..81c4c1841 100644
--- a/lib/modules/meeting/controllers/meeting_details_controller.dart
+++ b/lib/modules/meeting/controllers/meeting_details_controller.dart
@@ -10,6 +10,7 @@ import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart' as http;
import '../../../core/utils/logger.dart';
+import '../../../core/utils/synchrodata.dart';
import '../../../core/utils/upload_oss.dart';
import '../../../data/models/appconfig.dart';
import '../../../data/services/db/sqflite_api.dart';
@@ -208,6 +209,10 @@ class MeetingDetailsController extends GetxController
textList[index] = dataMap;
}
}
+ Synchrodata.saveOperationRecord('update', {
+ 'original': jsonEncode(textList),
+ 'id': id,
+ });
isSpeakerText.value = false;
editSpeakerTextId.value = 0;
editSpeakerTextList = [];
@@ -417,6 +422,10 @@ class MeetingDetailsController extends GetxController
await SqfliteApi.editMeeting(id, {'tasktype': 2, 'personnel': personnel});
List speakerList = await SqfliteApi.getMeetingSpeaker(id);
textList.value = List.from(speakerList);
+ Synchrodata.saveOperationRecord('update', {
+ 'original': jsonEncode(textList),
+ 'id': id,
+ });
meetingDetails.value.tasktype = 2;
meetingDetails.value.personnel = personnel;
meetingDetails.refresh();
diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart
index 2fea38ea5..ee4640b18 100644
--- a/lib/modules/meeting/controllers/meeting_record_controller.dart
+++ b/lib/modules/meeting/controllers/meeting_record_controller.dart
@@ -113,12 +113,15 @@ 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;
@@ -234,7 +237,7 @@ class MeetingRecordController extends GetxController
void _startHardwareServices() {
switch (audioType.value) {
case 0:
- _bleManager.openEncoder();
+ // _bleManager.openEncoder();
break;
case 1:
_bleManager.openDecoder();
diff --git a/lib/modules/meeting/views/meeting_view.dart b/lib/modules/meeting/views/meeting_view.dart
index a20cdd12a..2a1c2c7e9 100644
--- a/lib/modules/meeting/views/meeting_view.dart
+++ b/lib/modules/meeting/views/meeting_view.dart
@@ -11,27 +11,30 @@ import '../model/meeting_model.dart';
class MeetingView extends GetView {
const MeetingView({super.key});
- static OverlayEntry? _recordingOverlayEntry;
+
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
- return Scaffold(
- backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
- appBar: _buildAppBar(context, isDarkMode),
- body: _buildBody(context, isDarkMode),
- floatingActionButton: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.end,
- children: [
- FloatingActionButton(
- heroTag: "addAudio",
- onPressed: () {
- _showAddAudioMenu(context, isDarkMode);
- },
- child: const Icon(Icons.add),
- ),
- ],
+ return PopScope(
+ onPopInvokedWithResult: controller.onPopInvokedWithResult,
+ child: Scaffold(
+ backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
+ appBar: _buildAppBar(context, isDarkMode),
+ body: _buildBody(context, isDarkMode),
+ floatingActionButton: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ FloatingActionButton(
+ heroTag: "addAudio",
+ onPressed: () {
+ _showAddAudioMenu(context, isDarkMode);
+ },
+ child: const Icon(Icons.add),
+ ),
+ ],
+ ),
),
);
}
@@ -451,43 +454,43 @@ class MeetingView extends GetView {
// TODO: 实现从文件导入逻辑
},
),
- SizedBox(height: 8.w),
- _importAudioItem1(
- context,
- isDarkMode,
- icon: Image.asset('assets/images/explore.png',
- width: 32.w, height: 32.w),
- title: '从翻译里导入',
- onTap: () {
- Navigator.of(context).pop();
- _showLocalAudioVideoPicker(context, isDarkMode);
- // TODO: 实现从相册导入逻辑
- },
- ),
- SizedBox(height: 8.w),
- _importAudioItem1(
- context,
- isDarkMode,
- icon: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Image.asset('assets/images/explore.png',
- width: 20.w, height: 20.w),
- SizedBox(width: 4.w),
- Image.asset('assets/images/explore.png',
- width: 20.w, height: 20.w),
- SizedBox(width: 4.w),
- Image.asset('assets/images/explore.png',
- width: 20.w, height: 20.w),
- ],
- ),
- title: '从opus导出',
- onTap: () {
- Navigator.of(context).pop();
- // TODO: 实现从其他APP导入逻辑
- _showLocalOpusPicker(context, isDarkMode);
- },
- ),
+ // SizedBox(height: 8.w),
+ // _importAudioItem1(
+ // context,
+ // isDarkMode,
+ // icon: Image.asset('assets/images/explore.png',
+ // width: 32.w, height: 32.w),
+ // title: '从翻译里导入',
+ // onTap: () {
+ // Navigator.of(context).pop();
+ // _showLocalAudioVideoPicker(context, isDarkMode);
+ // // TODO: 实现从相册导入逻辑
+ // },
+ // ),
+ // SizedBox(height: 8.w),
+ // _importAudioItem1(
+ // context,
+ // isDarkMode,
+ // icon: Row(
+ // mainAxisSize: MainAxisSize.min,
+ // children: [
+ // Image.asset('assets/images/explore.png',
+ // width: 20.w, height: 20.w),
+ // SizedBox(width: 4.w),
+ // Image.asset('assets/images/explore.png',
+ // width: 20.w, height: 20.w),
+ // SizedBox(width: 4.w),
+ // Image.asset('assets/images/explore.png',
+ // width: 20.w, height: 20.w),
+ // ],
+ // ),
+ // title: '从opus导出',
+ // onTap: () {
+ // Navigator.of(context).pop();
+ // // TODO: 实现从其他APP导入逻辑
+ // _showLocalOpusPicker(context, isDarkMode);
+ // },
+ // ),
],
),
),
@@ -575,13 +578,12 @@ class MeetingView extends GetView {
SizedBox(height: 8.w),
// 音、视频录音 - 根据控制器状态禁用
_importAudioItem(
- context,
- isDarkMode,
+ context, isDarkMode,
icon: Image.asset('assets/images/explore.png',
width: 32.w, height: 32.w),
title: '音、视频录音',
- enabled: false, // 依赖控制器状态
- onTap: false
+ enabled: controller.bleManager.isConnected, // 依赖控制器状态
+ onTap: controller.bleManager.isConnected
? () async {
// 条件启用
Navigator.of(context).pop();
@@ -596,14 +598,13 @@ class MeetingView extends GetView {
SizedBox(height: 8.w),
// 通话录音 - 根据控制器状态禁用
_importAudioItem(
- context,
- isDarkMode,
+ context, isDarkMode,
icon: Row(
// ... 图标组合保持不变
),
title: '通话录音',
- enabled: false, // 依赖控制器状态
- onTap: false
+ enabled: controller.bleManager.isConnected, // 依赖控制器状态
+ onTap: controller.bleManager.isConnected
? () async {
// 条件启用
Navigator.of(context).pop();
diff --git a/lib/modules/meeting/views/share_bottom_sheet.dart b/lib/modules/meeting/views/share_bottom_sheet.dart
index 229d83501..318d40df7 100644
--- a/lib/modules/meeting/views/share_bottom_sheet.dart
+++ b/lib/modules/meeting/views/share_bottom_sheet.dart
@@ -6,6 +6,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
+import 'package:get_storage/get_storage.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
@@ -71,7 +72,42 @@ class _ShareBottomSheetState extends State {
child: Column(
children: [
_box([
- _shareItem(Icons.link, '分享链接', isBorder: false),
+ _shareItem(
+ Icons.link,
+ '分享链接',
+ isBorder: false,
+ onTap: () async {
+ Get.back();
+ final GetStorage storage = GetStorage();
+ String token = storage.read("logintoken") ?? '';
+ String userId = '';
+ Map? userInfo = storage.read("user_info");
+ if (userInfo != null) {
+ userId = userInfo['user']['uid'];
+ }
+
+ String id =
+ '${userId}_${_controller.meetingData.value.id}';
+
+ final thumbnail = await getAssetThumbnailFile(
+ 'assets/images/headphone_dark.png',
+ 'share_thumb.png',
+ );
+
+ final result = await SharePlus.instance.share(ShareParams(
+ title: _controller.meetingData.value.title,
+ subject:
+ 'DeepSound WEB,由DeepSound.Ai提供支持-实时记录会议纪要,多语言翻译。',
+ uri: Uri.parse(
+ 'http://web.ideapsound.com/share/$id/$token',
+ ),
+ previewThumbnail: thumbnail,
+ ));
+ if (result.status == ShareResultStatus.success) {
+ EasyLoading.showToast('分享完成!');
+ }
+ },
+ ),
]),
_box([
_shareItem(Icons.copy, '复制转写', onTap: () {
@@ -213,4 +249,12 @@ class _ShareBottomSheetState extends State {
}
return text;
}
+
+ Future getAssetThumbnailFile(String assetPath, String fileName) async {
+ final byteData = await rootBundle.load(assetPath);
+ final tempDir = await getTemporaryDirectory();
+ final file = File('${tempDir.path}/$fileName');
+ await file.writeAsBytes(byteData.buffer.asUint8List());
+ return XFile(file.path);
+ }
}
diff --git a/lib/modules/meeting/views/speaker_all_bottom_sheet.dart b/lib/modules/meeting/views/speaker_all_bottom_sheet.dart
index 93b89c0f3..1d7a7c03b 100644
--- a/lib/modules/meeting/views/speaker_all_bottom_sheet.dart
+++ b/lib/modules/meeting/views/speaker_all_bottom_sheet.dart
@@ -1,3 +1,5 @@
+import 'dart:convert';
+
import 'package:common_utils/common_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
@@ -6,6 +8,7 @@ import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:lottie/lottie.dart';
+import '../../../core/utils/synchrodata.dart';
import '../../../data/services/db/sqflite_api.dart';
import '../controllers/meeting_details_controller.dart';
@@ -556,6 +559,10 @@ class _SpeakerAllBottomSheetState extends State {
bool isEditSummary = false;
String summaryText = _controller.meetingDetails.value.summary;
_controller.textList.value = newTextList;
+ Synchrodata.saveOperationRecord('update', {
+ 'original': jsonEncode(_controller.textList),
+ 'id': _controller.meetingData.value.id,
+ });
for (var element in personnelList) {
if (summaryText.contains('[${element['original']}]')) {
summaryText = summaryText.replaceAll(
diff --git a/lib/modules/meeting/views/speaker_bottom_sheet.dart b/lib/modules/meeting/views/speaker_bottom_sheet.dart
index 0a0a40871..2e814e26f 100644
--- a/lib/modules/meeting/views/speaker_bottom_sheet.dart
+++ b/lib/modules/meeting/views/speaker_bottom_sheet.dart
@@ -1,9 +1,12 @@
+import 'dart:convert';
+
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
+import '../../../core/utils/synchrodata.dart';
import '../../../data/services/db/sqflite_api.dart';
import '../controllers/meeting_details_controller.dart';
@@ -331,6 +334,10 @@ class _SpeakerBottomSheetState extends State {
dataMap['speaker'] = _speakerController.text;
_controller.textList[index] = dataMap;
}
+ Synchrodata.saveOperationRecord('update', {
+ 'original': jsonEncode(_controller.textList),
+ 'id': _controller.meetingData.value.id,
+ });
_addHistorySpeaker();
Get.back();
}
@@ -361,6 +368,10 @@ class _SpeakerBottomSheetState extends State {
var result = await SqfliteApi.editMeetingSpeaker(editSpeakerList);
if (result != null) {
_controller.textList.value = newTextList;
+ Synchrodata.saveOperationRecord('update', {
+ 'original': jsonEncode(_controller.textList),
+ 'id': _controller.meetingData.value.id,
+ });
if (_controller.meetingDetails.value.summary
.contains('[${widget.speakerName}]')) {
String summaryText = _controller.meetingDetails.value.summary
diff --git a/lib/modules/music/views/music_playlist_view.dart b/lib/modules/music/views/music_playlist_view.dart
index 65fecb2eb..37a35fbf2 100644
--- a/lib/modules/music/views/music_playlist_view.dart
+++ b/lib/modules/music/views/music_playlist_view.dart
@@ -175,19 +175,19 @@ class MusicPlaylistView extends StatelessWidget {
backgroundColor: Colors.grey,
size: 40.0,
strokeWidth: 2.5,
- onPlayPause: () {
- if (controller.isPlaying.value) {
- if (controller.currentId.value == id) {
- controller.musicPause();
- } else {
- controller.musicPlay(song);
- }
- } else {
- if (controller.currentId.value == id) {
- controller.musicResume();
+ onPlayPause: () async {
+ try {
+ if (controller.isPlaying.value) {
+ if (controller.currentId.value == id) {
+ await controller.musicPause();
+ } else {
+ await controller.musicPlay(song);
+ }
} else {
controller.musicPlay(song);
}
+ } catch (e) {
+ // 静默处理错误
}
},
onSeek: isThisPlaying
diff --git a/lib/modules/settings/controllers/settings_controller.dart b/lib/modules/settings/controllers/settings_controller.dart
index 36fd66fe3..285f687a0 100644
--- a/lib/modules/settings/controllers/settings_controller.dart
+++ b/lib/modules/settings/controllers/settings_controller.dart
@@ -16,6 +16,8 @@ import 'package:package_info_plus/package_info_plus.dart';
import 'package:permission_handler/permission_handler.dart';
import 'dart:async';
import 'package:app_settings/app_settings.dart';
+import '../../../data/services/version_update_service.dart';
+import '../../common/widgets/version_update_dialog.dart';
class SettingsController extends GetxController {
// 语言设置
@@ -490,4 +492,50 @@ class SettingsController extends GetxController {
}
}
}
+
+ /// 手动检查版本更新
+ Future checkForUpdate() async {
+ try {
+ // 显示加载提示
+ Get.dialog(
+ const Center(
+ child: CircularProgressIndicator(),
+ ),
+ barrierDismissible: false,
+ );
+
+ final versionService = VersionUpdateService.to;
+ final updateInfo = await versionService.checkForUpdate();
+
+ // 关闭加载提示
+ Get.back();
+
+ if (updateInfo.hasUpdate) {
+ // 显示更新对话框
+ await VersionUpdateDialog.show(updateInfo);
+ } else {
+ // 显示已是最新版本的提示
+ Get.snackbar(
+ '检查更新',
+ '当前已是最新版本 ${updateInfo.currentVersion}',
+ snackPosition: SnackPosition.TOP,
+ backgroundColor: Colors.green.withOpacity(0.8),
+ colorText: Colors.white,
+ );
+ }
+ } catch (e) {
+ // 关闭加载提示
+ if (Get.isDialogOpen == true) {
+ Get.back();
+ }
+
+ Get.snackbar(
+ '检查更新失败',
+ '网络连接异常,请稍后重试',
+ snackPosition: SnackPosition.TOP,
+ backgroundColor: Colors.red.withOpacity(0.8),
+ colorText: Colors.white,
+ );
+ }
+ }
}
diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart
index fc37c5bad..777f8c48f 100644
--- a/lib/modules/settings/views/settings_view.dart
+++ b/lib/modules/settings/views/settings_view.dart
@@ -543,6 +543,28 @@ class SettingsView extends GetView {
},
isDarkMode: isDarkMode,
),
+ Divider(
+ height: 1,
+ color: isDarkMode
+ ? Colors.white.withOpacity(0.1)
+ : Colors.grey[200]),
+ // 面对面翻译测试
+ _buildSimpleNavigationSetting(
+ title: '面对面翻译测试',
+ subtitle: '测试面对面翻译',
+ icon: Icons.bluetooth_searching,
+ iconBgColor: isDarkMode
+ ? Colors.green[900]!.withOpacity(0.3)
+ : Colors.green[100]!,
+ iconColor:
+ isDarkMode ? Colors.green[300]! : Colors.green[600]!,
+ titleColor: isDarkMode ? Colors.white : null,
+ subtitleColor: isDarkMode ? Colors.white70 : null,
+ onTap: () {
+ Get.toNamed(Routes.ftftranslation);
+ },
+ isDarkMode: isDarkMode,
+ ),
Divider(
height: 1,
color: isDarkMode
@@ -880,7 +902,8 @@ class SettingsView extends GetView {
),
InkWell(
onTap: () {
- // 在这里添加检查更新的逻辑
+ // 调用检查更新方法
+ controller.checkForUpdate();
},
child: Container(
constraints: BoxConstraints(
diff --git a/lib/modules/splash/controllers/splash_controller.dart b/lib/modules/splash/controllers/splash_controller.dart
index e53f146dc..de6a1d09e 100644
--- a/lib/modules/splash/controllers/splash_controller.dart
+++ b/lib/modules/splash/controllers/splash_controller.dart
@@ -8,6 +8,8 @@ import '../../../data/models/user_Info.dart';
import '../../../routes/app_routes.dart';
import '../../../core/themes/theme_service.dart';
import '../../../data/services/network/api.dart';
+import '../../../data/services/version_update_service.dart';
+import '../../../modules/common/widgets/version_update_dialog.dart';
class SplashController extends GetxController {
final GetStorage _storage = GetStorage();
@@ -73,6 +75,9 @@ class SplashController extends GetxController {
// await _loadImages();
// await _initDatabase();
+ // 检查版本更新(在后台进行,不阻塞启动流程)
+ _checkVersionUpdate();
+
_isResourceLoaded.value = true;
_checkNavigateCondition();
} catch (e) {
@@ -151,4 +156,23 @@ class SplashController extends GetxController {
return false;
}
}
+
+ /// 检查版本更新(后台执行,不阻塞启动流程)
+ void _checkVersionUpdate() {
+ // 延迟执行版本检查,避免影响启动速度
+ Timer(const Duration(seconds: 1), () async {
+ try {
+ final versionService = VersionUpdateService.to;
+ final updateInfo = await versionService.checkForUpdate();
+
+ if (updateInfo.hasUpdate) {
+ // 显示更新对话框
+ await VersionUpdateDialog.show(updateInfo);
+ }
+ } catch (e) {
+ print('版本检查失败: $e');
+ // 版本检查失败不影响用户使用,静默处理
+ }
+ });
+ }
}
diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart
index 9a911accf..d83b04bdd 100644
--- a/lib/modules/translation/controllers/translation_controller.dart
+++ b/lib/modules/translation/controllers/translation_controller.dart
@@ -26,7 +26,7 @@ class TranslationController extends GetxController {
final LanguageManager _languageManager = Get.find();
final GetStorage _storage = GetStorage();
// 蓝牙服务
- final _bleManager = Get.find();
+ final bleManager = Get.find();
// 音频输入源
bool _audioSourceType = false;
// 存储相关
@@ -308,14 +308,14 @@ class TranslationController extends GetxController {
// translationHistory.add(newItem);
// 开始连续语音识别
if (currentMode.value == "audioVideo") {
- _bleManager.openDecoder();
+ bleManager.openDecoder();
// 发送ble音乐或者通话远端声音
_audioSourceType = true;
isTtsEnabled.value = false; // 禁用TTS以避免干扰
Logger.info('发送ble音乐或者通话远端声音');
} else if (currentMode.value == "call") {
// 发送ble系统mic和dac(音乐或者通话远端)声音
- _bleManager.openA2DPDecoder();
+ bleManager.openA2DPDecoder();
_audioSourceType = true;
isTtsEnabled.value = true;
Logger.info('发送ble系统mic和dac(音乐或者通话远端)声音');
@@ -563,7 +563,7 @@ class TranslationController extends GetxController {
Future stopAll() async {
try {
if (currentMode.value == "audioVideo" || currentMode.value == "call") {
- _bleManager.closeCodec();
+ bleManager.closeCodec();
// 发送ble音乐或者通话远端声音
Logger.info('发送ble音乐或者通话远端声音');
}
diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart
index e8eaf3d3e..048f5f463 100644
--- a/lib/modules/translation/views/translation_view.dart
+++ b/lib/modules/translation/views/translation_view.dart
@@ -67,12 +67,12 @@ class TranslationView extends GetView {
_buildPopupMenuItem(
'audioVideo',
'audioVideoTranslation'.tr,
- enabled: true, // 根据连接状态启用/禁用
+ enabled: controller.bleManager.isConnected, // 根据连接状态启用/禁用
),
_buildPopupMenuItem(
'call',
'callTranslation'.tr,
- enabled: true, // 根据连接状态启用/禁用
+ enabled: controller.bleManager.isConnected, // 根据连接状态启用/禁用
),
],
child: Row(
diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart
index 66c5f2516..8f3a14799 100644
--- a/lib/routes/app_pages.dart
+++ b/lib/routes/app_pages.dart
@@ -46,6 +46,8 @@ import '../modules/realtime/bindings/realtime_binding.dart';
import '../modules/realtime/views/realtime_view.dart';
import '../modules/music/views/music_playlist_view.dart';
import '../modules/music/bindings/music_playlist_binding.dart';
+import '../modules/FTFTranslation/bindings/FTFTranslation_binding.dart';
+import '../modules/FTFTranslation/views/FTFTranslation_view.dart';
abstract class AppPages {
static final pages = [
@@ -191,6 +193,13 @@ abstract class AppPages {
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
+ GetPage(
+ name: Routes.ftftranslation,
+ page: () => const FTFTranslationView(),
+ binding: FTFTranslationBinding(),
+ transition: Transition.noTransition,
+ transitionDuration: Duration(milliseconds: 250),
+ ),
GetPage(
name: Routes.opusTest,
page: () => const OpusTestView(),
diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart
index 3e2e1be89..550ab16cc 100644
--- a/lib/routes/app_routes.dart
+++ b/lib/routes/app_routes.dart
@@ -28,6 +28,7 @@ abstract class Routes {
static const translationHistory = '/translation/history'; // 翻译历史
static const speechDemo = '/speech_demo'; // 语音演示
static const translationList = '/translation/List'; // 翻译历史
+ static const ftftranslation = '/ftftranslation'; // 面对面翻译
// 测试路由(生产环境建议移除)
static const ttsTest = '/tts_test'; // 文字转语音测试
static const asrTest = '/asr_test'; // 语音识别测试
diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
index 55eb87911..2e9d93f87 100644
--- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
+++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
@@ -22,6 +22,7 @@ import java.util.concurrent.LinkedBlockingQueue
import android.os.Handler
import java.util.concurrent.atomic.AtomicBoolean
import android.util.Log
+
/**
* BLE服务类:提供蓝牙低功耗设备的扫描、连接和通信功能
* 支持:
@@ -67,17 +68,20 @@ object BleService {
private var bluetoothLeScanner: BluetoothLeScanner? = null
// GATT连接相关
- var bluetoothGatt: BluetoothGatt? = null
+ var bluetoothGatt: BluetoothGatt? = null
private var notifyChar: BluetoothGattCharacteristic? = null
private var writeChar: BluetoothGattCharacteristic? = null
private var audioChar: BluetoothGattCharacteristic? = null
var recordfile: RecordingFile? = null
+
// 扫描相关
private lateinit var scanHandler: Handler
private val scanResults = ArrayList()
private var isScanning = false
-// 是否是ota模式
+
+ // 是否是ota模式
var isEnterOta = false
+
// 连接状态
val connectionState = MutableLiveData(BleConst.STATE_DISCONNECTED)
@@ -136,7 +140,7 @@ object BleService {
Log.e(TAG, "OpusManager初始化失败: ${e.message}", e)
// 根据需要决定是否因为Opus初始化失败而返回false
}
-recordfile = RecordingFile(this.context)
+ recordfile = RecordingFile(this.context)
isInitialized = true
Log.d(TAG, "BLE服务初始化成功")
return true
@@ -198,7 +202,6 @@ recordfile = RecordingFile(this.context)
// 获取一个局部引用,避免并发访问问题
val scanner = bluetoothLeScanner ?: return false
if (isScanning) return false
-
Log.i(TAG, "开始主动扫描BLE设备...")
scanResults.clear()
@@ -341,13 +344,12 @@ recordfile = RecordingFile(this.context)
Log.w(TAG, "未找到已配对的设备 MAC 地址")
return
}
-
-
- if(!isEnterOta)
- {
- Log.i(TAG, "检测到已配对设备,MAC: $pairedMac,尝试连接")
- // 调用现有的 connect 方法
- connect(pairedMac)
+
+
+ if (!isEnterOta) {
+ Log.i(TAG, "检测到已配对设备,MAC: $pairedMac,尝试连接")
+ // 调用现有的 connect 方法
+ connect(pairedMac)
}
} catch (e: Exception) {
@@ -518,7 +520,7 @@ recordfile = RecordingFile(this.context)
when (c.uuid) {
// 音频特征数据
BleConst.RECEIVE_AUDIO_CHAR_UUID -> {
-
+
// Log.i(TAG, "收到音频特征数据")
processAudioData(data)
}
@@ -597,7 +599,7 @@ recordfile = RecordingFile(this.context)
private fun processAudioData(data: ByteArray) {
try {
if (opusManager?.isDecodeStream == true) {
- recordfile?.saveAudioDataToWav(data)
+ recordfile?.saveAudioDataToWav(data)
opusManager?.writeAudioStream(data)
} else {
// Log.d(TAG, "Opus解码流未启动,忽略音频数据")
@@ -795,17 +797,16 @@ recordfile = RecordingFile(this.context)
BleConst.CODEC_CONTROL_ENCODE_ON -> "已打开编码"
else -> "未知状态($codecStatus)"
}
-if (codecStatus == BleConst.CODEC_CONTROL_DECODE_ON ||
- codecStatus == BleConst.CODEC_CONTROL_A2DP_PLAY ||
- codecStatus == BleConst.CODEC_CONTROL_ENCODE_ON) {
-
- recordfile!!.closeFile()
- recordfile!!.creatingFiles()
-}
-else if(codecStatus == BleConst.CODEC_CONTROL_CLOSE)
-{
- recordfile!!.closeFile()
-}
+ if (codecStatus == BleConst.CODEC_CONTROL_DECODE_ON ||
+ codecStatus == BleConst.CODEC_CONTROL_A2DP_PLAY ||
+ codecStatus == BleConst.CODEC_CONTROL_ENCODE_ON
+ ) {
+
+ recordfile!!.closeFile()
+ recordfile!!.creatingFiles()
+ } else if (codecStatus == BleConst.CODEC_CONTROL_CLOSE) {
+ recordfile!!.closeFile()
+ }
// 声道模式描述
val channelDesc = when (channelMode) {
BleConst.AUDIO_CHANNEL_LEFT -> "左声道"
@@ -1093,7 +1094,7 @@ else if(codecStatus == BleConst.CODEC_CONTROL_CLOSE)
// // The library might expect this in a different unit or derive it.
// // Given JlOpusPlugin.kt, packetSize refers to Opus encoded frame duration in ms.
// )
- startOpusStreamDecoding(false, 1, 16000, 40)
+ startOpusStreamDecoding(false, 1, 16000, 40)
Log.i(TAG, "打开解码0xA2")
return sendCommand(
@@ -1110,7 +1111,7 @@ else if(codecStatus == BleConst.CODEC_CONTROL_CLOSE)
*/
fun openA2DPDecoder(): Boolean {
Log.i(TAG, "打开编码 0xA1")
- startOpusStreamDecoding(false, 1, 16000, 80)
+ startOpusStreamDecoding(false, 1, 16000, 80)
// Log.i(TAG, "打开解码...")
return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(
@@ -1135,7 +1136,7 @@ else if(codecStatus == BleConst.CODEC_CONTROL_CLOSE)
fun openEncoder(): Boolean {
Log.i(TAG, "打开编码0xB1")
- startOpusStreamDecoding(false, 1, 16000, 40)
+ startOpusStreamDecoding(false, 1, 16000, 40)
// 目前仅发送命令通知设备开始编码。
return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(),
@@ -1385,9 +1386,10 @@ else if(codecStatus == BleConst.CODEC_CONTROL_CLOSE)
}
}
-
+
}
- class RecordingFile(private val context: Context) {
+
+class RecordingFile(private val context: Context) {
private var currentAudioFile: File? = null
private var fos: FileOutputStream? = null
@@ -1408,8 +1410,9 @@ else if(codecStatus == BleConst.CODEC_CONTROL_CLOSE)
// 创建新的音频文件
val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault())
val timestamp = dateFormat.format(Date())
-
- val filePath = File(context.getExternalFilesDir(null), "${fileName}_${timestamp}.opus").absolutePath
+
+ val filePath =
+ File(context.getExternalFilesDir(null), "${fileName}_${timestamp}.opus").absolutePath
currentAudioFile = File(filePath)
currentAudioFile?.createNewFile()
// 追加音频数据到文件
@@ -1417,42 +1420,42 @@ else if(codecStatus == BleConst.CODEC_CONTROL_CLOSE)
startWriteThread() // 新增:启动写入线程
}
- // 新增:启动写入线程
- private fun startWriteThread() {
- if (isWriting.get()) return
- isWriting.set(true)
- writeThread = Thread {
- try {
- while (isWriting.get() || writeQueue.isNotEmpty()) {
- val data = writeQueue.poll() ?: continue
- fos?.write(data)
- }
- } catch (e: Exception) {
- Log.e("", "异步写入音频数据失败: ${e.message}")
+ // 新增:启动写入线程
+ private fun startWriteThread() {
+ if (isWriting.get()) return
+ isWriting.set(true)
+ writeThread = Thread {
+ try {
+ while (isWriting.get() || writeQueue.isNotEmpty()) {
+ val data = writeQueue.poll() ?: continue
+ fos?.write(data)
}
+ } catch (e: Exception) {
+ Log.e("", "异步写入音频数据失败: ${e.message}")
}
- writeThread?.start()
}
+ writeThread?.start()
+ }
- /**
- * 保存音频数据到 WAV 文件(异步)
- */
- internal fun saveAudioDataToWav(buffer: ByteArray) {
- if (fos == null || currentAudioFile == null) return
- // 放入队列,由写线程写入
- // 1. 将字节数据转换为十六进制字符串
- // val hexData = buildString {
- // buffer.forEachIndexed { index, byte ->
- // append("%02X".format(byte))
-
- // }
- // }
-
- // 2. 写入十六进制字符串
-
- writeQueue.offer(buffer)
- totalBytesWritten += buffer.size
- }
+ /**
+ * 保存音频数据到 WAV 文件(异步)
+ */
+ internal fun saveAudioDataToWav(buffer: ByteArray) {
+ if (fos == null || currentAudioFile == null) return
+ // 放入队列,由写线程写入
+ // 1. 将字节数据转换为十六进制字符串
+ // val hexData = buildString {
+ // buffer.forEachIndexed { index, byte ->
+ // append("%02X".format(byte))
+
+ // }
+ // }
+
+ // 2. 写入十六进制字符串
+
+ writeQueue.offer(buffer)
+ totalBytesWritten += buffer.size
+ }
internal fun closeFile() {
diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt
index 642ce5ca1..1c5b7de75 100644
--- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt
+++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt
@@ -10,6 +10,7 @@ import android.content.pm.PackageManager
import android.location.LocationManager
import android.net.Uri
import android.os.Build
+import android.provider.AlarmClock
import android.provider.CalendarContract
import android.provider.ContactsContract
import android.provider.Settings
@@ -558,6 +559,66 @@ private class SendTextMessageHandler(private val context: Context?) : FunctionHa
* 拨打电话处理器
*/
private class MakePhoneCallHandler(private val context: Context?) : FunctionHandler {
+
+ /**
+ * 根据联系人名称获取电话号码
+ * @param contact 联系人名称或电话号码
+ * @return 电话号码
+ */
+ private fun getPhoneNumberForContact(contact: String): String {
+ // 如果输入的是电话号码(简单判断,包含数字和特殊符号)
+ if (contact.matches(Regex("^[+\\d\\s\\-()]*\$")) && contact.any { it.isDigit() }) {
+ // 移除非数字字符
+ return contact.replace(Regex("[^+\\d]"), "")
+ }
+
+ // 检查读取联系人权限
+ if (!checkPermissionAndGuide(context!!, Manifest.permission.READ_CONTACTS, "读取联系人", "访问联系人")) {
+ return ""
+ }
+
+ // 从联系人数据库查询
+ try {
+ val contentResolver = context.contentResolver
+ val uri = Uri.withAppendedPath(
+ ContactsContract.Contacts.CONTENT_FILTER_URI,
+ Uri.encode(contact)
+ )
+
+ val projection = arrayOf(
+ ContactsContract.Contacts._ID,
+ ContactsContract.Contacts.DISPLAY_NAME,
+ ContactsContract.Contacts.HAS_PHONE_NUMBER
+ )
+
+ contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
+ if (cursor.moveToFirst()) {
+ val hasPhoneNumber = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.HAS_PHONE_NUMBER))
+
+ if (hasPhoneNumber > 0) {
+ val contactId = cursor.getString(cursor.getColumnIndexOrThrow(
+ ContactsContract.Contacts._ID))
+ val phoneUri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI
+ val phoneProjection = arrayOf(ContactsContract.CommonDataKinds.Phone.NUMBER)
+ val phoneSelection = "${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} = ?"
+ val phoneSelectionArgs = arrayOf(contactId)
+
+ contentResolver?.query(phoneUri, phoneProjection, phoneSelection, phoneSelectionArgs, null)?.use { phoneCursor ->
+ if (phoneCursor.moveToFirst()) {
+ return phoneCursor.getString(phoneCursor.getColumnIndexOrThrow(
+ ContactsContract.CommonDataKinds.Phone.NUMBER))
+ }
+ }
+ }
+ }
+ }
+ } catch (e: Exception) {
+ Log.e("ChatApi", "查询联系人信息失败: ${e.message}", e)
+ }
+
+ return ""
+ }
+
override suspend fun handle(arguments: Map): String {
val contact = arguments["contact"] as? String
@@ -630,6 +691,21 @@ private class MakePhoneCallHandler(private val context: Context?) : FunctionHand
})
}.toString()
}
+// return try {
+// val callIntent = Intent(Intent.ACTION_DIAL).apply {
+// data = Uri.parse("tel:$contact")
+// flags = Intent.FLAG_ACTIVITY_NEW_TASK
+// }
+//
+// if (context?.packageManager?.queryIntentActivities(callIntent, 0)?.isNotEmpty() == true) {
+// context.startActivity(callIntent)
+// "{\"result\": \"已发起电话呼叫\"}"
+// } else {
+// "{\"result\": \"无法拨打电话\"}"
+// }
+// } catch (e: Exception) {
+// "{\"result\": \"拨打电话失败:${e.message}\"}"
+// }
}
}
@@ -637,6 +713,140 @@ private class MakePhoneCallHandler(private val context: Context?) : FunctionHand
* 设置提醒处理器
*/
private class SetReminderHandler(private val context: Context?) : FunctionHandler {
+
+ /**
+ * 获取默认日历ID
+ */
+ private fun getDefaultCalendarId(): Long {
+ val projection = arrayOf(CalendarContract.Calendars._ID)
+ var calendarId = -1L
+
+ try {
+ val uri = CalendarContract.Calendars.CONTENT_URI
+ val selection = "(${CalendarContract.Calendars.VISIBLE} = 1) AND (${CalendarContract.Calendars.IS_PRIMARY} = 1)"
+ context?.contentResolver?.query(uri, projection, selection, null, null)?.use { cursor ->
+ if (cursor.moveToFirst()) {
+ calendarId = cursor.getLong(0)
+ } else {
+ // 如果没有找到主日历,尝试找任何可见的日历
+ context.contentResolver.query(uri, projection, "${CalendarContract.Calendars.VISIBLE} = 1", null, null)?.use { c ->
+ if (c.moveToFirst()) {
+ calendarId = c.getLong(0)
+ }
+ }
+ }
+ }
+ } catch (e: Exception) {
+ Log.e("CahtApi", "获取默认日历ID失败: ${e.message}", e)
+ }
+
+ return calendarId
+ }
+
+ /**
+ * 尝试创建闹钟提醒(备选方案)
+ */
+ private fun tryCreateAlarm(title: String, content: String, timeStr: String): String {
+ try {
+ // 解析小时和分钟
+ val timeParts = timeStr.split(" ")[1].split(":")
+ val hour = timeParts[0].toInt()
+ val minute = timeParts[1].toInt()
+
+ val alarmIntent = Intent(AlarmClock.ACTION_SET_ALARM)
+ alarmIntent.putExtra(AlarmClock.EXTRA_MESSAGE, "$title: $content")
+ alarmIntent.putExtra(AlarmClock.EXTRA_HOUR, hour)
+ alarmIntent.putExtra(AlarmClock.EXTRA_MINUTES, minute)
+ alarmIntent.putExtra(AlarmClock.EXTRA_SKIP_UI, true) // 尝试跳过UI
+ alarmIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+
+ if (alarmIntent.resolveActivity(context!!.packageManager) != null) {
+ context.startActivity(alarmIntent)
+ return "{\"result\": \"已创建闹钟提醒: $title, 时间: $timeStr\"}"
+ }
+ } catch (e: Exception) {
+ Log.e("CahtApi", "创建闹钟失败: ${e.message}", e)
+ }
+ return "{\"result\": \"未找到可用的日历或闹钟应用\"}"
+ }
+
+ /**
+ * 设置提醒
+ * @param title 提醒标题
+ * @param content 提醒内容(可选)
+ * @param timeStr 提醒时间,格式为"yyyy-MM-dd HH:mm"
+ * @return 操作结果
+ */
+ fun setReminder(title: String, content: String, timeStr: String): String {
+ Log.d("CahtApi", "设置提醒: $title, 时间: $timeStr")
+
+ try {
+ // 解析时间字符串
+ val time:Long = try {
+ val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm", java.util.Locale.getDefault())
+ dateFormat.parse(timeStr)?.time ?: System.currentTimeMillis()
+ } catch (e: Exception) {
+ Log.e("CahtApi", "解析时间失败: $timeStr, ${e.message}")
+ return "{\"result\": \"提醒时间格式无效,请使用'yyyy-MM-dd HH:mm'格式\"}"
+ }
+
+ // 检查日历权限
+ if (!checkPermissionAndGuide(context!!,Manifest.permission.WRITE_CALENDAR, "日历写入", "设置提醒")) {
+ return "{\"result\": \"需要日历权限才能创建提醒,请在权限管理中开启日历权限\"}"
+ }
+
+ // 直接使用ContentProvider写入日历
+ val endTime = time + 3600000 // 默认1小时后结束
+
+ // 获取默认日历ID
+ val calendarId = getDefaultCalendarId()
+ if (calendarId == -1L) {
+ Log.w("CahtApi", "未找到默认日历")
+ return tryCreateAlarm(title, content, timeStr)
+ }
+
+ // 创建事件
+ val eventValues = ContentValues().apply {
+ put(CalendarContract.Events.CALENDAR_ID, calendarId)
+ put(CalendarContract.Events.TITLE, title)
+ put(CalendarContract.Events.DESCRIPTION, content)
+ put(CalendarContract.Events.DTSTART, time)
+ put(CalendarContract.Events.DTEND, endTime)
+ put(CalendarContract.Events.HAS_ALARM, 1)
+ put(CalendarContract.Events.EVENT_TIMEZONE, java.util.TimeZone.getDefault().id)
+ }
+
+ val eventUri = context.contentResolver.insert(CalendarContract.Events.CONTENT_URI, eventValues)
+ if (eventUri == null) {
+ Log.e("CahtApi", "插入日历事件失败")
+ return tryCreateAlarm(title, content, timeStr)
+ }
+
+ // 获取事件ID
+ val eventId = ContentUris.parseId(eventUri)
+
+ // 添加提醒(15分钟前提醒)
+ val reminderValues = ContentValues().apply {
+ put(CalendarContract.Reminders.EVENT_ID, eventId)
+ put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT)
+ put(CalendarContract.Reminders.MINUTES, 15) // 提前15分钟提醒
+ }
+
+ val reminderUri = context.contentResolver.insert(CalendarContract.Reminders.CONTENT_URI, reminderValues)
+ if (reminderUri != null) {
+ return "{\"result\": \"已成功创建提醒: $title, 时间: $timeStr\"}"
+ } else {
+ // 仍然创建了事件,但没有提醒
+ return "{\"result\": \"已创建日历事件,但未设置提醒: $title\"}"
+ }
+ } catch (e: Exception) {
+ Log.e("CahtApi", "设置提醒失败: ${e.message}", e)
+ return "{\"result\": \"设置提醒失败: ${e.message}\"}"
+ }
+ }
+
+
+
override suspend fun handle(arguments: Map): String {
val title = arguments["title"] as? String
val content = arguments["content"] as? String ?: ""
diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/Utils.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/Utils.kt
index 0d1b90153..31b7bba05 100644
--- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/Utils.kt
+++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/Utils.kt
@@ -1,5 +1,13 @@
package com.yunqiinnovation.chat_api
+import android.app.ActivityManager
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.provider.Settings
+import android.util.Log
+import androidx.core.content.ContextCompat
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.sse.*
@@ -54,4 +62,90 @@ fun createSslTrustAllClient(): HttpClient {
*/
fun createMcpHttpClient(): HttpClient {
return createSslTrustAllClient()
-}
\ No newline at end of file
+}
+
+/**
+ * 检查应用是否在前台运行
+ * @return 应用是否在前台
+ */
+fun isAppInForeground(context: Context,): Boolean {
+ try {
+ val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+ val appProcesses = activityManager.runningAppProcesses ?: return false
+ val packageName = context.packageName
+
+ for (appProcess in appProcesses) {
+ if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
+ && appProcess.processName == packageName) {
+ return true
+ }
+ }
+ } catch (e: Exception) {
+ Log.e("ChatApi", "检查应用状态失败: ${e.message}", e)
+ }
+ return false
+}
+/**
+ * 检查权限并根据应用状态采取不同行为
+ * @param permission 需要检查的权限
+ * @param permissionName 权限名称(用于显示给用户)
+ * @param functionName 功能名称(用于日志和提示)
+ * @return 是否拥有权限
+ */
+fun checkPermissionAndGuide(context: Context, permission: String, permissionName: String, functionName: String): Boolean {
+ if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) {
+ Log.w("ChatApi", "缺少${permissionName}权限,无法执行${functionName}")
+
+ // 检查应用是否在前台
+ if (isAppInForeground(context)) {
+ // 应用在前台,尝试直接弹出权限申请框
+ try {
+ // 首先尝试发送广播通知DeepSound应用弹出权限申请对话框
+ val intent = Intent("com.yunqiinnovation.deepsound.action.REQUEST_PERMISSION").apply {
+ setPackage(context.packageName)
+ putExtra("permission", permission)
+ putExtra("permissionName", permissionName)
+ putExtra("functionName", functionName)
+ addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
+ }
+ context.sendBroadcast(intent)
+
+ // 由于插件无法直接请求权限(需要Activity),上面的广播会让主应用来处理权限请求
+ // 这里设置一个短暂延迟,让用户有时间响应权限请求
+ Thread.sleep(500)
+
+ // 再次检查权限是否已授予(以防用户立即授予)
+ if (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED) {
+ return true
+ }
+ } catch (e: Exception) {
+ Log.e("ChatApi", "请求权限失败: ${e.message}", e)
+ }
+
+ // 如果权限仍未授予,引导用户到权限管理页面
+ try {
+ // 尝试跳转到应用内的权限管理页面
+ val intent = Intent("com.yunqiinnovation.deepsound.action.PERMISSIONS_SETTINGS").apply {
+ setPackage(context.packageName)
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+
+ if (intent.resolveActivity(context.packageManager) != null) {
+ context.startActivity(intent)
+ } else {
+ // 如果应用内权限页面不可用,则跳转到系统应用设置页面
+ val settingsIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
+ data = Uri.fromParts("package", context.packageName, null)
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ context.startActivity(settingsIntent)
+ }
+ } catch (e: Exception) {
+ Log.e("ChatApi", "打开权限设置页面失败: ${e.message}", e)
+ }
+ }
+ // 应用在后台或权限请求失败,只返回文字提示
+ return false
+ }
+ return true
+}
\ No newline at end of file
diff --git a/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt b/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt
index 5a2319917..8e25fdea2 100644
--- a/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt
+++ b/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt
@@ -1,23 +1,61 @@
package com.yunqiinnovation.location_service
+import android.Manifest
+import android.app.ActivityManager
import android.content.Context
+import android.content.pm.PackageManager
import android.location.Location
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
import android.util.Log
+import androidx.core.content.ContextCompat
import com.amap.api.location.*
import io.flutter.plugin.common.MethodChannel
object LocationService {
private const val TAG = "LocationService"
-
+ private var appcontext:Context? = null
private var locationClient: AMapLocationClient? = null
private var currentLocation: AMapLocation? = null
private var initialized = false
+ private var permissionRetryHandler: Handler? = null
+ private var permissionRetryRunnable: Runnable? = null
+
+ /**
+ * 检查应用是否在前台运行
+ * @return 应用是否在前台
+ */
+ fun isAppInForeground(context: Context,): Boolean {
+ try {
+ val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+ val appProcesses = activityManager.runningAppProcesses ?: return false
+ val packageName = context.packageName
+
+ for (appProcess in appProcesses) {
+ if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
+ && appProcess.processName == packageName) {
+ return true
+ }
+ }
+ } catch (e: Exception) {
+ Log.e("ChatApi", "检查应用状态失败: ${e.message}", e)
+ }
+ return false
+ }
+
fun initialize(context: Context) {
if (initialized) return
+ appcontext = context
// Log.e(TAG, "liwei--------- 初始化高德定位服务 1")
-
+ if (!checkPermissions(context)) {
+ Log.w(TAG, "没有定位权限,等待用户授权中...")
+ startPermissionMonitor()
+ return
+ }
try {
+
AMapLocationClient.updatePrivacyShow(context, true, true)
AMapLocationClient.updatePrivacyAgree(context, true)
// Log.e(TAG, "liwei--------- 初始化高德定位服务 2")
@@ -47,6 +85,61 @@ object LocationService {
}
}
+
+
+ private fun startPermissionMonitor() {
+ if (permissionRetryHandler != null) return
+
+ permissionRetryHandler = Handler(Looper.getMainLooper())
+ permissionRetryRunnable = object : Runnable {
+ override fun run() {
+ val ctx = appcontext ?: return
+ val hasPermission = checkPermissions(ctx)
+ if (hasPermission && !initialized) {
+ Log.d(TAG, "权限已授予,重新初始化定位服务")
+ initialize(ctx)
+ stopPermissionMonitor() // 停止定时器
+ } else {
+ permissionRetryHandler?.postDelayed(this, 5000)
+ }
+ }
+ }
+ permissionRetryHandler?.post(permissionRetryRunnable!!)
+ }
+
+ private fun stopPermissionMonitor() {
+ permissionRetryHandler?.removeCallbacks(permissionRetryRunnable!!)
+ permissionRetryHandler = null
+ permissionRetryRunnable = null
+ }
+
+ private fun checkPermissions(context: Context): Boolean {
+ if (isAppInForeground(context)) {
+ return hasForegroundLocationPermission(context)
+ }else{
+ return hasBackgroundLocationPermission(context)
+ }
+ }
+
+ private fun hasForegroundLocationPermission(context: Context): Boolean {
+ return ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.ACCESS_FINE_LOCATION
+ ) == PackageManager.PERMISSION_GRANTED
+ }
+
+ private fun hasBackgroundLocationPermission(context: Context): Boolean {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.ACCESS_BACKGROUND_LOCATION
+ ) == PackageManager.PERMISSION_GRANTED
+ } else {
+ true // Android 9 及以下没有这个权限
+ }
+ }
+
+
fun getCachedLocation(): Map? {
// // 定位成功,获取经纬度
// val latitude = location.latitude
@@ -60,6 +153,22 @@ object LocationService {
// val streetNum = location.streetNum // 门牌号
// val adCode = location.adCode // 行政区划代码
// val address = location.address // 详细地址拼接字符串
+ if (!initialized){
+ if (checkPermissions(appcontext!!)) {
+ return mapOf(
+ "user_location" to "当前定位权限为获取",
+ "user_lat" to "当前定位权限为获取",
+ "user_lng" to "当前定位权限为获取",
+ )
+ }else{
+ return mapOf(
+ "user_location" to "后台服务下,需要后台定位权限",
+ "user_lat" to "后台服务下,需要后台定位权限",
+ "user_lng" to "后台服务下,需要后台定位权限",
+ )
+ }
+ }
+
return currentLocation?.let {
mapOf(
"user_location" to it.address,
diff --git a/pubspec.yaml b/pubspec.yaml
index 391b0446e..200cfc6fb 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -57,6 +57,7 @@ dependencies:
lottie: ^3.3.1
app_settings: 5.2.0
fluwx: ^5.4.1
+ url_launcher: ^6.3.1
firebase_core: ^3.12.1
firebase_auth: ^5.5.1
google_sign_in: ^6.2.2