Browse Source

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

weicu
tanlongsheng 1 year ago
parent
commit
da59adaef6
  1. 3
      devtools_options.yaml
  2. 6
      lib/core/bindings/initial_binding.dart
  3. 2
      lib/core/themes/language/lang/en_us.dart
  4. 2
      lib/core/themes/language/lang/zh_cn.dart
  5. 6
      lib/data/models/user_Info.dart
  6. 2
      lib/data/services/music_manager.dart
  7. 9
      lib/data/services/network/api.dart
  8. 535
      lib/modules/agent/controllers/agent_controller.dart
  9. 1020
      lib/modules/agent/views/agent_view.dart
  10. 9
      lib/modules/agent/views/message_bubble.dart
  11. 42
      lib/modules/agent/views/music_card.dart
  12. 11
      lib/modules/devices/bindings/devices_binding.dart
  13. 61
      lib/modules/devices/controllers/devices_controller.dart
  14. 290
      lib/modules/devices/views/devices_view.dart
  15. 15
      lib/modules/music/bindings/music_binding.dart
  16. 493
      lib/modules/music/controllers/music_controller.dart
  17. 125
      lib/modules/music/services/music_service_facade.dart
  18. 108
      lib/modules/music/views/music_playlist_view.dart
  19. 149
      lib/modules/pairing/controllers/pairing_controller.dart
  20. 47
      lib/modules/pairing/views/pairing_view.dart
  21. 20
      lib/modules/pairing/views/pairing_view_new.dart
  22. 8
      lib/modules/settings/views/settings_view.dart
  23. 7
      lib/routes/app_pages.dart
  24. 3
      lib/routes/app_routes.dart
  25. 2
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  26. 4
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt
  27. 146
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt
  28. 8
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt
  29. 14
      local_plugins/music_service/lib/music_service.dart

3
devtools_options.yaml

@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

6
lib/core/bindings/initial_binding.dart

