Browse Source

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

weicu
wolfplus2048 1 year ago
parent
commit
1287deeb4d
  1. 4
      android/app/build.gradle.kts
  2. 2
      ios/Runner/Info.plist
  3. 4
      lib/core/bindings/initial_binding.dart
  4. 103
      lib/core/utils/synchrodata.dart
  5. 151
      lib/data/models/channel_app_model.dart
  6. 47
      lib/data/models/channel_app_model.g.dart
  7. 17
      lib/data/services/account_service.dart
  8. 8
      lib/data/services/db/database_helper.dart
  9. 56
      lib/data/services/db/sqflite_api.dart
  10. 35
      lib/data/services/network/api.dart
  11. 315
      lib/data/services/version_update_service.dart
  12. 160
      lib/modules/FTFTranslation/README.md
  13. 10
      lib/modules/FTFTranslation/bindings/FTFTranslation_binding.dart
  14. 241
      lib/modules/FTFTranslation/controllers/FTFTranslation_controller.dart
  15. 212
      lib/modules/FTFTranslation/views/FTFTranslation_view.dart
  16. 189
      lib/modules/agent/controllers/agent_controller.dart
  17. 30
      lib/modules/agent/views/music_card.dart
  18. 248
      lib/modules/common/widgets/version_update_dialog.dart
  19. 53
      lib/modules/meeting/controllers/meeting_controller.dart
  20. 9
      lib/modules/meeting/controllers/meeting_details_controller.dart
  21. 5
      lib/modules/meeting/controllers/meeting_record_controller.dart
  22. 125
      lib/modules/meeting/views/meeting_view.dart
  23. 46
      lib/modules/meeting/views/share_bottom_sheet.dart
  24. 7
      lib/modules/meeting/views/speaker_all_bottom_sheet.dart
  25. 11
      lib/modules/meeting/views/speaker_bottom_sheet.dart
  26. 20
      lib/modules/music/views/music_playlist_view.dart
  27. 48
      lib/modules/settings/controllers/settings_controller.dart
  28. 25
      lib/modules/settings/views/settings_view.dart
  29. 24
      lib/modules/splash/controllers/splash_controller.dart
  30. 8
      lib/modules/translation/controllers/translation_controller.dart
  31. 4
      lib/modules/translation/views/translation_view.dart
  32. 9
      lib/routes/app_pages.dart
  33. 1
      lib/routes/app_routes.dart
  34. 129
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  35. 210
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt
  36. 96
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/Utils.kt
  37. 113
      local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt
  38. 1
      pubspec.yaml

4
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 {

2
ios/Runner/Info.plist

@ -39,7 +39,7 @@
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<key>CFBundleVersion</key> <!-- 构建号 -->
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>FacebookAdvertiserIDCollectionEnabled</key>
<false/>

4
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>(() => AccountService(), fenix: true);
// 版本更新服务
Get.lazyPut<VersionUpdateService>(() => VersionUpdateService(), fenix: true);
// 杰理蓝牙服务
// // 语言管理器(需要最先初始化)

103
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;
}
}

151
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<String, dynamic> json) =>
_$ChannelAppResponseFromJson(json);
Map<String, dynamic> toJson() => _$ChannelAppResponseToJson(this);
}
/// 渠道应用数据模型
@JsonSerializable()
class ChannelAppData {
@JsonKey(name: 'app')
final AppInfo app;
const ChannelAppData({
required this.app,
});
factory ChannelAppData.fromJson(Map<String, dynamic> json) =>
_$ChannelAppDataFromJson(json);
Map<String, dynamic> 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<String, dynamic> json) =>
_$AppInfoFromJson(json);
Map<String, dynamic> 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,
});
}

47
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<String, dynamic> json) =>
ChannelAppResponse(
code: (json['code'] as num).toInt(),
data: json['data'] == null
? null
: ChannelAppData.fromJson(json['data'] as Map<String, dynamic>),
msg: json['msg'] as String,
);
Map<String, dynamic> _$ChannelAppResponseToJson(ChannelAppResponse instance) =>
<String, dynamic>{
'code': instance.code,
'data': instance.data,
'msg': instance.msg,
};
ChannelAppData _$ChannelAppDataFromJson(Map<String, dynamic> json) =>
ChannelAppData(
app: AppInfo.fromJson(json['app'] as Map<String, dynamic>),
);
Map<String, dynamic> _$ChannelAppDataToJson(ChannelAppData instance) =>
<String, dynamic>{
'app': instance.app,
};
AppInfo _$AppInfoFromJson(Map<String, dynamic> json) => AppInfo(
address: json['address'] as String,
channel: json['channel'] as String,
description: json['description'] as String,
version: json['version'] as String,
);
Map<String, dynamic> _$AppInfoToJson(AppInfo instance) => <String, dynamic>{
'address': instance.address,
'channel': instance.channel,
'description': instance.description,
'version': instance.version,
};

17
lib/data/services/account_service.dart

@ -25,7 +25,7 @@ class AccountService extends GetxService {
Future<bool> 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<bool> 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,

8
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 (

56
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<String, Object?> 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,
});
});
}

35
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,
);
}
}

315
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<String> getCurrentVersion() async {
try {
final packageInfo = await PackageInfo.fromPlatform();
return '${packageInfo.version}+${packageInfo.buildNumber}';
} catch (e) {
_logger.e('获取当前版本信息失败: $e');
return '1.0.0';
}
}
/// 获取当前平台信息
Future<PlatformInfo> 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<PlatformInfo> 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<VersionUpdateInfo> 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<int> _parseVersion(String version) {
final parts = version.split('.');
final result = <int>[];
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<bool> 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<bool> _openAppStore() async {
// 这里需要替换为实际的App Store ID
const appStoreUrl = 'https://apps.apple.com/app/id你的应用ID';
return await _launchUrl(appStoreUrl);
}
/// 打开Google Play
Future<bool> _openGooglePlay() async {
// 这里需要替换为实际的包名
const packageName = 'com.yourcompany.deepvoice';
const playStoreUrl =
'https://play.google.com/store/apps/details?id=$packageName';
return await _launchUrl(playStoreUrl);
}
/// 打开华为应用市场
Future<bool> _openHuaweiStore() async {
const huaweiStoreUrl = 'appmarket://details?id=com.yourcompany.deepvoice';
return await _launchUrl(huaweiStoreUrl);
}
/// 打开小米应用商店
Future<bool> _openXiaomiStore() async {
const xiaomiStoreUrl = 'mimarket://details?id=com.yourcompany.deepvoice';
return await _launchUrl(xiaomiStoreUrl);
}
/// 打开OPPO软件商店
Future<bool> _openOppoStore() async {
const oppoStoreUrl =
'oppomarket://details?packagename=com.yourcompany.deepvoice';
return await _launchUrl(oppoStoreUrl);
}
/// 打开vivo应用商店
Future<bool> _openVivoStore() async {
const vivoStoreUrl = 'vivomarket://details?id=com.yourcompany.deepvoice';
return await _launchUrl(vivoStoreUrl);
}
/// 打开荣耀应用市场
Future<bool> _openHonorStore() async {
const honorStoreUrl = 'honormarket://details?id=com.yourcompany.deepvoice';
return await _launchUrl(honorStoreUrl);
}
/// 打开官网下载
Future<bool> _openOfficialDownload(String downloadUrl) async {
return await _launchUrl(downloadUrl);
}
/// 启动URL
Future<bool> _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;
}
}
}

160
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年
- **状态**: 完整功能实现

10
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>(() => FTFTranslationController());
}
}

241
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 = <FTFTranslationMessage>[].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<VolcanoTranslationService>();
final LanguageManager _languageManager = Get.find<LanguageManager>();
// 当前使用的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<TtsService>();
_asrService = Get.find<XunfeiAsrService>();
}
@override
void onClose() {
_asrService.dispose();
_recognitionSubscription?.cancel();
super.onClose();
}
//开始录音
Future<void> _startRecorder() async {
final recognitionStream =
await _asrService.startContinuousRecognition(false);
_recognitionSubscription = recognitionStream.listen(
_handleRecognitionEvent,
);
}
//结束录音
Future<void> _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<String?> 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<void> handleFinalResult(String text) async {
if (text.isEmpty) return;
// 使用项目的时间戳进行翻译
translateText(text, isFinal: true);
}
// 播放翻译文本
Future<void> 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,
});
}

212
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<FTFTranslationController> {
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<String> 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<String>(
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,
),
),
],
),
),
);
}
}

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