@ -10,8 +10,6 @@ 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';
import '../../modules/music/controllers/music_controller.dart';
import '../../modules/music/services/music_service_facade.dart';
/// 初始绑定,用于管理全局依赖
class InitialBinding extends Bindings {
@ -37,10 +35,8 @@ class InitialBinding extends Bindings {
Get.lazyPut<BleManager>(() => BleManager(), fenix: true);
// MusicManager
Get.lazyPut<MusicManager>(() => MusicManager(), fenix: true);
// 音乐模块
Get.lazyPut<MusicController>(() => MusicController(), fenix: true);
Get.lazyPut<MusicServiceFacade>(() => MusicServiceFacade(), fenix: true);
Get.lazyPut<NavigationManager>(() => NavigationManager(), fenix: true);
Get.lazyPut<LocationManager>(() => LocationManager(), fenix: true);
// 火山AI服务

2
lib/core/themes/language/lang/en_us.dart

@ -90,4 +90,6 @@ const Map<String, String> enUS = {
"goToBind": "Unbind",
"scanQrCode": "Scan the QR code",
"endRecording": "Do you want to end the recording?",
"earphone": "Earphone",
"voicerecorder": "VoiceRecorder"
};

2
lib/core/themes/language/lang/zh_cn.dart

@ -88,4 +88,6 @@ const Map<String, String> zhCN = {
"goToBind": "去绑定",
"scanQrCode": "扫描二维码",
"endRecording": "你想结束录音吗?",
"earphone": "耳机",
"voicerecorder": "录音笔"
};

6
lib/data/models/user_Info.dart

@ -21,6 +21,7 @@ class User {
String phonemac;
String password;
int viplv = 0; //添加会员等级字段,0表示普通用户
String earphonename;
String earphonemac;
String earphoneauthcode;
// final Int64 lastbettime;
@ -41,6 +42,7 @@ class User {
required this.phonemac,
required this.password,
required this.viplv, // 添加默认值
required this.earphonename,
required this.earphonemac,
required this.earphoneauthcode,
// required this.lastbettime,
@ -70,6 +72,7 @@ class User {
phonemac: (userData?['phonemac'] as String?) ?? '',
password: (userData?['password'] as String?) ?? '',
viplv: (userData?['viplv'] as int?) ?? 0, // 提供默认值
earphonename: (userData?['earphonename'] as String?) ?? '',
earphonemac: (userData?['earphonemac'] as String?) ?? '',
earphoneauthcode: (userData?['earphoneauthcode'] as String?) ?? '',
// lastbettime: _parseTimestamp(userData['lastbettime']),
@ -97,6 +100,7 @@ class User {
password: (userData?['password'] as String?) ?? '',
viplv: (userData?['viplv'] as int?) ?? 0, // 提供默认值
earphonename: (userData?['earphonename'] as String?) ?? '',
earphonemac: (userData?['earphonemac'] as String?) ?? '',
earphoneauthcode: (userData?['earphoneauthcode'] as String?) ?? '',
// lastbettime: _parseTimestamp(userData['lastbettime']),
@ -121,6 +125,8 @@ class User {
'phonemac': phonemac,
'password': password,
'viplv': viplv,
'earphonename': earphonename,
'earphonemac': earphonemac,
'earphoneauthcode': earphoneauthcode,

2
lib/data/services/music_manager.dart

@ -105,6 +105,7 @@ class MusicManager extends GetxService {
required Function() onPrevious,
required Function(int) onSeekTo,
required Function() onStop,
required Function(String, int, int, double) onStop1,
}) async {
try {
await _musicService.setMediaSessionCallbacks(
@ -114,6 +115,7 @@ class MusicManager extends GetxService {
onPrevious: onPrevious,
onSeekTo: onSeekTo,
onStop: onStop,
onStop1: onStop1,
);
} catch (e) {
Logger.d('MusicManager', '设置媒体会话回调失败: $e');

9
lib/data/services/network/api.dart

@ -136,6 +136,15 @@ class Api {
);
}
//解绑定设备
static unbindearphone([params]) {
return DioManager().request(
NWMethod.post,
'/api/home/user_unbindearphone',
params: params,
);
}
//绑定设备
static bindauthcode([params]) {
return DioManager().request(

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

@ -25,23 +25,35 @@ import '../../../data/models/message_model.dart';
// 语音输入模式枚举
enum VoiceInputMode {
text, // 文字输入模式
pushToTalk, // 按住说话模式
phoneCall, // 打电话模式
text, // 文字输入模式
pushToTalk, // 按住说话模式
phoneCall, // 打电话模式
}
class AgentController extends GetxController {
static AgentController get to => Get.find(); //单例访问模式
// 聊天消息列表
final messages = <Message>[].obs;
// UI控制器
final TextEditingController textController = TextEditingController();
final ScrollController scrollController = ScrollController();
//音乐播放管理
final _musiceManager = Get.find<MusicManager>();
// 导航管理
final _navigationManager = Get.find<NavigationManager>();
RxBool isPlaying = false.obs;
RxInt currentId = 0.obs;
// 播放进度相关状态
RxInt currentPosition = 0.obs; // 当前播放位置(毫秒)
RxInt duration = 0.obs; // 总时长(毫秒)
RxDouble progress = 0.0.obs; // 播放进度(0.0-1.0)
// 当前播放列表管理
final currentPlaylist = <Map<String, dynamic>>[].obs; // 当前播放列表
final currentPlaylistIndex = 0.obs; // 当前播放歌曲在列表中的索引
// 各种状态标志
final isInitialized = false.obs;
@ -98,6 +110,8 @@ class AgentController extends GetxController {
// BLE事件订阅
StreamSubscription<dynamic>? _bleEventSubscription;
// 进度更新定时器
Timer? _progressTimer;
// 用于跟踪AI回复状态
bool _isNewAssistantResponse = true;
String _currentResponseId = '';
@ -110,17 +124,54 @@ class AgentController extends GetxController {
// 是否已加载聊天记录
final isHistoryLoaded = false.obs;
// 添加加载状态跟踪
final RxBool isLoadingMusic = false.obs;
final RxString loadingMusicId = ''.obs;
// 设置加载状态的方法
void setLoadingMusic(String id, bool loading) {
loadingMusicId.value = loading ? id : '';
isLoadingMusic.value = loading;
}
@override
void onInit() {
super.onInit();
_loadTtsPreference(); //加载TTS偏好设置
_loadChatHistory(); //加载聊天历史记录
_subscribeToEvents(); //订阅代理服务事件
_subscribeToBleEvents(); //订阅BLE事件
_initMusicStatus(); //初始化音乐状态
_setupMediaSessionCallbacks(); //设置媒体会话回调
_setupTextInputListener(); //设置文本输入监听器
}
// 初始化音乐播放状态
Future<void> _initMusicStatus() async {
try {
await musicStatus();
Logger.d('AgentController', '音乐状态初始化完成');
} catch (e) {
Logger.e('AgentController', '音乐状态初始化失败: $e');
}
}
// 设置媒体会话回调
void _setupMediaSessionCallbacks() async {
try {
await _musiceManager.setMediaSessionCallbacks(
onPlay: onMediaSessionPlay,
onPause: onMediaSessionPause,
onNext: onMediaSessionNext,
onPrevious: onMediaSessionPrevious,
onSeekTo: onMediaSessionSeekTo,
onStop: onMediaSessionStop,
onStop1: onMediaSessionSeekTo1,
);
Logger.d('AgentController', '媒体会话回调设置完成');
} catch (e) {
Logger.e('AgentController', '媒体会话回调设置失败: $e');
}
}
@override
void onClose() {
@ -994,7 +1045,8 @@ class AgentController extends GetxController {
_pendingPlusOptionsExpansion = false;
// 添加小延迟确保UI稳定
Future.delayed(const Duration(milliseconds: 50), () {
if (!isKeyboardVisible.value) { // 再次确认键盘状态
if (!isKeyboardVisible.value) {
// 再次确认键盘状态
isPlusOptionsExpanded.value = true;
}
});
@ -1194,7 +1246,8 @@ class AgentController extends GetxController {
}
// 检查文字模式与语音模式的一致性
if (isTextInputMode.value && (isPushToTalkMode.value || isPhoneCallMode.value)) {
if (isTextInputMode.value &&
(isPushToTalkMode.value || isPhoneCallMode.value)) {
Logger.w(TAG, '状态冲突:文字模式与语音模式同时启用,优先保持语音模式');
isTextInputMode.value = false;
}
@ -1268,12 +1321,299 @@ class AgentController extends GetxController {
await stopVoiceInput();
}
// 获取音乐播放状态
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}');
print("音乐状态更新: playing=${isPlaying.value}, url=${currentId.value}");
} catch (e) {
Logger.e('AgentController', '获取音乐状态失败: $e');
}
}
// 播放音乐
Future<void> musicPlay(Map<String, dynamic> song) async {
try {
Logger.d(TAG, '开始播放音乐: ${song['title'] ?? song['name']}');
// 立即更新UI状态,提供即时反馈
isPlaying.value = true;
// 修复:确保currentId接收int类型
final songId = song['id'];
currentId.value = songId is int
? songId
: (int.tryParse(songId?.toString() ?? '0') ?? 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(TAG, '音乐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接收int类型
final newSongId = song['id'];
currentId.value = newSongId is int
? newSongId
: (int.tryParse(newSongId?.toString() ?? '0') ?? 0);
Logger.d(TAG, '获取新URL成功');
} catch (e) {
Logger.e(TAG, '获取新URL失败: $e');
// 如果获取新URL失败,尝试使用原URL
}
}
}
// 转换为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);
// 更新媒体会话元数据和播放状态
await _updateMediaSessionMetadata(musicData);
await _updateMediaSessionPlaybackState(true);
// 启动进度定时器
_startProgressTimer();
Logger.d(TAG, '音乐播放启动成功');
} catch (e) {
Logger.e(TAG, '播放音乐失败: $e');
// 发生错误时重置状态
isPlaying.value = false;
currentId.value = 0;
}
}
// 暂停音乐
Future<void> musicPause() async {
try {
Logger.d('AgentController', '暂停音乐播放');
await _musiceManager.pause();
isPlaying.value = false;
_stopProgressTimer();
// 更新媒体会话播放状态
await _updateMediaSessionPlaybackState(false);
} catch (e) {
Logger.e('AgentController', '暂停音乐失败: $e');
}
}
// 恢复音乐播放
Future<void> musicResume() async {
try {
Logger.d('AgentController', '恢复播放音乐');
await _musiceManager.resume();
isPlaying.value = true;
_startProgressTimer();
// 更新媒体会话播放状态
await _updateMediaSessionPlaybackState(true);
Logger.d('AgentController', '音乐恢复播放成功');
} catch (e) {
Logger.e('AgentController', '恢复播放音乐失败: $e');
isPlaying.value = false;
}
}
// 启动进度更新定时器
void _startProgressTimer() {
_stopProgressTimer(); // 先停止现有定时器
Logger.d('AgentController', '启动播放进度定时器');
_progressTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) {
if (isPlaying.value && currentId.value != 0) {
updateMusicProgress();
} else if (!isPlaying.value) {
// 如果不在播放状态,停止定时器
timer.cancel();
_progressTimer = null;
Logger.d('AgentController', '播放已停止,取消进度定时器');
}
});
}
// 停止进度更新定时器
void _stopProgressTimer() {
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();
// final newPosition = progressInfo['currentPosition'] ?? 0;
// final newDuration = progressInfo['duration'] ?? 0;
// final newProgress = progressInfo['progress'] ?? 0.0;
// bool hasUpdate = false;
// // 只有当值发生变化时才更新,避免不必要的UI刷新
// if (currentPosition.value != newPosition) {
// currentPosition.value = newPosition;
// hasUpdate = true;
// }
// if (duration.value != newDuration) {
// duration.value = newDuration;
// hasUpdate = true;
// }
// if ((progress.value - newProgress).abs() > 0.001) {
// // 避免浮点数精度问题
// progress.value = newProgress;
// hasUpdate = true;
// }
// 如果有进度更新,通知系统媒体会话更新进度信息
// if (hasUpdate) {
// await _updateMediaSessionProgress();
// }
} catch (e) {
Logger.e('AgentController', '获取播放进度失败: $e');
}
}
// 获取播放进度
Future<void> updateMusicProgress1(String currentIdInternal, int newPosition,
int newDuration, double newProgress) async {
try {
if (int.parse(currentIdInternal) != 0) {
isPlaying.value = true;
// return; // 如果没有在播放,直接返回
}
// // var progressInfo = await _musiceManager.getProgress();
// final newPosition = progressInfo['currentPosition'] ?? 0;
// final newDuration = progressInfo['duration'] ?? 0;
// final newProgress = progressInfo['progress'] ?? 0.0;
print(
"updateMusicProgress1:currentIdInternal:$currentIdInternal newPosition:$newPosition newDuration:$newDuration newProgress:$newProgress");
bool hasUpdate = false;
if (currentId.value != int.parse(currentIdInternal)) {
currentId.value = int.parse(currentIdInternal);
hasUpdate = true;
}
// 只有当值发生变化时才更新,避免不必要的UI刷新
if (currentPosition.value != newPosition) {
currentPosition.value = newPosition;
hasUpdate = true;
}
if (duration.value != newDuration) {
duration.value = newDuration;
hasUpdate = true;
}
if ((progress.value - newProgress).abs() > 0.001) {
// 避免浮点数精度问题
progress.value = newProgress;
hasUpdate = true;
}
// // 如果有进度更新,通知系统媒体会话更新进度信息
// if (hasUpdate) {
// await _updateMediaSessionProgress();
// }
} catch (e) {
Logger.e('AgentController', '获取播放进度失败: $e');
}
}
/// 更新系统媒体会话的进度信息
Future<void> _updateMediaSessionProgress() async {
try {
// 这里将调用music_service插件的方法来更新系统媒体会话进度
// 具体实现将在music_service插件中添加
await _musiceManager.updateMediaSessionProgress(
currentPosition.value,
duration.value,
progress.value,
);
} catch (e) {
// 忽略错误,避免影响正常播放
Logger.d('AgentController', '更新媒体会话进度失败: $e');
}
}
/// 更新系统媒体会话的元数据
Future<void> _updateMediaSessionMetadata(Map<String, dynamic> song) async {
try {
await _musiceManager.updateMediaSessionMetadata(song);
} catch (e) {
Logger.d('AgentController', '更新媒体会话元数据失败: $e');
}
}
/// 更新系统媒体会话的播放状态
Future<void> _updateMediaSessionPlaybackState(bool isPlaying) async {
try {
await _musiceManager.updateMediaSessionPlaybackState(isPlaying);
} catch (e) {
Logger.d('AgentController', '更新媒体会话播放状态失败: $e');
}
}
// 跳转到指定位置
Future<void> seekToPosition(int positionMs) async {
try {
await _musiceManager.seekTo(positionMs);
// 立即更新进度状态
// await updateMusicProgress();
} catch (e) {
Logger.e('AgentController', '跳转播放位置失败: $e');
}
}
// 跳转到指定位置
Future<void> seekToPosition1(String currentIdInternal, int newPosition,
int newDuration, double newProgress) async {
try {
//await _musiceManager.seekTo(positionMs);
// 立即更新进度状态
await updateMusicProgress1(
currentIdInternal, newPosition, newDuration, newProgress);
} catch (e) {
Logger.e('AgentController', '跳转播放位置失败: $e');
}
}
// 修改自动停止处理,遵循TTS设置
void _handleAutoStop(int index) {
@ -1313,7 +1653,188 @@ class AgentController extends GetxController {
);
}
/// 播放音乐列表
Future<void> playPlaylist(List<Map<String, dynamic>> playlist) async {
try {
if (playlist.isEmpty) {
Logger.e(TAG, '播放列表为空,无法播放');
return;
}
Logger.d(TAG, '开始播放音乐列表,共${playlist.length}首歌曲');
// 保存当前播放列表
currentPlaylist.assignAll(playlist);
currentPlaylistIndex.value = 0;
// 设置加载状态
isLoadingMusic.value = true;
loadingMusicId.value = playlist[0]['id']?.toString() ?? '';
// 重置播放状态
isPlaying.value = true;
currentPosition.value = 0;
duration.value = 0;
progress.value = 0.0;
// 格式化歌曲列表数据
final formattedPlaylist = playlist.map((song) {
return {
'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() ?? '',
};
}).toList();
// 调用音乐管理器播放列表
await _musiceManager.playPlaylist(formattedPlaylist);
// 更新当前播放歌曲ID
if (formattedPlaylist.isNotEmpty) {
currentId.value = int.tryParse(formattedPlaylist[0]['id'] ?? '0') ?? 0;
}
// 启动进度定时器
_startProgressTimer();
Logger.d(TAG, '音乐列表播放启动成功');
} catch (e) {
Logger.e(TAG, '播放音乐列表失败: $e');
isPlaying.value = false;
currentId.value = 0;
// 显示错误提示
Get.snackbar(
'播放失败',
'无法播放音乐列表,请稍后重试',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red[400],
colorText: Colors.white,
duration: const Duration(seconds: 2),
);
} finally {
// 清除加载状态
isLoadingMusic.value = false;
loadingMusicId.value = '';
}
}
/// 播放上一曲
Future<void> playPrevious() async {
if (currentPlaylist.isEmpty) {
Logger.w(TAG, '当前播放列表为空,无法播放上一曲');
return;
}
// 计算上一曲的索引
int previousIndex = currentPlaylistIndex.value - 1;
if (previousIndex < 0) {
previousIndex = currentPlaylist.length - 1; // 循环到最后一首
}
currentPlaylistIndex.value = previousIndex;
final previousSong = currentPlaylist[previousIndex];
Logger.d(TAG, '播放上一曲: ${previousSong['name'] ?? previousSong['title']}');
await musicPlay(previousSong);
}
/// 播放下一曲
Future<void> playNext() async {
if (currentPlaylist.isEmpty) {
Logger.w(TAG, '当前播放列表为空,无法播放下一曲');
return;
}
// 计算下一曲的索引
int nextIndex = currentPlaylistIndex.value + 1;
if (nextIndex >= currentPlaylist.length) {
nextIndex = 0; // 循环到第一首
}
currentPlaylistIndex.value = nextIndex;
final nextSong = currentPlaylist[nextIndex];
Logger.d(TAG, '播放下一曲: ${nextSong['name'] ?? nextSong['title']}');
await musicPlay(nextSong);
}
/// 更新当前播放列表和索引(当单独播放某首歌曲时调用)
void updateCurrentPlaylistContext(
List<Map<String, dynamic>> playlist, int songId) {
currentPlaylist.assignAll(playlist);
// 查找当前歌曲在列表中的索引
int index = -1;
for (int i = 0; i < playlist.length; i++) {
if (int.tryParse(playlist[i]['id']?.toString() ?? '0') == songId) {
index = i;
break;
}
}
if (index != -1) {
currentPlaylistIndex.value = index;
Logger.d(TAG, '更新播放列表上下文,当前歌曲索引: $index');
}
}
// ========== 媒体会话回调处理方法 ==========
/// 处理来自系统通知栏的播放操作
Future<void> onMediaSessionPlay() async {
Logger.d(TAG, '收到媒体会话播放回调');
if (!isPlaying.value) {
await musicResume();
}
}
/// 处理来自系统通知栏的暂停操作
Future<void> onMediaSessionPause() async {
Logger.d(TAG, '收到媒体会话暂停回调');
if (isPlaying.value) {
await musicPause();
}
}
/// 处理来自系统通知栏的下一曲操作
Future<void> onMediaSessionNext() async {
Logger.d(TAG, '收到媒体会话下一曲回调');
await playNext();
}
/// 处理来自系统通知栏的上一曲操作
Future<void> onMediaSessionPrevious() async {
Logger.d(TAG, '收到媒体会话上一曲回调');
await playPrevious();
}
/// 处理来自系统通知栏的跳转操作
Future<void> onMediaSessionSeekTo(int positionMs) async {
Logger.d(TAG, '收到媒体会话跳转回调: ${positionMs}ms');
await seekToPosition(positionMs);
}
/// 处理来自系统通知栏的停止操作
Future<void> onMediaSessionStop() async {
Logger.d(TAG, '收到媒体会话停止回调');
await musicPause();
// 重置播放状态
currentPosition.value = 0;
progress.value = 0.0;
_stopProgressTimer();
}
/// 处理来自系统通知栏的跳转操作
Future<void> onMediaSessionSeekTo1(String currentIdInternal, int newPosition,
int newDuration, double newProgress) async {
print(
"onMediaSessionSeekTo1 currentIdInternal :$currentIdInternal ,newPosition:$newPosition newDuration:$newDuration newProgress:$newProgress");
await seekToPosition1(
currentIdInternal, newPosition, newDuration, newProgress);
}
}

1020
lib/modules/agent/views/agent_view.dart

File diff suppressed because it is too large

9
lib/modules/agent/views/message_bubble.dart

@ -240,9 +240,11 @@ class MessageBubble extends GetView<AgentController> {
final musicUrl = card_music['url'] as String? ?? '';
final musicSinger = card_music['sgener'] as String? ?? '未知歌手';
final musicImage = card_music['image'] as String? ?? '';
final ts = card_music['ts'] as int? ?? 0;
print("music_play card_music: $card_music");
//音乐卡片
return buildMusicItem(
context, musicId, musicName, musicUrl, musicSinger, musicImage);
return buildMusicItem(context, musicId, musicName, musicUrl,
musicSinger, musicImage, ts);
}
} else if (meta!.keys.contains('music_playlist')) {
final card_musiclist =
@ -281,7 +283,8 @@ class MessageBubble extends GetView<AgentController> {
final end = card_navigation['end'] as String? ?? '';
final destination = card_navigation['destination'] as String? ?? '';
final rawList = card_navigation['items'] as List? ?? [];
final list = rawList.map((e) => Map<String, dynamic>.from(e)).toList();
final list =
rawList.map((e) => Map<String, dynamic>.from(e)).toList();
final rawNavigation = card_navigation['navigation'];
final Map<String, dynamic>? navigation =
rawNavigation is Map<String, dynamic> ? rawNavigation : null;

42
lib/modules/agent/views/music_card.dart

@ -1,22 +1,24 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../music/services/music_service_facade.dart';
import '../../../routes/app_routes.dart';
import '../controllers/agent_controller.dart';
import 'circular_progress_button.dart';
/// 构建音乐项目
Widget buildMusicItem(BuildContext context, int id, String name, String url,
String singer, String image) {
final musicFacade = Get.find<MusicServiceFacade>(); // 获取音乐服务门面
String singer, String image, int ts) {
final controller = Get.find<AgentController>(); // 获取控制器
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
Map<String, dynamic> song = {
'id': id,
'url': url,
'title': name,
'artist': singer,
'coverUrl': image,
'ts': ts,
};
// 音乐状态由MusicController自动管理,无需手动刷新
controller.musicStatus(); //刷新状态
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
@ -82,11 +84,11 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url,
),
Obx(() {
final isThisPlaying =
musicFacade.isPlaying.value && musicFacade.currentId.value == id;
controller.isPlaying.value && controller.currentId.value == id;
// 只有当前播放的歌曲才显示进度
final currentProgress =
isThisPlaying ? musicFacade.progress.value : 0.0;
isThisPlaying ? controller.progress.value : 0.0;
return CircularProgressButton(
isPlaying: isThisPlaying,
@ -97,21 +99,21 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url,
strokeWidth: 2.0,
onPlayPause: () async {
try {
if (musicFacade.isPlaying.value) {
if (musicFacade.currentId.value == id) {
if (controller.isPlaying.value) {
if (controller.currentId.value == id) {
// 当前歌曲正在播放,暂停它
await musicFacade.musicPause();
await controller.musicPause();
} else {
// 其他歌曲正在播放,播放这首歌
await musicFacade.musicPlay(song);
await controller.musicPlay(song);
}
} else {
if (musicFacade.currentId.value == id) {
if (controller.currentId.value == id) {
// 当前歌曲已暂停,恢复播放
await musicFacade.musicResume();
await controller.musicResume();
} else {
// 播放新歌曲
await musicFacade.musicPlay(song);
await controller.musicPlay(song);
}
}
} catch (e) {
@ -122,8 +124,8 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url,
? (seekProgress) {
// 只有当前播放的歌曲才允许跳转
final targetPosition =
(musicFacade.duration.value * seekProgress).round();
musicFacade.seekToPosition(targetPosition);
(controller.duration.value * seekProgress).round();
controller.seekToPosition(targetPosition);
}
: null,
);
@ -137,7 +139,8 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url,
Widget buildMusicContent(
BuildContext context, List<Map<String, dynamic>> playlist) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
final controller = Get.find<AgentController>(); // 获取控制器
controller.updateCurrentPlaylistContext(playlist, controller.currentId.value);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -202,9 +205,10 @@ Widget buildMusicContent(
final url = item["url"] ?? "";
final singer = item["sgener"] ?? "未知歌手";
final image = item["image"] ?? "";
final ts = item["ts"] ?? 0;
return Column(
children: [
buildMusicItem(context, id, name, url, singer, image),
buildMusicItem(context, id, name, url, singer, image, ts),
if (index != playlist.length - 1)
Divider(
height: 1,
@ -221,7 +225,7 @@ Widget buildMusicContent(
child: TextButton(
onPressed: () {
// 跳转到完整歌单页面,传递完整的歌单数据
Get.toNamed('/music/playlist', arguments: playlist);
Get.toNamed(Routes.musicPlaylist, arguments: playlist);
},
style: TextButton.styleFrom(
minimumSize: Size.zero,

11
lib/modules/devices/bindings/devices_binding.dart

@ -0,0 +1,11 @@
import 'package:deep_voice/modules/devices/controllers/devices_controller.dart';
import 'package:get/get.dart';
class DevicesBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<DevicesController>(
() => DevicesController(),
);
}
}

61
lib/modules/devices/controllers/devices_controller.dart

@ -0,0 +1,61 @@
import 'package:deep_voice/core/utils/logger.dart';
import 'package:deep_voice/data/models/user_Info.dart';
import 'package:deep_voice/data/services/ble_manager.dart';
import 'package:deep_voice/data/services/network/api.dart';
import 'package:deep_voice/routes/app_routes.dart';
import 'package:get/get.dart';
import 'package:get/get_rx/get_rx.dart';
import 'package:get/get_state_manager/src/simple/get_controllers.dart';
/*
设备管理
*/
class DevicesController extends GetxController {
late final BleManager _bleManager;
//耳机绑定
final isearphone = false.obs;
final earphoneName = "".obs;
final earphoneMac = "".obs;
final earphoneCode = "".obs;
//录音笔
final isvoicepen = false.obs;
final voicepenName = "".obs;
final voicepenMac = "".obs;
@override
void onInit() {
super.onInit();
_bleManager = Get.find<BleManager>();
earphoneName.value = User.instance.earphonename;
earphoneMac.value = User.instance.earphonemac;
earphoneCode.value = User.instance.earphoneauthcode;
if (earphoneCode.value != "") {
isearphone.value = true;
}
}
@override
void onClose() {
super.onClose();
}
void unbindearphone() async {
final response = await Api.unbindearphone({
"code": earphoneCode.value,
"earphonname": earphoneMac.value,
});
if (response != null) {
User.instance.earphonename = "";
User.instance.earphonemac = "";
User.instance.earphoneauthcode = "";
isearphone.value = false;
_bleManager.clearPairedDevices();
_bleManager.disconnect();
Get.toNamed(Routes.pairing);
return;
} else {
return;
}
}
}

290
lib/modules/devices/views/devices_view.dart

@ -0,0 +1,290 @@
import 'dart:ui';
import 'package:deep_voice/modules/devices/controllers/devices_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
class DevicesView extends GetView<DevicesController> {
const DevicesView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// 直接使用 Theme.of(context) 获取当前主题
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: AppBar(
automaticallyImplyLeading: false,
centerTitle: true,
elevation: 0,
backgroundColor: isDarkMode ? Colors.black : Colors.white,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
size: 20.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
onPressed: () {
Get.back(); // 正常返回
},
),
title: Text(
'设备管理'.tr,
style: TextStyle(
fontSize: 20.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),
body: ListView(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
children: [
_buildEarphoneCard(isDarkMode),
SizedBox(height: 20.h),
_buildVoiceRecorderCard(isDarkMode),
SizedBox(height: 20.h),
],
),
);
}
// 耳机
Widget _buildEarphoneCard(bool isDarkMode) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(left: 4.w, bottom: 8.h),
child: Text(
'earphone'.tr, // AI设置
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.blue[400] : Colors.blue[600],
letterSpacing: 1,
),
),
),
Container(
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16.r),
child: BackdropFilter(
filter: isDarkMode
? ImageFilter.blur(sigmaX: 5, sigmaY: 5)
: ImageFilter.blur(sigmaX: 0, sigmaY: 0),
child: Column(
children: [
Obx(
() =>
// 耳机列表
_buildSimpleNavigationSetting(
title: controller.isearphone.value
? controller.earphoneName.value
: "未绑定",
subtitle: controller.isearphone.value
? controller.earphoneMac.value
: "还未绑定设备",
icon: Icons.headphones,
iconBgColor: isDarkMode
? const Color.fromARGB(255, 255, 255, 255)!
.withOpacity(0.3)
: const Color.fromARGB(255, 0, 0, 0)!,
iconColor: isDarkMode
? const Color.fromARGB(255, 0, 0, 0)!
: const Color.fromARGB(255, 255, 255, 255)!,
titleColor: isDarkMode ? Colors.white : null,
subtitleColor: isDarkMode ? Colors.white70 : null,
isDarkMode: isDarkMode,
showbutt: controller.isearphone.value,
onPressed: () {
controller.unbindearphone();
},
),
)
],
),
),
),
),
],
);
}
// 录音笔
Widget _buildVoiceRecorderCard(bool isDarkMode) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(left: 4.w, bottom: 8.h),
child: Text(
'voicerecorder'.tr, //AI设置
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.blue[400] : Colors.blue[600],
letterSpacing: 1,
),
),
),
Container(
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16.r),
child: BackdropFilter(
filter: isDarkMode
? ImageFilter.blur(sigmaX: 5, sigmaY: 5)
: ImageFilter.blur(sigmaX: 0, sigmaY: 0),
child: Column(
children: [
// 实时对话入口
Obx(
() => _buildSimpleNavigationSetting(
title: '未绑定',
subtitle: '还未绑定设备',
icon: Icons.edit,
iconBgColor: isDarkMode
? const Color.fromARGB(255, 255, 255, 255)!
.withOpacity(0.3)
: const Color.fromARGB(255, 0, 0, 0)!,
iconColor: isDarkMode
? const Color.fromARGB(255, 0, 0, 0)!
: const Color.fromARGB(255, 255, 255, 255)!,
titleColor: isDarkMode ? Colors.white : null,
subtitleColor: isDarkMode ? Colors.white70 : null,
isDarkMode: isDarkMode,
showbutt: controller.isvoicepen.value,
onPressed: () {},
),
)
],
),
),
),
),
],
);
}
// 简单导航设置项
Widget _buildSimpleNavigationSetting({
required String title,
String? subtitle,
required IconData icon,
required Color iconBgColor,
required Color iconColor,
Color? titleColor,
Color? subtitleColor,
required bool isDarkMode,
required bool showbutt,
required VoidCallback onPressed,
}) {
return InkWell(
child: Padding(
padding: EdgeInsets.all(16.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
children: [
Container(
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: iconBgColor,
borderRadius: BorderRadius.circular(16.r),
),
child: Icon(
icon,
size: 16.w,
color: iconColor,
),
),
SizedBox(width: 12.w),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: titleColor ??
(isDarkMode ? Colors.white : Colors.black87),
),
overflow: TextOverflow.ellipsis,
),
if (subtitle != null) SizedBox(height: 2.h),
if (subtitle != null)
Text(
subtitle,
style: TextStyle(
fontSize: 12.sp,
color: subtitleColor ??
(isDarkMode
? Colors.white70
: Colors.black54),
),
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
if (showbutt)
Container(
width: 74.w,
height: 40.w,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: EdgeInsets.symmetric(horizontal: 2, vertical: 2),
),
onPressed: onPressed,
child: const Text(
'解绑',
style: TextStyle(
color: Colors.white,
),
),
)),
],
),
),
);
}
}

15
lib/modules/music/bindings/music_binding.dart

@ -1,15 +0,0 @@
import 'package:get/get.dart';
import '../controllers/music_controller.dart';
/// 音乐模块绑定
/// 负责注册音乐相关的控制器和服务
class MusicBinding extends Bindings {
@override
void dependencies() {
// 注册音乐控制器为单例
Get.lazyPut<MusicController>(
() => MusicController(),
fenix: true, // 允许重新创建
);
}
}

493
lib/modules/music/controllers/music_controller.dart

@ -1,493 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../core/utils/logger.dart';
import '../../../data/services/music_manager.dart';
import '../../../data/services/network/api.dart';
/// 音乐播放控制器
/// 负责管理音乐播放状态、播放列表和播放控制
class MusicController extends GetxController {
static const String TAG = 'MusicController';
// 音乐播放管理器
final _musicManager = Get.find<MusicManager>();
// 播放状态
final isPlaying = false.obs;
final currentId = 0.obs;
// 播放进度相关状态
final currentPosition = 0.obs; // 当前播放位置(毫秒)
final duration = 0.obs; // 总时长(毫秒)
final progress = 0.0.obs; // 播放进度(0.0-1.0)
// 当前播放列表管理
final currentPlaylist = <Map<String, dynamic>>[].obs; // 当前播放列表
final currentPlaylistIndex = 0.obs; // 当前播放歌曲在列表中的索引
// 加载状态跟踪
final isLoadingMusic = false.obs;
final loadingMusicId = ''.obs;
// 进度更新定时器
Timer? _progressTimer;
@override
void onInit() {
super.onInit();
_initMusicStatus();
_setupMediaSessionCallbacks();
}
@override
void onClose() {
_stopProgressTimer();
super.onClose();
}
// 初始化音乐播放状态
Future<void> _initMusicStatus() async {
try {
await musicStatus();
Logger.d(TAG, '音乐状态初始化完成');
} catch (e) {
Logger.e(TAG, '音乐状态初始化失败: $e');
}
}
// 设置媒体会话回调
void _setupMediaSessionCallbacks() async {
try {
await _musicManager.setMediaSessionCallbacks(
onPlay: onMediaSessionPlay,
onPause: onMediaSessionPause,
onNext: onMediaSessionNext,
onPrevious: onMediaSessionPrevious,
onSeekTo: onMediaSessionSeekTo,
onStop: onMediaSessionStop,
);
Logger.d(TAG, '媒体会话回调设置完成');
} catch (e) {
Logger.e(TAG, '媒体会话回调设置失败: $e');
}
}
// 设置加载状态的方法
void setLoadingMusic(String id, bool loading) {
loadingMusicId.value = loading ? id : '';
isLoadingMusic.value = loading;
}
// 获取音乐播放状态
Future<void> musicStatus() async {
try {
var status = await _musicManager.getStatus();
isPlaying.value = status['isPlaying'] ?? false;
currentId.value = status['id'] ?? 0;
Logger.d(TAG, '音乐状态更新: playing=${isPlaying.value}, id=${currentId.value}');
} catch (e) {
Logger.e(TAG, '获取音乐状态失败: $e');
}
}
// 播放音乐
Future<void> musicPlay(Map<String, dynamic> song) async {
try {
Logger.d(TAG, '开始播放音乐: ${song['title'] ?? song['name']}');
// 立即更新UI状态,提供即时反馈
isPlaying.value = true;
// 修复:确保currentId接收int类型
final songId = song['id'];
currentId.value = songId is int
? songId
: (int.tryParse(songId?.toString() ?? '0') ?? 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(TAG, '音乐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接收int类型
final newSongId = song['id'];
currentId.value = newSongId is int
? newSongId
: (int.tryParse(newSongId?.toString() ?? '0') ?? 0);
Logger.d(TAG, '获取新URL成功');
} catch (e) {
Logger.e(TAG, '获取新URL失败: $e');
// 如果获取新URL失败,尝试使用原URL
}
}
}
// 转换为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 _musicManager.play(musicData);
// 更新媒体会话元数据和播放状态
await _updateMediaSessionMetadata(song);
await _updateMediaSessionPlaybackState(true);
// 启动进度定时器
_startProgressTimer();
Logger.d(TAG, '音乐播放启动成功');
} catch (e) {
Logger.e(TAG, '播放音乐失败: $e');
// 发生错误时重置状态
isPlaying.value = false;
currentId.value = 0;
}
}
// 暂停音乐
Future<void> musicPause() async {
try {
Logger.d(TAG, '暂停音乐播放');
await _musicManager.pause();
isPlaying.value = false;
_stopProgressTimer();
// 更新媒体会话播放状态
await _updateMediaSessionPlaybackState(false);
} catch (e) {
Logger.e(TAG, '暂停音乐失败: $e');
}
}
// 恢复音乐播放
Future<void> musicResume() async {
try {
Logger.d(TAG, '恢复播放音乐');
await _musicManager.resume();
isPlaying.value = true;
_startProgressTimer();
// 更新媒体会话播放状态
await _updateMediaSessionPlaybackState(true);
Logger.d(TAG, '音乐恢复播放成功');
} catch (e) {
Logger.e(TAG, '恢复播放音乐失败: $e');
isPlaying.value = false;
}
}
// 启动进度更新定时器
void _startProgressTimer() {
_stopProgressTimer(); // 先停止现有定时器
Logger.d(TAG, '启动播放进度定时器');
_progressTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) {
if (isPlaying.value && currentId.value != 0) {
updateMusicProgress();
} else if (!isPlaying.value) {
// 如果不在播放状态,停止定时器
timer.cancel();
_progressTimer = null;
Logger.d(TAG, '播放已停止,取消进度定时器');
}
});
}
// 停止进度更新定时器
void _stopProgressTimer() {
if (_progressTimer != null) {
Logger.d(TAG, '停止播放进度定时器');
_progressTimer?.cancel();
_progressTimer = null;
}
}
// 获取播放进度
Future<void> updateMusicProgress() async {
try {
if (!isPlaying.value || currentId.value == 0) {
return; // 如果没有在播放,直接返回
}
var progressInfo = await _musicManager.getProgress();
final newPosition = progressInfo['currentPosition'] ?? 0;
final newDuration = progressInfo['duration'] ?? 0;
final newProgress = progressInfo['progress'] ?? 0.0;
bool hasUpdate = false;
// 只有当值发生变化时才更新,避免不必要的UI刷新
if (currentPosition.value != newPosition) {
currentPosition.value = newPosition;
hasUpdate = true;
}
if (duration.value != newDuration) {
duration.value = newDuration;
hasUpdate = true;
}
if ((progress.value - newProgress).abs() > 0.001) {
// 避免浮点数精度问题
progress.value = newProgress;
hasUpdate = true;
}
// 如果有进度更新,通知系统媒体会话更新进度信息
if (hasUpdate) {
await _updateMediaSessionProgress();
}
} catch (e) {
Logger.e(TAG, '获取播放进度失败: $e');
}
}
// 跳转到指定位置
Future<void> seekToPosition(int positionMs) async {
try {
await _musicManager.seekTo(positionMs);
// 立即更新进度状态
await updateMusicProgress();
} catch (e) {
Logger.e(TAG, '跳转播放位置失败: $e');
}
}
/// 播放音乐列表
Future<void> playPlaylist(List<Map<String, dynamic>> playlist) async {
try {
if (playlist.isEmpty) {
Logger.e(TAG, '播放列表为空,无法播放');
return;
}
Logger.d(TAG, '开始播放音乐列表,共${playlist.length}首歌曲');
// 保存当前播放列表
currentPlaylist.assignAll(playlist);
currentPlaylistIndex.value = 0;
// 设置加载状态
isLoadingMusic.value = true;
loadingMusicId.value = playlist[0]['id']?.toString() ?? '';
// 重置播放状态
isPlaying.value = true;
currentPosition.value = 0;
duration.value = 0;
progress.value = 0.0;
// 格式化歌曲列表数据
final formattedPlaylist = playlist.map((song) {
return {
'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() ?? '',
};
}).toList();
// 调用音乐管理器播放列表
await _musicManager.playPlaylist(formattedPlaylist);
// 更新当前播放歌曲ID
if (formattedPlaylist.isNotEmpty) {
currentId.value = int.tryParse(formattedPlaylist[0]['id'] ?? '0') ?? 0;
}
// 启动进度定时器
_startProgressTimer();
Logger.d(TAG, '音乐列表播放启动成功');
} catch (e) {
Logger.e(TAG, '播放音乐列表失败: $e');
isPlaying.value = false;
currentId.value = 0;
// 显示错误提示
Get.snackbar(
'播放失败',
'无法播放音乐列表,请稍后重试',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red[400],
colorText: Colors.white,
duration: const Duration(seconds: 2),
);
} finally {
// 清除加载状态
isLoadingMusic.value = false;
loadingMusicId.value = '';
}
}
/// 播放上一曲
Future<void> playPrevious() async {
if (currentPlaylist.isEmpty) {
Logger.w(TAG, '当前播放列表为空,无法播放上一曲');
return;
}
// 计算上一曲的索引
int previousIndex = currentPlaylistIndex.value - 1;
if (previousIndex < 0) {
previousIndex = currentPlaylist.length - 1; // 循环到最后一首
}
currentPlaylistIndex.value = previousIndex;
final previousSong = currentPlaylist[previousIndex];
Logger.d(TAG, '播放上一曲: ${previousSong['name'] ?? previousSong['title']}');
await musicPlay(previousSong);
}
/// 播放下一曲
Future<void> playNext() async {
if (currentPlaylist.isEmpty) {
Logger.w(TAG, '当前播放列表为空,无法播放下一曲');
return;
}
// 计算下一曲的索引
int nextIndex = currentPlaylistIndex.value + 1;
if (nextIndex >= currentPlaylist.length) {
nextIndex = 0; // 循环到第一首
}
currentPlaylistIndex.value = nextIndex;
final nextSong = currentPlaylist[nextIndex];
Logger.d(TAG, '播放下一曲: ${nextSong['name'] ?? nextSong['title']}');
await musicPlay(nextSong);
}
/// 更新当前播放列表和索引(当单独播放某首歌曲时调用)
void updateCurrentPlaylistContext(
List<Map<String, dynamic>> playlist, int songId) {
currentPlaylist.assignAll(playlist);
// 查找当前歌曲在列表中的索引
int index = -1;
for (int i = 0; i < playlist.length; i++) {
if (int.tryParse(playlist[i]['id']?.toString() ?? '0') == songId) {
index = i;
break;
}
}
if (index != -1) {
currentPlaylistIndex.value = index;
Logger.d(TAG, '更新播放列表上下文,当前歌曲索引: $index');
}
}
// ========== 媒体会话回调处理方法 ==========
/// 处理来自系统通知栏的播放操作
Future<void> onMediaSessionPlay() async {
Logger.d(TAG, '收到媒体会话播放回调');
if (!isPlaying.value) {
await musicResume();
}
}
/// 处理来自系统通知栏的暂停操作
Future<void> onMediaSessionPause() async {
Logger.d(TAG, '收到媒体会话暂停回调');
if (isPlaying.value) {
await musicPause();
}
}
/// 处理来自系统通知栏的下一曲操作
Future<void> onMediaSessionNext() async {
Logger.d(TAG, '收到媒体会话下一曲回调');
await playNext();
}
/// 处理来自系统通知栏的上一曲操作
Future<void> onMediaSessionPrevious() async {
Logger.d(TAG, '收到媒体会话上一曲回调');
await playPrevious();
}
/// 处理来自系统通知栏的跳转操作
Future<void> onMediaSessionSeekTo(int positionMs) async {
Logger.d(TAG, '收到媒体会话跳转回调: ${positionMs}ms');
await seekToPosition(positionMs);
}
/// 处理来自系统通知栏的停止操作
Future<void> onMediaSessionStop() async {
Logger.d(TAG, '收到媒体会话停止回调');
await musicPause();
// 重置播放状态
currentPosition.value = 0;
progress.value = 0.0;
_stopProgressTimer();
}
// ========== 私有辅助方法 ==========
/// 更新系统媒体会话的进度信息
Future<void> _updateMediaSessionProgress() async {
try {
// 具体实现将在music_service插件中添加
await _musicManager.updateMediaSessionProgress(
currentPosition.value,
duration.value,
progress.value,
);
} catch (e) {
// 忽略错误,避免影响正常播放
Logger.d(TAG, '更新媒体会话进度失败: $e');
}
}
/// 更新系统媒体会话的元数据
Future<void> _updateMediaSessionMetadata(Map<String, dynamic> song) async {
try {
await _musicManager.updateMediaSessionMetadata(song);
} catch (e) {
Logger.d(TAG, '更新媒体会话元数据失败: $e');
}
}
/// 更新系统媒体会话的播放状态
Future<void> _updateMediaSessionPlaybackState(bool isPlaying) async {
try {
await _musicManager.updateMediaSessionPlaybackState(isPlaying);
} catch (e) {
Logger.d(TAG, '更新媒体会话播放状态失败: $e');
}
}
}