@ -91,6 +91,17 @@ class AgentController extends GetxController {
_loadTtsPreference(); //加载TTS偏好设置
_loadChatHistory(); //加载聊天历史记录
_subscribeToEvents(); //订阅代理服务事件
_initMusicStatus(); //初始化音乐状态
}
// 初始化音乐播放状态
Future<void> _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<void> 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<String, dynamic> 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<String, dynamic>;
song["url"] = urldata['url'] ?? '';
} catch (e) {
Logger.e(TAG, '转换响应数据失败: $e');
return;
Future<void> musicPlay(Map<String, dynamic> 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<String, dynamic>;
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 = <String, String>{
'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<void> 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<void> 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<void> 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');
}

30
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

248
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<void> _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<void> show(VersionUpdateInfo updateInfo) async {
await Get.dialog(
VersionUpdateDialog(updateInfo: updateInfo),
// 关键修改:禁用外部点击关闭
barrierDismissible: false,
// 防止返回键关闭(在强制更新模式下)
//useRootNavigator: true,
);
}
}

53
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<String> selected = [];
final appDocDir = getApplicationDocumentsDirectory();
//final appopus = getExternalStorageDirectories();
// final directory = Directory(
// '/storage/emulated/0/Android/data/com.yunqiinnovation.deepsound/files');
List<FileSystemEntity> files = [];
TextEditingController titleController = TextEditingController(); // 录音标题
RxList dataList = [].obs;
// 蓝牙服务
final bleManager = Get.find<BleManager>();
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<String, dynamic> 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<void> 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<String, Object?> 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;
}
}

9
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();

5
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();

125
lib/modules/meeting/views/meeting_view.dart

@ -11,27 +11,30 @@ import '../model/meeting_model.dart';
class MeetingView extends GetView<MeetingController> {
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<MeetingController> {
// 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<MeetingController> {
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<MeetingController> {
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();

46
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<ShareBottomSheet> {
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<ShareBottomSheet> {
}
return text;
}
Future<XFile> 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);
}
}

7
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<SpeakerAllBottomSheet> {
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(

11
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<SpeakerBottomSheet> {
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<SpeakerBottomSheet> {
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

20
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

48
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<void> 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,
);
}
}
}

25
lib/modules/settings/views/settings_view.dart

@ -543,6 +543,28 @@ class SettingsView extends GetView<SettingsController> {
},
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<SettingsController> {
),
InkWell(
onTap: () {
// 在这里添加检查更新的逻辑
// 调用检查更新方法
controller.checkForUpdate();
},
child: Container(
constraints: BoxConstraints(

24
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');
// 版本检查失败不影响用户使用,静默处理
}
});
}
}

8
lib/modules/translation/controllers/translation_controller.dart

@ -26,7 +26,7 @@ class TranslationController extends GetxController {
final LanguageManager _languageManager = Get.find<LanguageManager>();
final GetStorage _storage = GetStorage();
// 蓝牙服务
final _bleManager = Get.find<BleManager>();
final bleManager = Get.find<BleManager>();
// 音频输入源
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<void> stopAll() async {
try {
if (currentMode.value == "audioVideo" || currentMode.value == "call") {
_bleManager.closeCodec();
bleManager.closeCodec();
// 发送ble音乐或者通话远端声音
Logger.info('发送ble音乐或者通话远端声音');
}

4
lib/modules/translation/views/translation_view.dart

@ -67,12 +67,12 @@ class TranslationView extends GetView<TranslationController> {
_buildPopupMenuItem(
'audioVideo',
'audioVideoTranslation'.tr,
enabled: true, // 根据连接状态启用/禁用
enabled: controller.bleManager.isConnected, // 根据连接状态启用/禁用
),
_buildPopupMenuItem(
'call',
'callTranslation'.tr,
enabled: true, // 根据连接状态启用/禁用
enabled: controller.bleManager.isConnected, // 根据连接状态启用/禁用
),
],
child: Row(

9
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(),

1
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'; // 语音识别测试

129
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<ScanResult>()
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() {

210
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, Any>): 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, Any>): String {
val title = arguments["title"] as? String
val content = arguments["content"] as? String ?: ""

96
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()
}
}
/**
* 检查应用是否在前台运行
* @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
}

113
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<String, String>? {
// // 定位成功,获取经纬度
// 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,

1
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

Loading…
Cancel
Save