125
lib/modules/music/services/music_service_facade.dart

@ -1,125 +0,0 @@
import 'package:get/get.dart';
import '../controllers/music_controller.dart';
import '../../../data/services/music_manager.dart';
/// 音乐服务门面
/// 作为音乐功能的统一入口,简化模块间的交互
class MusicServiceFacade {
// 单例实现
static final MusicServiceFacade _instance = MusicServiceFacade._internal();
factory MusicServiceFacade() => _instance;
MusicServiceFacade._internal();
// 获取音乐控制器实例
MusicController get musicController => Get.find<MusicController>();
// 获取音乐管理器实例
MusicManager get musicManager => Get.find<MusicManager>();
/// 播放音乐
Future<void> musicPlay(Map<String, dynamic> song) async {
await musicController.musicPlay(song);
}
/// 暂停音乐
Future<void> musicPause() async {
await musicController.musicPause();
}
/// 恢复播放
Future<void> musicResume() async {
await musicController.musicResume();
}
/// 播放音乐(兼容旧接口)
Future<void> playMusic(Map<String, dynamic> song) async {
await musicPlay(song);
}
/// 暂停音乐(兼容旧接口)
Future<void> pauseMusic() async {
await musicPause();
}
/// 恢复播放(兼容旧接口)
Future<void> resumeMusic() async {
await musicResume();
}
/// 播放/暂停切换
Future<void> togglePlayPause() async {
if (musicController.isPlaying.value) {
await musicPause();
} else {
await musicResume();
}
}
/// 播放上一曲
Future<void> playPrevious() async {
await musicController.playPrevious();
}
/// 播放下一曲
Future<void> playNext() async {
await musicController.playNext();
}
/// 播放音乐列表
Future<void> playPlaylist(List<Map<String, dynamic>> playlist) async {
await musicController.playPlaylist(playlist);
}
/// 跳转到指定位置
Future<void> seekToPosition(int positionMs) async {
await musicController.seekToPosition(positionMs);
}
/// 获取当前播放状态
bool get isPlayingValue => musicController.isPlaying.value;
/// 获取当前播放歌曲ID
int get currentIdValue => musicController.currentId.value;
/// 获取当前播放位置(毫秒)
int get currentPositionValue => musicController.currentPosition.value;
/// 获取当前歌曲总时长(毫秒)
int get durationValue => musicController.duration.value;
/// 获取当前播放进度(0.0-1.0)
double get progressValue => musicController.progress.value;
/// 获取响应式播放状态(用于Obx)
get isPlaying => musicController.isPlaying;
/// 获取响应式当前播放歌曲ID(用于Obx)
get currentId => musicController.currentId;
/// 获取响应式当前播放位置(用于Obx)
get currentPosition => musicController.currentPosition;
/// 获取响应式当前歌曲总时长(用于Obx)
get duration => musicController.duration;
/// 获取响应式当前播放进度(用于Obx)
get progress => musicController.progress;
/// 获取当前播放列表
List<Map<String, dynamic>> get currentPlaylist => musicController.currentPlaylist;
/// 获取当前播放索引
int get currentPlaylistIndex => musicController.currentPlaylistIndex.value;
/// 设置加载状态
void setLoadingMusic(String id, bool loading) {
musicController.setLoadingMusic(id, loading);
}
/// 更新当前播放列表上下文
void updateCurrentPlaylistContext(List<Map<String, dynamic>> playlist, int songId) {
musicController.updateCurrentPlaylistContext(playlist, songId);
}
}

108
lib/modules/music/views/music_playlist_view.dart

@ -3,8 +3,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter/services.dart';
import '../services/music_service_facade.dart';
import '../../../core/utils/logger.dart';
import '../../agent/controllers/agent_controller.dart';
import '../../agent/views/circular_progress_button.dart';
class MusicPlaylistView extends StatelessWidget {
@ -13,7 +13,7 @@ class MusicPlaylistView extends StatelessWidget {
@override
Widget build(BuildContext context) {
ScreenUtil.init(context, designSize: const Size(375, 812)); // 添加设计尺寸参考
final musicFacade = Get.find<MusicServiceFacade>();
final controller = Get.find<AgentController>(); // 获取控制器
final List<Map<String, dynamic>> playlist = Get.arguments ?? [];
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
@ -55,14 +55,14 @@ class MusicPlaylistView extends StatelessWidget {
Container(
height: MediaQuery.of(context).size.height * 0.38, // 增大高度比例
child: _buildCurrentPlayingSection(
musicFacade, playlist, isDarkMode),
controller, playlist, isDarkMode),
),
// 中部:播放全部按钮
_buildPlayAllSection(musicFacade, playlist, isDarkMode),
_buildPlayAllSection(controller, playlist, isDarkMode),
// 下部:歌单列表 - 使用剩余空间
Expanded(
child:
_buildPlaylistSection(musicFacade, playlist, isDarkMode),
_buildPlaylistSection(controller, playlist, isDarkMode),
),
],
),
@ -70,19 +70,19 @@ class MusicPlaylistView extends StatelessWidget {
}
// 构建当前播放音乐详情部分
Widget _buildCurrentPlayingSection(MusicServiceFacade musicFacade,
Widget _buildCurrentPlayingSection(AgentController controller,
List<Map<String, dynamic>> playlist, bool isDarkMode) {
return SingleChildScrollView(
// 添加滚动支持
child: Obx(() {
// 查找当前播放的歌曲
Map<String, dynamic>? currentSong;
if (musicFacade.currentId.value != 0) {
if (controller.currentId.value != 0) {
try {
currentSong = playlist.firstWhere(
(song) =>
int.tryParse(song['id']?.toString() ?? '0') ==
musicFacade.currentId.value,
controller.currentId.value,
);
} catch (e) {
// 如果没找到,使用第一首歌曲
@ -137,10 +137,10 @@ class MusicPlaylistView extends StatelessWidget {
_buildSongInfo(title, artist, isDarkMode),
SizedBox(height: 16.h),
// 进度条
_buildProgressBar(musicFacade, isDarkMode),
_buildProgressBar(controller, isDarkMode),
SizedBox(height: 15.h),
// 播放控制按钮
_buildPlayControls(musicFacade, playlist, isDarkMode),
_buildPlayControls(controller, playlist, isDarkMode),
],
),
);
@ -149,7 +149,7 @@ class MusicPlaylistView extends StatelessWidget {
}
// 构建播放全部按钮部分
Widget _buildPlayAllSection(MusicServiceFacade musicFacade,
Widget _buildPlayAllSection(AgentController controller,
List<Map<String, dynamic>> playlist, bool isDarkMode) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 12.h),
@ -159,7 +159,7 @@ class MusicPlaylistView extends StatelessWidget {
child: ElevatedButton.icon(
onPressed: () async {
if (playlist.isNotEmpty) {
await musicFacade.playPlaylist(playlist);
await controller.playPlaylist(playlist);
}
},
icon: Icon(Icons.play_arrow, color: Colors.white, size: 20.w),
@ -185,7 +185,7 @@ class MusicPlaylistView extends StatelessWidget {
}
// 构建歌单列表部分
Widget _buildPlaylistSection(MusicServiceFacade musicFacade,
Widget _buildPlaylistSection(AgentController controller,
List<Map<String, dynamic>> playlist, bool isDarkMode) {
return ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 8.h),
@ -200,7 +200,7 @@ class MusicPlaylistView extends StatelessWidget {
return _buildMusicItem(
context,
musicFacade,
controller,
id,
name,
url,
@ -214,7 +214,7 @@ class MusicPlaylistView extends StatelessWidget {
Widget _buildMusicItem(
BuildContext context,
MusicServiceFacade musicFacade,
AgentController controller,
String id,
String name,
String url,
@ -232,7 +232,7 @@ class MusicPlaylistView extends StatelessWidget {
return GestureDetector(
onTap: () {
_playSelectedSong(musicFacade, song, Get.arguments ?? []);
_playSelectedSong(controller, song, Get.arguments ?? []);
},
child: Container(
margin: EdgeInsets.only(bottom: 10.h),
@ -324,10 +324,10 @@ class MusicPlaylistView extends StatelessWidget {
// 播放按钮
Obx(() {
final currentIdInt = int.tryParse(id) ?? 0;
final isThisPlaying = musicFacade.isPlaying.value &&
musicFacade.currentId.value == currentIdInt;
final isThisPlaying = controller.isPlaying.value &&
controller.currentId.value == currentIdInt;
final currentProgress =
isThisPlaying ? musicFacade.progress.value : 0.0;
isThisPlaying ? controller.progress.value : 0.0;
return CircularProgressButton(
isPlaying: isThisPlaying,
@ -339,20 +339,20 @@ class MusicPlaylistView extends StatelessWidget {
onPlayPause: () async {
try {
Logger.d('MusicPlaylistView',
'播放按钮被点击,当前播放状态: ${musicFacade.isPlaying.value}');
'播放按钮被点击,当前播放状态: ${controller.isPlaying.value}');
Logger.d('MusicPlaylistView',
'当前歌曲ID: ${musicFacade.currentId.value}, 点击歌曲ID: $currentIdInt');
if (musicFacade.isPlaying.value) {
if (musicFacade.currentId.value == currentIdInt) {
'当前歌曲ID: ${controller.currentId.value}, 点击歌曲ID: $currentIdInt');
if (controller.isPlaying.value) {
if (controller.currentId.value == currentIdInt) {
Logger.d('MusicPlaylistView', '暂停当前播放的歌曲');
await musicFacade.musicPause();
await controller.musicPause();
} else {
Logger.d('MusicPlaylistView', '播放新歌曲');
await musicFacade.musicPlay(song);
await controller.musicPlay(song);
}
} else {
Logger.d('MusicPlaylistView', '开始播放歌曲');
await musicFacade.musicPlay(song);
await controller.musicPlay(song);
}
} catch (e) {
Logger.e('MusicPlaylistView', '播放音乐时发生错误: $e');
@ -362,8 +362,8 @@ class MusicPlaylistView extends StatelessWidget {
? (seekProgress) {
// 只有当前播放的歌曲才允许跳转
final targetPosition =
(musicFacade.duration.value * seekProgress).round();
musicFacade.seekToPosition(targetPosition);
(controller.duration.value * seekProgress).round();
controller.seekToPosition(targetPosition);
}
: null,
);
@ -449,14 +449,14 @@ class MusicPlaylistView extends StatelessWidget {
}
// 构建进度条
Widget _buildProgressBar(MusicServiceFacade musicFacade, bool isDarkMode) {
Widget _buildProgressBar(AgentController controller, bool isDarkMode) {
return Obx(() {
return Row(
children: [
SizedBox(
width: 40.w,
child: Text(
_formatDuration(musicFacade.currentPosition.value),
_formatDuration(controller.currentPosition.value),
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
@ -480,11 +480,11 @@ class MusicPlaylistView extends StatelessWidget {
overlayShape: const RoundSliderOverlayShape(overlayRadius: 14),
),
child: Slider(
value: musicFacade.progress.value.clamp(0.0, 1.0),
value: controller.progress.value.clamp(0.0, 1.0),
onChanged: (value) {
final targetPosition =
(musicFacade.duration.value * value).round();
musicFacade.seekToPosition(targetPosition);
(controller.duration.value * value).round();
controller.seekToPosition(targetPosition);
},
),
),
@ -493,7 +493,7 @@ class MusicPlaylistView extends StatelessWidget {
SizedBox(
width: 40.w,
child: Text(
_formatDuration(musicFacade.duration.value),
_formatDuration(controller.duration.value),
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
@ -508,14 +508,14 @@ class MusicPlaylistView extends StatelessWidget {
}
// 构建播放控制按钮
Widget _buildPlayControls(MusicServiceFacade musicFacade,
Widget _buildPlayControls(AgentController controller,
List<Map<String, dynamic>> playlist, bool isDarkMode) {
return Obx(() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: () => _playPrevious(musicFacade, playlist),
onPressed: () => _playPrevious(controller, playlist),
iconSize: 32.w,
padding: EdgeInsets.all(8.w), // 增大触摸区域
icon: Icon(
@ -540,10 +540,10 @@ class MusicPlaylistView extends StatelessWidget {
],
),
child: IconButton(
onPressed: () => _togglePlayPause(musicFacade),
onPressed: () => _togglePlayPause(controller),
padding: EdgeInsets.zero,
icon: Icon(
musicFacade.isPlaying.value ? Icons.pause : Icons.play_arrow,
controller.isPlaying.value ? Icons.pause : Icons.play_arrow,
size: 28.w,
color: Colors.white,
),
@ -551,7 +551,7 @@ class MusicPlaylistView extends StatelessWidget {
),
SizedBox(width: 16.w),
IconButton(
onPressed: () => _playNext(musicFacade, playlist),
onPressed: () => _playNext(controller, playlist),
iconSize: 32.w,
padding: EdgeInsets.all(8.w), // 增大触摸区域
icon: Icon(
@ -574,35 +574,35 @@ class MusicPlaylistView extends StatelessWidget {
}
// 播放/暂停切换
Future<void> _togglePlayPause(MusicServiceFacade musicFacade) async {
if (musicFacade.isPlaying.value) {
await musicFacade.musicPause();
Future<void> _togglePlayPause(AgentController controller) async {
if (controller.isPlaying.value) {
await controller.musicPause();
} else {
await musicFacade.musicResume();
await controller.musicResume();
}
}
// 播放上一曲
void _playPrevious(
MusicServiceFacade musicFacade, List<Map<String, dynamic>> playlist) {
musicFacade.updateCurrentPlaylistContext(
playlist, musicFacade.currentId.value);
musicFacade.playPrevious();
AgentController controller, List<Map<String, dynamic>> playlist) {
controller.updateCurrentPlaylistContext(
playlist, controller.currentId.value);
controller.playPrevious();
}
// 播放下一曲
void _playNext(
MusicServiceFacade musicFacade, List<Map<String, dynamic>> playlist) {
musicFacade.updateCurrentPlaylistContext(
playlist, musicFacade.currentId.value);
musicFacade.playNext();
AgentController controller, List<Map<String, dynamic>> playlist) {
controller.updateCurrentPlaylistContext(
playlist, controller.currentId.value);
controller.playNext();
}
// 播放选中的歌曲
void _playSelectedSong(MusicServiceFacade musicFacade, Map<String, dynamic> song,
void _playSelectedSong(AgentController controller, Map<String, dynamic> song,
List<Map<String, dynamic>> playlist) {
final songId = int.tryParse(song['id']?.toString() ?? '0') ?? 0;
musicFacade.updateCurrentPlaylistContext(playlist, songId);
musicFacade.musicPlay(song);
controller.updateCurrentPlaylistContext(playlist, songId);
controller.musicPlay(song);
}
}

149
lib/modules/pairing/controllers/pairing_controller.dart

@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:typed_data';
import 'package:app_settings/app_settings.dart';
import 'package:deep_voice/data/models/appconfig.dart';
import 'package:deep_voice/data/models/user_Info.dart';
import 'package:deep_voice/data/services/network/api.dart';
import 'package:flutter/material.dart';
@ -21,6 +22,7 @@ enum PairingState {
checking, // 检查中(蓝牙、权限等)
waitingClassic, // 等待经典蓝牙连接
verifying, // 验证设备
binding, //确认绑定设备
manualbinding, //手动绑定设备
associating, // 正在进行BLE设备关联
waitingBle, //等待BLE连接
@ -61,6 +63,8 @@ class PairingController extends GetxController {
final isLoading = false.obs;
final showConnectButton = false.obs;
final showRetryButton = false.obs;
final showBindButton = false.obs;
final showRetryChectButton = false.obs;
final showAutoCodeInput = false.obs;
final showSkipButton = false.obs;
// 增加临时跳过绑定按钮状态
@ -68,6 +72,7 @@ class PairingController extends GetxController {
final connectButtonText = '连接设备'.obs;
final retryButtonText = '重试'.obs;
final deviceCode = ''.obs;
// ========== 监听器管理 ==========
final List<Worker> _workers = [];
@ -251,7 +256,8 @@ class PairingController extends GetxController {
showSkipBindingButton.value = false; // 重置临时跳过绑定按钮
showSkipButton.value = false;
showAutoCodeInput.value = false;
showBindButton.value = false;
showRetryChectButton.value = false;
switch (state) {
case PairingState.idle:
statusText.value = '准备开始配对';
@ -266,6 +272,7 @@ class PairingController extends GetxController {
statusText.value = '请确保耳机已开启并进入配对模式';
showConnectButton.value = true;
showSkipButton.value = true;
connectButtonText.value = '前往蓝牙设置';
break;
@ -273,7 +280,12 @@ class PairingController extends GetxController {
statusText.value = '正在验证设备...';
isLoading.value = true;
break;
case PairingState.binding:
statusText.value = '确认绑定';
isLoading.value = true;
showBindButton.value = true;
showRetryChectButton.value = true;
break;
case PairingState.associating:
statusText.value = '正在关联设备...';
isLoading.value = true;
@ -418,39 +430,27 @@ class PairingController extends GetxController {
Future<void> _verifyDevice(BluetoothDevice device) async {
try {
Logger.i(_tag, '开始验证设备: ${device.name} (${device.address})');
// TODO: 实现API验证逻辑
// 暂时模拟验证通过
// await Future.delayed(const Duration(seconds: 2));;
if (await authbindingDevice(device)) {
Logger.i(_tag, '设备验证通过');
_verifiedDeviceAddress = device.address;
// 检查是否已有BLE关联
final pairedMacAddress = await _bleManager.getPairedMacAddress();
Logger.i(_tag, '已关联的MAC地址: $pairedMacAddress');
if (pairedMacAddress != null &&
pairedMacAddress.toUpperCase() == device.address.toUpperCase()) {
// 已有关联,检查BLE连接状态
Logger.i(_tag, '设备已关联,检查BLE连接状态');
final currentBleState = await _bleManager.getConnectionState();
Logger.i(_tag, '验证时BLE状态: $currentBleState');
if (currentBleState == 2) {
// BleConnectionState.connected
Logger.i(_tag, 'BLE已连接,直接完成配对');
_transition(PairingState.completed);
} else {
Logger.i(_tag, 'BLE未连接,等待BLE连接');
_transition(PairingState.waitingBle);
}
final result = await authbindingDevice(device);
if (result == 2) {
//验证失败 进入手动授权
await connectble(device);
} else if (result == 1) {
//跳转绑定界面
//验证失败 进入手动授权
if (currentDevice != null) {
_transition(PairingState.binding);
} else {
// 没有关联或关联的是其他设备,需要进行关联
Logger.i(_tag, '需要进行BLE设备关联');
_transition(PairingState.associating);
_transition(PairingState.checking);
_checkCurrentStatus();
}
} else {
//验证失败 进入手动授权
_transition(PairingState.manualbinding);
final authcode_open = AppConfig.env("APP_AUTHCODE_OPEN") ?? "";
if (authcode_open == "true") {
//验证失败 进入手动授权
_transition(PairingState.manualbinding);
} else {
_setError(ErrorType.deviceNotMatch, '请连接 DeepSound 设备');
}
}
} catch (e) {
Logger.e(_tag, '设备验证异常: $e');
@ -458,6 +458,37 @@ class PairingController extends GetxController {
}
}
//连接ble
Future<void> connectble(BluetoothDevice device) async {
Logger.i(_tag, '设备验证通过');
_verifiedDeviceAddress = device.address;
// 检查是否已有BLE关联
final pairedMacAddress = await _bleManager.getPairedMacAddress();
Logger.i(_tag, '已关联的MAC地址: $pairedMacAddress');
if (pairedMacAddress != null &&
pairedMacAddress.toUpperCase() == device.address.toUpperCase()) {
// 已有关联,检查BLE连接状态
Logger.i(_tag, '设备已关联,检查BLE连接状态');
final currentBleState = await _bleManager.getConnectionState();
Logger.i(_tag, '验证时BLE状态: $currentBleState');
if (currentBleState == 2) {
// BleConnectionState.connected
Logger.i(_tag, 'BLE已连接,直接完成配对');
_transition(PairingState.completed);
} else {
Logger.i(_tag, 'BLE未连接,等待BLE连接');
_transition(PairingState.waitingBle);
}
} else {
// 没有关联或关联的是其他设备,需要进行关联
Logger.i(_tag, '需要进行BLE设备关联');
_transition(PairingState.associating);
}
return;
}
/// 开始设备关联
void _startDeviceAssociation() async {
try {
@ -524,18 +555,19 @@ class PairingController extends GetxController {
}
}
//自动绑定设备
Future<bool> authbindingDevice(BluetoothDevice device) async {
//自动绑定设备 0 未合作商 1 合作商 2,已绑定
Future<int> authbindingDevice(BluetoothDevice device) async {
if (User.instance.earphonemac == device.address) {
// 已经绑定
return true;
return 2;
}
// 检查设备名称是否包含 DeepSound 或 DeapSound
final deviceName = device.name ?? '';
final deviceName = device.name;
print('获取到 deviceName: $deviceName');
if (deviceName.contains('DeepSound') || deviceName.contains('DeapSound')) {
await _bleManager.startScan();
await Future.delayed(const Duration(seconds: 1));
// 尝试最多读取 3 次设备信息,每次间隔 1 秒
Map<String, String>? deviceinfo;
for (int i = 0; i < 3; i++) {
@ -553,27 +585,38 @@ class PairingController extends GetxController {
print('获取到 code: $code');
// 安全检查长度
if (code != "") {
final response = await Api.bindearphone({
"code": code,
"earphonemac": device.address,
});
Logger.i(_tag, '手动授权结果 ${response}');
if (response != null) {
Logger.i(_tag, '设备自动绑定成功');
User.instance.earphonemac = device.address ?? '';
return true;
} else {
Logger.e(_tag, '手动绑定失败!');
return false;
}
deviceCode.value = code;
return 1;
} else {
return false;
return 0;
}
}
return false;
return 0;
// 调用BLE管理器的自动绑定方法
} else {
return false;
return 0;
}
}
//确认绑定设备
void confirmBindDevice() async {
final response = await Api.bindearphone({
"code": deviceCode.value,
"earphonname": currentDevice?.name,
"earphonemac": currentDevice?.address,
});
Logger.i(_tag, '手动授权结果 ${response}');
if (response != null) {
Logger.i(_tag, '绑定设备成功');
User.instance.earphonemac = currentDevice?.address ?? '';
User.instance.earphonename = currentDevice?.name ?? '';
User.instance.earphoneauthcode = deviceCode.value;
_transition(PairingState.verifying);
_verifyDevice(_currentClassicDevice!);
return;
} else {
Logger.e(_tag, '绑定设备失败!');
return;
}
}
@ -605,6 +648,8 @@ class PairingController extends GetxController {
Logger.i(_tag, '手动授权结果 ${response}');
if (response != null) {
User.instance.earphonemac = currentDevice?.address ?? '';
User.instance.earphonename = currentDevice?.name ?? '';
User.instance.earphoneauthcode = codeInputController.text.trim();
_transition(PairingState.verifying);
_verifyDevice(_currentClassicDevice!);
} else {

47
lib/modules/pairing/views/pairing_view.dart

@ -290,6 +290,7 @@ class PairingView extends GetView<PairingController> {
// 当前连接设备(有设备时才显示)
if (device != null &&
(state == PairingState.verifying ||
state == PairingState.binding ||
state == PairingState.waitingBle ||
state == PairingState.completed)) ...[
SizedBox(height: 24.h),
@ -311,6 +312,8 @@ class PairingView extends GetView<PairingController> {
return '请先通过系统蓝牙连接DeapSound设备';
case PairingState.verifying:
return '正在验证设备...';
case PairingState.binding:
return '设备绑定确认...';
case PairingState.associating:
return '正在建立设备关联...';
case PairingState.waitingBle:
@ -434,6 +437,8 @@ class PairingView extends GetView<PairingController> {
return Obx(() {
final showConnectButton = controller.showConnectButton.value;
final showRetryButton = controller.showRetryButton.value;
final showBindButton = controller.showBindButton.value;
final showRetryChectButton = controller.showRetryChectButton.value;
final showSkipButton = controller.showSkipButton.value;
final showAutoCodeInput = controller.showAutoCodeInput.value;
final showSkipBindingButton = controller.showSkipBindingButton.value;
@ -470,7 +475,28 @@ class PairingView extends GetView<PairingController> {
],
),
),
if (showBindButton)
ElevatedButton(
onPressed: () => controller.confirmBindDevice(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
elevation: 0,
minimumSize: Size(double.infinity, 56.h),
),
child: Text(
"绑定",
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
),
),
SizedBox(height: 12.h),
// 显示重试按钮
if (showRetryButton)
ElevatedButton(
@ -578,6 +604,25 @@ class PairingView extends GetView<PairingController> {
),
),
],
//重新检测
if (showRetryChectButton) ...[
SizedBox(height: 12.h),
TextButton(
onPressed: () => controller.onRetryTap(),
style: TextButton.styleFrom(
foregroundColor: Color(0xFF0066CC),
padding: EdgeInsets.symmetric(vertical: 12.h),
minimumSize: Size(double.infinity, 48.h),
),
child: Text(
'重新扫描'.tr, // 稍后再说
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
),
),
),
],
],
),
);

20
lib/modules/pairing/views/pairing_view_new.dart

@ -152,7 +152,10 @@ class PairingView extends GetView<PairingController> {
iconData = Icons.verified_user;
iconColor = Colors.orange;
break;
case PairingState.binding:
iconData = Icons.blind;
iconColor = Colors.blue;
break;
case PairingState.associating:
iconData = Icons.link;
iconColor = Colors.blue;
@ -195,6 +198,7 @@ class PairingView extends GetView<PairingController> {
/// 构建按钮
Widget _buildButtons(bool isDarkMode) {
final showConnectButton = controller.showConnectButton.value;
final showBindButton = controller.showBindButton.value;
final showRetryButton = controller.showRetryButton.value;
final showAutoCodeInput = controller.showAutoCodeInput.value;
final showSkipButton = controller.showSkipButton.value;
@ -209,6 +213,19 @@ class PairingView extends GetView<PairingController> {
isDarkMode,
),
// 绑定按钮
if (showBindButton)
TextButton(
onPressed: controller.confirmBindDevice,
child: Text(
'绑定',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
),
if (showRetryButton)
_buildPrimaryButton(
'重试',
@ -217,6 +234,7 @@ class PairingView extends GetView<PairingController> {
),
SizedBox(height: 12.h),
if (showAutoCodeInput)
// 自动输入代码按钮
SizedBox(

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

@ -505,8 +505,8 @@ class SettingsView extends GetView<SettingsController> {
// 清除配对设备
_buildSimpleNavigationSetting(
title: '清除配对设备',
subtitle: '清除所有BLE配对设备记录',
title: '设备管理',
subtitle: '设备管理列表',
icon: Icons.bluetooth_disabled,
iconBgColor: isDarkMode
? Colors.red[900]!.withOpacity(0.3)
@ -514,9 +514,7 @@ class SettingsView extends GetView<SettingsController> {
iconColor: isDarkMode ? Colors.red[300]! : Colors.red[600]!,
titleColor: isDarkMode ? Colors.white : null,
subtitleColor: isDarkMode ? Colors.white70 : null,
onTap: () {
controller.clearBleDevices();
},
onTap: () => Get.toNamed(Routes.devices),
isDarkMode: isDarkMode,
),
Divider(

7
lib/routes/app_pages.dart

@ -1,3 +1,5 @@
import 'package:deep_voice/modules/devices/bindings/devices_binding.dart';
import 'package:deep_voice/modules/devices/views/devices_view.dart';
import 'package:get/get.dart';
import '../modules/feedback/bindings/feedback_bindings.dart';
import '../modules/feedback/views/feedback_view.dart';
@ -242,5 +244,10 @@ abstract class AppPages {
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.devices,
page: () => const DevicesView(),
binding: DevicesBinding(),
),
];
}

3
lib/routes/app_routes.dart

@ -53,4 +53,7 @@ abstract class Routes {
// 实时语音对话
static const realtime = '/realtime';
// 设备管理
static const devices = '/devices';
}

2
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt

@ -231,6 +231,7 @@ object BleService {
return true
} catch (e: Exception) {
Log.e(TAG, "启动扫描失败: ${e.message}", e)
isScanning = false
return false
}
}
@ -255,6 +256,7 @@ object BleService {
return true
} catch (e: Exception) {
Log.e(TAG, "停止扫描失败: ${e.message}", e)
isScanning = false
return false
}
}

4
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt

@ -304,6 +304,10 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
Log.d(TAG, "getDeviceInfo $info")
result.success(info)
}
"disconnect" -> {
BleService.disconnect();
result.success(null)
}
else -> {
result.notImplemented()
}

146
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt

@ -1,5 +1,6 @@
package com.yunqiinnovation.music_service
import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
@ -13,7 +14,9 @@ import android.media.AudioManager
import android.media.MediaPlayer
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
@ -35,7 +38,7 @@ class MusicService : MediaBrowserServiceCompat() {
private const val DELETE_ACTION = "com.yunqiinnovation.music_service.DELETE_NOTIFICATION"
private val isRunning = AtomicBoolean(false)
private var currentIdInternal: Int? = null
private var currentIdInternal: String? = null
private var currentStatus = false
private var playlist = mutableListOf<Map<String,Any>>()
private var currentIndex = 0
@ -45,38 +48,38 @@ class MusicService : MediaBrowserServiceCompat() {
fun isServiceRunning() = isRunning.get()
fun isPlaying() = currentStatus
fun getCurrentId(): Int? = currentIdInternal
// 获取播放进度信息
fun getProgress(): Map<String, Any> {
return if (instance != null && instance!!.mediaPlayer != null) {
val player = instance!!.mediaPlayer!!
try {
val currentPosition = if (player.isPlaying || currentStatus) player.currentPosition else 0
val duration = player.duration
val progress = if (duration > 0) currentPosition.toDouble() / duration.toDouble() else 0.0
mapOf(
"currentPosition" to currentPosition,
"duration" to duration,
"progress" to progress
)
} catch (e: Exception) {
Log.e(TAG, "获取播放进度失败: ${e.message}")
mapOf(
"currentPosition" to 0,
"duration" to 0,
"progress" to 0.0
)
}
} else {
mapOf(
"currentPosition" to 0,
"duration" to 0,
"progress" to 0.0
)
}
}
fun getCurrentId(): String? = currentIdInternal
// // 获取播放进度信息
// fun getProgress(): Map<String, Any> {
// return if (instance != null && instance!!.mediaPlayer != null) {
// val player = instance!!.mediaPlayer!!
// try {
// val currentPosition = if (player.isPlaying || currentStatus) player.currentPosition else 0
// val duration = player.duration
// val progress = if (duration > 0) currentPosition.toDouble() / duration.toDouble() else 0.0
// mapOf(
// "currentPosition" to currentPosition,
// "duration" to duration,
// "progress" to progress
// )
// } catch (e: Exception) {
// Log.e(TAG, "获取播放进度失败: ${e.message}")
// mapOf(
// "currentPosition" to 0,
// "duration" to 0,
// "progress" to 0.0
// )
// }
// } else {
// mapOf(
// "currentPosition" to 0,
// "duration" to 0,
// "progress" to 0.0
// )
// }
// }
private var instance: MusicService? = null
private var flutterChannel: io.flutter.plugin.common.MethodChannel? = null
@ -118,7 +121,8 @@ class MusicService : MediaBrowserServiceCompat() {
val title = song["title"] as? String ?: song["name"] as? String ?: "未知标题"
val artist = song["artist"] as? String ?: song["sgener"] as? String ?: "未知歌手"
val coverUrl = song["coverUrl"] as? String ?: song["image"] as? String ?: ""
currentIdInternal = song["id"] as? String ?: song["id"] as? String ?: "0"
Log.i(TAG, "updateMediaSessionMetadata currentIdInternal: $currentIdInternal")
val metadata = android.support.v4.media.MediaMetadataCompat.Builder()
.putString(android.support.v4.media.MediaMetadataCompat.METADATA_KEY_TITLE, title)
.putString(android.support.v4.media.MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
@ -166,6 +170,8 @@ class MusicService : MediaBrowserServiceCompat() {
private lateinit var mediaSession: MediaSessionCompat
private lateinit var audioManager: AudioManager
private var mediaPlayer: MediaPlayer? = null
private val progressHandler = Handler(Looper.getMainLooper())
private lateinit var progressUpdater: Runnable
override fun onCreate() {
super.onCreate()
@ -191,11 +197,35 @@ class MusicService : MediaBrowserServiceCompat() {
registerReceiver(controlReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
} else {
registerReceiver(controlReceiver, filter)
}
// 初始化进度更新任务
progressUpdater = Runnable {
updateProgress()
progressHandler.postDelayed(progressUpdater, 1000) // 每秒更新一次
}
isReceiverRegistered = true
Log.i(TAG, "MediaService 启动完成")
}
@SuppressLint("SuspiciousIndentation")
private fun updateProgress() {
mediaPlayer?.let { player ->
if (player.isPlaying || currentStatus) {
val currentPosition = player.currentPosition
val duration = player.duration
val progress = if (duration > 0) currentPosition.toFloat() / duration else 0f
// 更新媒体会话进度
// 通知Flutter层
flutterChannel?.invokeMethod("onMediaSessionSeekTo1",
mapOf(
"currentIdInternal" to currentIdInternal,
"currentPosition" to currentPosition,
"duration" to duration,
"progress" to progress
))
}
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i(TAG, "onStartCommand: $intent")
intent?.action?.takeIf { it == Intent.ACTION_MEDIA_BUTTON }?.let {
@ -209,8 +239,10 @@ class MusicService : MediaBrowserServiceCompat() {
"play" -> intent.getSerializableExtra("song")?.let { url ->
val songMap = intent.getSerializableExtra("song") as? Map<String, Any>
songMap?.let { song ->
val songId = song["id"] ?: return@let
val songId = song["id"]as? String ?:"0"
Log.i(TAG, "command songId: $songId")
currentIdInternal = songId
val existingIndex = playlist.indexOfFirst { it["id"] == songId }
if (existingIndex == -1) {
playlist.add(song)
@ -323,9 +355,9 @@ class MusicService : MediaBrowserServiceCompat() {
}
override fun onSeekTo(pos: Long) {
Log.i(TAG, "onSeekTo 通知: ${pos}ms")
seekTo(pos.toInt())
// 通知Flutter层
flutterChannel?.invokeMethod("onMediaSessionSeekTo", mapOf("position" to pos.toInt()))
// seekTo(pos.toInt())
// // 通知Flutter层
// flutterChannel?.invokeMethod("onMediaSessionSeekTo", mapOf("position" to pos.toInt()))
}
override fun onStop() {
Log.i(TAG, "onStop 通知!")
@ -518,22 +550,28 @@ class MusicService : MediaBrowserServiceCompat() {
private fun play(song: Map<String,Any>) {
val url = song["url"] as? String ?: return
Log.i(TAG, "开始播放: $url")
val id = song["id"] as? Int ?: 0
val id = song["id"] as? String ?: "0"
Log.i(TAG, "开始播放play id: $id")
val title = song["title"] as? String ?: "未知标题"
val artist = song["artist"] as? String ?: "未知歌手"
val coverUrl = song["coverUrl"] as String
mediaPlayer?.release()
currentStatus = true
currentIdInternal = id
Log.i(TAG, "开始播放play currentIdInternal: $currentIdInternal")
mediaPlayer = MediaPlayer().apply {
setDataSource(url)
setAudioStreamType(AudioManager.STREAM_MUSIC)
setOnPreparedListener {
start()
updateNotification(title,artist,coverUrl)
// 启动进度更新
progressHandler.post(progressUpdater)
Log.i(TAG, "开始播放: $url")
}
setOnCompletionListener { playNextOrStop() }
setOnCompletionListener { // 停止进度更新
progressHandler.removeCallbacks(progressUpdater)
playNextOrStop() }
setOnErrorListener { _, what, extra ->
Log.e(TAG, "播放错误 what=$what extra=$extra")
true
@ -547,17 +585,18 @@ class MusicService : MediaBrowserServiceCompat() {
mediaPlayer?.takeIf { it.isPlaying }?.apply {
pause()
currentStatus = false
mediaSession.setPlaybackState(
PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PAUSED, currentPosition.toLong(), 1.0f)
.build()
)
val song = playlist[currentIndex]
val title = song["title"] as? String ?: "未知标题"
val artist = song["artist"] as? String ?: "未知歌手"
val coverUrl = song["coverUrl"] as String
val id = song["id"] as? String ?: "0"
Log.i(TAG, "pause id: $id")
currentIdInternal = id
updateNotification(title,artist,coverUrl)
setPlaybackState(PlaybackStateCompat.STATE_PAUSED);
// 停止进度更新
progressHandler.removeCallbacks(progressUpdater)
Log.i(TAG, "已暂停")
}
@ -567,17 +606,18 @@ class MusicService : MediaBrowserServiceCompat() {
mediaPlayer?.takeIf { !it.isPlaying }?.apply {
start()
currentStatus = true
mediaSession.setPlaybackState(
PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PLAYING, currentPosition.toLong(), 1.0f)
.build()
)
val song = playlist[currentIndex]
val title = song["title"] as? String ?: "未知标题"
val artist = song["artist"] as? String ?: "未知歌手"
val coverUrl = song["coverUrl"] as String
val id = song["id"] as? String ?: "0"
Log.i(TAG, "resume id: $id")
currentIdInternal = id
updateNotification(title,artist,coverUrl)
setPlaybackState(PlaybackStateCompat.STATE_PLAYING);
// 重启进度更新
progressHandler.post(progressUpdater)
// Log.i(TAG, "继续播放")
}
}
@ -635,6 +675,8 @@ class MusicService : MediaBrowserServiceCompat() {
unregisterReceiver(controlReceiver)
isReceiverRegistered = false
}
// 停止进度更新
progressHandler.removeCallbacks(progressUpdater)
mediaPlayer?.release()
stopForeground(true)
isRunning.set(false)

8
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt

@ -70,10 +70,10 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler {
)
result.success(status)
}
"getProgress" -> {
val progressInfo = MusicService.getProgress()
result.success(progressInfo)
}
// "getProgress" -> {
// // val progressInfo = MusicService.getProgress()
// result.success(progressInfo)
// }
"seekTo" -> {
val position = call.argument<Int>("position")
if (position != null) {

14
local_plugins/music_service/lib/music_service.dart

@ -82,7 +82,10 @@ class MusicService {
/// 更新媒体会话进度信息
Future<void> updateMediaSessionProgress(
int currentPosition, int duration, double progress) async {
int currentPosition,
int duration,
double progress,
) async {
await _methodChannel.invokeMethod('updateMediaSessionProgress', {
'currentPosition': currentPosition,
'duration': duration,
@ -98,6 +101,7 @@ class MusicService {
required Function() onPrevious,
required Function(int) onSeekTo,
required Function() onStop,
required Function(String, int, int, double) onStop1,
}) async {
// 设置方法调用处理器来处理来自原生平台的回调
_methodChannel.setMethodCallHandler((call) async {
@ -121,6 +125,14 @@ class MusicService {
case 'onMediaSessionStop':
onStop();
break;
case 'onMediaSessionSeekTo1':
final currentIdInternal =
call.arguments['currentIdInternal'] as String;
final currentPosition = call.arguments['currentPosition'] as int;
final duration = call.arguments['duration'] as int;
final progress = call.arguments['progress'] as double;
onStop1(currentIdInternal, currentPosition, duration, progress);
break;
}
});

Loading…
Cancel
Save