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/ble_manager.dart';
import '../../data/services/account_service.dart'; import '../../data/services/account_service.dart';
import '../../data/services/version_update_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 { class InitialBinding extends Bindings {
@ -37,10 +35,8 @@ class InitialBinding extends Bindings {
Get.lazyPut<BleManager>(() => BleManager(), fenix: true); Get.lazyPut<BleManager>(() => BleManager(), fenix: true);
// MusicManager // MusicManager
Get.lazyPut<MusicManager>(() => MusicManager(), fenix: true); 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<NavigationManager>(() => NavigationManager(), fenix: true);
Get.lazyPut<LocationManager>(() => LocationManager(), fenix: true); Get.lazyPut<LocationManager>(() => LocationManager(), fenix: true);
// 火山AI服务 // 火山AI服务

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

@ -90,4 +90,6 @@ const Map<String, String> enUS = {
"goToBind": "Unbind", "goToBind": "Unbind",
"scanQrCode": "Scan the QR code", "scanQrCode": "Scan the QR code",
"endRecording": "Do you want to end the recording?", "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": "去绑定", "goToBind": "去绑定",
"scanQrCode": "扫描二维码", "scanQrCode": "扫描二维码",
"endRecording": "你想结束录音吗?", "endRecording": "你想结束录音吗?",
"earphone": "耳机",
"voicerecorder": "录音笔"
}; };

6
lib/data/models/user_Info.dart

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

2
lib/data/services/music_manager.dart

@ -105,6 +105,7 @@ class MusicManager extends GetxService {
required Function() onPrevious, required Function() onPrevious,
required Function(int) onSeekTo, required Function(int) onSeekTo,
required Function() onStop, required Function() onStop,
required Function(String, int, int, double) onStop1,
}) async { }) async {
try { try {
await _musicService.setMediaSessionCallbacks( await _musicService.setMediaSessionCallbacks(
@ -114,6 +115,7 @@ class MusicManager extends GetxService {
onPrevious: onPrevious, onPrevious: onPrevious,
onSeekTo: onSeekTo, onSeekTo: onSeekTo,
onStop: onStop, onStop: onStop,
onStop1: onStop1,
); );
} catch (e) { } catch (e) {
Logger.d('MusicManager', '设置媒体会话回调失败: $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]) { static bindauthcode([params]) {
return DioManager().request( return DioManager().request(

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

@ -25,23 +25,35 @@ import '../../../data/models/message_model.dart';
// 语音输入模式枚举 // 语音输入模式枚举
enum VoiceInputMode { enum VoiceInputMode {
text, // 文字输入模式 text, // 文字输入模式
pushToTalk, // 按住说话模式 pushToTalk, // 按住说话模式
phoneCall, // 打电话模式 phoneCall, // 打电话模式
} }
class AgentController extends GetxController { class AgentController extends GetxController {
static AgentController get to => Get.find(); //单例访问模式 static AgentController get to => Get.find(); //单例访问模式
// 聊天消息列表 // 聊天消息列表
final messages = <Message>[].obs; final messages = <Message>[].obs;
// UI控制器 // UI控制器
final TextEditingController textController = TextEditingController(); final TextEditingController textController = TextEditingController();
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
//音乐播放管理
final _musiceManager = Get.find<MusicManager>();
// 导航管理 // 导航管理
final _navigationManager = Get.find<NavigationManager>(); 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; final isInitialized = false.obs;
@ -98,6 +110,8 @@ class AgentController extends GetxController {
// BLE事件订阅 // BLE事件订阅
StreamSubscription<dynamic>? _bleEventSubscription; StreamSubscription<dynamic>? _bleEventSubscription;
// 进度更新定时器
Timer? _progressTimer;
// 用于跟踪AI回复状态 // 用于跟踪AI回复状态
bool _isNewAssistantResponse = true; bool _isNewAssistantResponse = true;
String _currentResponseId = ''; String _currentResponseId = '';
@ -110,17 +124,54 @@ class AgentController extends GetxController {
// 是否已加载聊天记录 // 是否已加载聊天记录
final isHistoryLoaded = false.obs; 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 @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
_loadTtsPreference(); //加载TTS偏好设置 _loadTtsPreference(); //加载TTS偏好设置
_loadChatHistory(); //加载聊天历史记录 _loadChatHistory(); //加载聊天历史记录
_subscribeToEvents(); //订阅代理服务事件 _subscribeToEvents(); //订阅代理服务事件
_subscribeToBleEvents(); //订阅BLE事件 _initMusicStatus(); //初始化音乐状态
_setupMediaSessionCallbacks(); //设置媒体会话回调
_setupTextInputListener(); //设置文本输入监听器 _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 @override
void onClose() { void onClose() {
@ -994,7 +1045,8 @@ class AgentController extends GetxController {
_pendingPlusOptionsExpansion = false; _pendingPlusOptionsExpansion = false;
// 添加小延迟确保UI稳定 // 添加小延迟确保UI稳定
Future.delayed(const Duration(milliseconds: 50), () { Future.delayed(const Duration(milliseconds: 50), () {
if (!isKeyboardVisible.value) { // 再次确认键盘状态 if (!isKeyboardVisible.value) {
// 再次确认键盘状态
isPlusOptionsExpanded.value = true; 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, '状态冲突:文字模式与语音模式同时启用,优先保持语音模式'); Logger.w(TAG, '状态冲突:文字模式与语音模式同时启用,优先保持语音模式');
isTextInputMode.value = false; isTextInputMode.value = false;
} }
@ -1268,12 +1321,299 @@ class AgentController extends GetxController {
await stopVoiceInput(); 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设置 // 修改自动停止处理,遵循TTS设置
void _handleAutoStop(int index) { 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 musicUrl = card_music['url'] as String? ?? '';
final musicSinger = card_music['sgener'] as String? ?? '未知歌手'; final musicSinger = card_music['sgener'] as String? ?? '未知歌手';
final musicImage = card_music['image'] 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( return buildMusicItem(context, musicId, musicName, musicUrl,
context, musicId, musicName, musicUrl, musicSinger, musicImage); musicSinger, musicImage, ts);
} }
} else if (meta!.keys.contains('music_playlist')) { } else if (meta!.keys.contains('music_playlist')) {
final card_musiclist = final card_musiclist =
@ -281,7 +283,8 @@ class MessageBubble extends GetView<AgentController> {
final end = card_navigation['end'] as String? ?? ''; final end = card_navigation['end'] as String? ?? '';
final destination = card_navigation['destination'] as String? ?? ''; final destination = card_navigation['destination'] as String? ?? '';
final rawList = card_navigation['items'] as List? ?? []; 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 rawNavigation = card_navigation['navigation'];
final Map<String, dynamic>? navigation = final Map<String, dynamic>? navigation =
rawNavigation is Map<String, dynamic> ? rawNavigation : null; 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:flutter/material.dart';
import 'package:get/get.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'; import 'circular_progress_button.dart';
/// 构建音乐项目 /// 构建音乐项目
Widget buildMusicItem(BuildContext context, int id, String name, String url, Widget buildMusicItem(BuildContext context, int id, String name, String url,
String singer, String image) { String singer, String image, int ts) {
final musicFacade = Get.find<MusicServiceFacade>(); // 获取音乐服务门面 final controller = Get.find<AgentController>(); // 获取控制器
final isDarkMode = Theme.of(context).brightness == Brightness.dark; final isDarkMode = Theme.of(context).brightness == Brightness.dark;
Map<String, dynamic> song = { Map<String, dynamic> song = {
'id': id, 'id': id,
'url': url, 'url': url,
'title': name, 'title': name,
'artist': singer, 'artist': singer,
'coverUrl': image, 'coverUrl': image,
'ts': ts,
}; };
// 音乐状态由MusicController自动管理,无需手动刷新
controller.musicStatus(); //刷新状态
return Container( return Container(
padding: const EdgeInsets.all(14), padding: const EdgeInsets.all(14),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -82,11 +84,11 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url,
), ),
Obx(() { Obx(() {
final isThisPlaying = final isThisPlaying =
musicFacade.isPlaying.value && musicFacade.currentId.value == id; controller.isPlaying.value && controller.currentId.value == id;
// 只有当前播放的歌曲才显示进度 // 只有当前播放的歌曲才显示进度
final currentProgress = final currentProgress =
isThisPlaying ? musicFacade.progress.value : 0.0; isThisPlaying ? controller.progress.value : 0.0;
return CircularProgressButton( return CircularProgressButton(
isPlaying: isThisPlaying, isPlaying: isThisPlaying,
@ -97,21 +99,21 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url,
strokeWidth: 2.0, strokeWidth: 2.0,
onPlayPause: () async { onPlayPause: () async {
try { try {
if (musicFacade.isPlaying.value) { if (controller.isPlaying.value) {
if (musicFacade.currentId.value == id) { if (controller.currentId.value == id) {
// 当前歌曲正在播放,暂停它 // 当前歌曲正在播放,暂停它
await musicFacade.musicPause(); await controller.musicPause();
} else { } else {
// 其他歌曲正在播放,播放这首歌 // 其他歌曲正在播放,播放这首歌
await musicFacade.musicPlay(song); await controller.musicPlay(song);
} }
} else { } else {
if (musicFacade.currentId.value == id) { if (controller.currentId.value == id) {
// 当前歌曲已暂停,恢复播放 // 当前歌曲已暂停,恢复播放
await musicFacade.musicResume(); await controller.musicResume();
} else { } else {
// 播放新歌曲 // 播放新歌曲
await musicFacade.musicPlay(song); await controller.musicPlay(song);
} }
} }
} catch (e) { } catch (e) {
@ -122,8 +124,8 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url,
? (seekProgress) { ? (seekProgress) {
// 只有当前播放的歌曲才允许跳转 // 只有当前播放的歌曲才允许跳转
final targetPosition = final targetPosition =
(musicFacade.duration.value * seekProgress).round(); (controller.duration.value * seekProgress).round();
musicFacade.seekToPosition(targetPosition); controller.seekToPosition(targetPosition);
} }
: null, : null,
); );
@ -137,7 +139,8 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url,
Widget buildMusicContent( Widget buildMusicContent(
BuildContext context, List<Map<String, dynamic>> playlist) { BuildContext context, List<Map<String, dynamic>> playlist) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark; final isDarkMode = Theme.of(context).brightness == Brightness.dark;
final controller = Get.find<AgentController>(); // 获取控制器
controller.updateCurrentPlaylistContext(playlist, controller.currentId.value);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -202,9 +205,10 @@ Widget buildMusicContent(
final url = item["url"] ?? ""; final url = item["url"] ?? "";
final singer = item["sgener"] ?? "未知歌手"; final singer = item["sgener"] ?? "未知歌手";
final image = item["image"] ?? ""; final image = item["image"] ?? "";
final ts = item["ts"] ?? 0;
return Column( return Column(
children: [ children: [
buildMusicItem(context, id, name, url, singer, image), buildMusicItem(context, id, name, url, singer, image, ts),
if (index != playlist.length - 1) if (index != playlist.length - 1)
Divider( Divider(
height: 1, height: 1,
@ -221,7 +225,7 @@ Widget buildMusicContent(
child: TextButton( child: TextButton(
onPressed: () { onPressed: () {
// 跳转到完整歌单页面,传递完整的歌单数据 // 跳转到完整歌单页面,传递完整的歌单数据
Get.toNamed('/music/playlist', arguments: playlist); Get.toNamed(Routes.musicPlaylist, arguments: playlist);
}, },
style: TextButton.styleFrom( style: TextButton.styleFrom(
minimumSize: Size.zero, 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_screenutil/flutter_screenutil.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import '../services/music_service_facade.dart';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
import '../../agent/controllers/agent_controller.dart';
import '../../agent/views/circular_progress_button.dart'; import '../../agent/views/circular_progress_button.dart';
class MusicPlaylistView extends StatelessWidget { class MusicPlaylistView extends StatelessWidget {
@ -13,7 +13,7 @@ class MusicPlaylistView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
ScreenUtil.init(context, designSize: const Size(375, 812)); // 添加设计尺寸参考 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 List<Map<String, dynamic>> playlist = Get.arguments ?? [];
final isDarkMode = Theme.of(context).brightness == Brightness.dark; final isDarkMode = Theme.of(context).brightness == Brightness.dark;
@ -55,14 +55,14 @@ class MusicPlaylistView extends StatelessWidget {
Container( Container(
height: MediaQuery.of(context).size.height * 0.38, // 增大高度比例 height: MediaQuery.of(context).size.height * 0.38, // 增大高度比例
child: _buildCurrentPlayingSection( child: _buildCurrentPlayingSection(
musicFacade, playlist, isDarkMode), controller, playlist, isDarkMode),
), ),
// 中部:播放全部按钮 // 中部:播放全部按钮
_buildPlayAllSection(musicFacade, playlist, isDarkMode), _buildPlayAllSection(controller, playlist, isDarkMode),
// 下部:歌单列表 - 使用剩余空间 // 下部:歌单列表 - 使用剩余空间
Expanded( Expanded(
child: 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) { List<Map<String, dynamic>> playlist, bool isDarkMode) {
return SingleChildScrollView( return SingleChildScrollView(
// 添加滚动支持 // 添加滚动支持
child: Obx(() { child: Obx(() {
// 查找当前播放的歌曲 // 查找当前播放的歌曲
Map<String, dynamic>? currentSong; Map<String, dynamic>? currentSong;
if (musicFacade.currentId.value != 0) { if (controller.currentId.value != 0) {
try { try {
currentSong = playlist.firstWhere( currentSong = playlist.firstWhere(
(song) => (song) =>
int.tryParse(song['id']?.toString() ?? '0') == int.tryParse(song['id']?.toString() ?? '0') ==
musicFacade.currentId.value, controller.currentId.value,
); );
} catch (e) { } catch (e) {
// 如果没找到,使用第一首歌曲 // 如果没找到,使用第一首歌曲
@ -137,10 +137,10 @@ class MusicPlaylistView extends StatelessWidget {
_buildSongInfo(title, artist, isDarkMode), _buildSongInfo(title, artist, isDarkMode),
SizedBox(height: 16.h), SizedBox(height: 16.h),
// 进度条 // 进度条
_buildProgressBar(musicFacade, isDarkMode), _buildProgressBar(controller, isDarkMode),
SizedBox(height: 15.h), 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) { List<Map<String, dynamic>> playlist, bool isDarkMode) {
return Container( return Container(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 12.h), padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 12.h),
@ -159,7 +159,7 @@ class MusicPlaylistView extends StatelessWidget {
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () async { onPressed: () async {
if (playlist.isNotEmpty) { if (playlist.isNotEmpty) {
await musicFacade.playPlaylist(playlist); await controller.playPlaylist(playlist);
} }
}, },
icon: Icon(Icons.play_arrow, color: Colors.white, size: 20.w), 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) { List<Map<String, dynamic>> playlist, bool isDarkMode) {
return ListView.builder( return ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 8.h), padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 8.h),
@ -200,7 +200,7 @@ class MusicPlaylistView extends StatelessWidget {
return _buildMusicItem( return _buildMusicItem(
context, context,
musicFacade, controller,
id, id,
name, name,
url, url,
@ -214,7 +214,7 @@ class MusicPlaylistView extends StatelessWidget {
Widget _buildMusicItem( Widget _buildMusicItem(
BuildContext context, BuildContext context,
MusicServiceFacade musicFacade, AgentController controller,
String id, String id,
String name, String name,
String url, String url,
@ -232,7 +232,7 @@ class MusicPlaylistView extends StatelessWidget {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
_playSelectedSong(musicFacade, song, Get.arguments ?? []); _playSelectedSong(controller, song, Get.arguments ?? []);
}, },
child: Container( child: Container(
margin: EdgeInsets.only(bottom: 10.h), margin: EdgeInsets.only(bottom: 10.h),
@ -324,10 +324,10 @@ class MusicPlaylistView extends StatelessWidget {
// 播放按钮 // 播放按钮
Obx(() { Obx(() {
final currentIdInt = int.tryParse(id) ?? 0; final currentIdInt = int.tryParse(id) ?? 0;
final isThisPlaying = musicFacade.isPlaying.value && final isThisPlaying = controller.isPlaying.value &&
musicFacade.currentId.value == currentIdInt; controller.currentId.value == currentIdInt;
final currentProgress = final currentProgress =
isThisPlaying ? musicFacade.progress.value : 0.0; isThisPlaying ? controller.progress.value : 0.0;
return CircularProgressButton( return CircularProgressButton(
isPlaying: isThisPlaying, isPlaying: isThisPlaying,
@ -339,20 +339,20 @@ class MusicPlaylistView extends StatelessWidget {
onPlayPause: () async { onPlayPause: () async {
try { try {
Logger.d('MusicPlaylistView', Logger.d('MusicPlaylistView',
'播放按钮被点击,当前播放状态: ${musicFacade.isPlaying.value}'); '播放按钮被点击,当前播放状态: ${controller.isPlaying.value}');
Logger.d('MusicPlaylistView', Logger.d('MusicPlaylistView',
'当前歌曲ID: ${musicFacade.currentId.value}, 点击歌曲ID: $currentIdInt'); '当前歌曲ID: ${controller.currentId.value}, 点击歌曲ID: $currentIdInt');
if (musicFacade.isPlaying.value) { if (controller.isPlaying.value) {
if (musicFacade.currentId.value == currentIdInt) { if (controller.currentId.value == currentIdInt) {
Logger.d('MusicPlaylistView', '暂停当前播放的歌曲'); Logger.d('MusicPlaylistView', '暂停当前播放的歌曲');
await musicFacade.musicPause(); await controller.musicPause();
} else { } else {
Logger.d('MusicPlaylistView', '播放新歌曲'); Logger.d('MusicPlaylistView', '播放新歌曲');
await musicFacade.musicPlay(song); await controller.musicPlay(song);
} }
} else { } else {
Logger.d('MusicPlaylistView', '开始播放歌曲'); Logger.d('MusicPlaylistView', '开始播放歌曲');
await musicFacade.musicPlay(song); await controller.musicPlay(song);
} }
} catch (e) { } catch (e) {
Logger.e('MusicPlaylistView', '播放音乐时发生错误: $e'); Logger.e('MusicPlaylistView', '播放音乐时发生错误: $e');
@ -362,8 +362,8 @@ class MusicPlaylistView extends StatelessWidget {
? (seekProgress) { ? (seekProgress) {
// 只有当前播放的歌曲才允许跳转 // 只有当前播放的歌曲才允许跳转
final targetPosition = final targetPosition =
(musicFacade.duration.value * seekProgress).round(); (controller.duration.value * seekProgress).round();
musicFacade.seekToPosition(targetPosition); controller.seekToPosition(targetPosition);
} }
: null, : null,
); );
@ -449,14 +449,14 @@ class MusicPlaylistView extends StatelessWidget {
} }
// 构建进度条 // 构建进度条
Widget _buildProgressBar(MusicServiceFacade musicFacade, bool isDarkMode) { Widget _buildProgressBar(AgentController controller, bool isDarkMode) {
return Obx(() { return Obx(() {
return Row( return Row(
children: [ children: [
SizedBox( SizedBox(
width: 40.w, width: 40.w,
child: Text( child: Text(
_formatDuration(musicFacade.currentPosition.value), _formatDuration(controller.currentPosition.value),
style: TextStyle( style: TextStyle(
fontSize: 12.sp, fontSize: 12.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600], color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
@ -480,11 +480,11 @@ class MusicPlaylistView extends StatelessWidget {
overlayShape: const RoundSliderOverlayShape(overlayRadius: 14), overlayShape: const RoundSliderOverlayShape(overlayRadius: 14),
), ),
child: Slider( child: Slider(
value: musicFacade.progress.value.clamp(0.0, 1.0), value: controller.progress.value.clamp(0.0, 1.0),
onChanged: (value) { onChanged: (value) {
final targetPosition = final targetPosition =
(musicFacade.duration.value * value).round(); (controller.duration.value * value).round();
musicFacade.seekToPosition(targetPosition); controller.seekToPosition(targetPosition);
}, },
), ),
), ),
@ -493,7 +493,7 @@ class MusicPlaylistView extends StatelessWidget {
SizedBox( SizedBox(
width: 40.w, width: 40.w,
child: Text( child: Text(
_formatDuration(musicFacade.duration.value), _formatDuration(controller.duration.value),
style: TextStyle( style: TextStyle(
fontSize: 12.sp, fontSize: 12.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600], 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) { List<Map<String, dynamic>> playlist, bool isDarkMode) {
return Obx(() { return Obx(() {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
IconButton( IconButton(
onPressed: () => _playPrevious(musicFacade, playlist), onPressed: () => _playPrevious(controller, playlist),
iconSize: 32.w, iconSize: 32.w,
padding: EdgeInsets.all(8.w), // 增大触摸区域 padding: EdgeInsets.all(8.w), // 增大触摸区域
icon: Icon( icon: Icon(
@ -540,10 +540,10 @@ class MusicPlaylistView extends StatelessWidget {
], ],
), ),
child: IconButton( child: IconButton(
onPressed: () => _togglePlayPause(musicFacade), onPressed: () => _togglePlayPause(controller),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
icon: Icon( icon: Icon(
musicFacade.isPlaying.value ? Icons.pause : Icons.play_arrow, controller.isPlaying.value ? Icons.pause : Icons.play_arrow,
size: 28.w, size: 28.w,
color: Colors.white, color: Colors.white,
), ),
@ -551,7 +551,7 @@ class MusicPlaylistView extends StatelessWidget {
), ),
SizedBox(width: 16.w), SizedBox(width: 16.w),
IconButton( IconButton(
onPressed: () => _playNext(musicFacade, playlist), onPressed: () => _playNext(controller, playlist),
iconSize: 32.w, iconSize: 32.w,
padding: EdgeInsets.all(8.w), // 增大触摸区域 padding: EdgeInsets.all(8.w), // 增大触摸区域
icon: Icon( icon: Icon(
@ -574,35 +574,35 @@ class MusicPlaylistView extends StatelessWidget {
} }
// 播放/暂停切换 // 播放/暂停切换
Future<void> _togglePlayPause(MusicServiceFacade musicFacade) async { Future<void> _togglePlayPause(AgentController controller) async {
if (musicFacade.isPlaying.value) { if (controller.isPlaying.value) {
await musicFacade.musicPause(); await controller.musicPause();
} else { } else {
await musicFacade.musicResume(); await controller.musicResume();
} }
} }
// 播放上一曲 // 播放上一曲
void _playPrevious( void _playPrevious(
MusicServiceFacade musicFacade, List<Map<String, dynamic>> playlist) { AgentController controller, List<Map<String, dynamic>> playlist) {
musicFacade.updateCurrentPlaylistContext( controller.updateCurrentPlaylistContext(
playlist, musicFacade.currentId.value); playlist, controller.currentId.value);
musicFacade.playPrevious(); controller.playPrevious();
} }
// 播放下一曲 // 播放下一曲
void _playNext( void _playNext(
MusicServiceFacade musicFacade, List<Map<String, dynamic>> playlist) { AgentController controller, List<Map<String, dynamic>> playlist) {
musicFacade.updateCurrentPlaylistContext( controller.updateCurrentPlaylistContext(
playlist, musicFacade.currentId.value); playlist, controller.currentId.value);
musicFacade.playNext(); controller.playNext();
} }
// 播放选中的歌曲 // 播放选中的歌曲
void _playSelectedSong(MusicServiceFacade musicFacade, Map<String, dynamic> song, void _playSelectedSong(AgentController controller, Map<String, dynamic> song,
List<Map<String, dynamic>> playlist) { List<Map<String, dynamic>> playlist) {
final songId = int.tryParse(song['id']?.toString() ?? '0') ?? 0; final songId = int.tryParse(song['id']?.toString() ?? '0') ?? 0;
musicFacade.updateCurrentPlaylistContext(playlist, songId); controller.updateCurrentPlaylistContext(playlist, songId);
musicFacade.musicPlay(song); controller.musicPlay(song);
} }
} }

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

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

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

@ -290,6 +290,7 @@ class PairingView extends GetView<PairingController> {
// 当前连接设备(有设备时才显示) // 当前连接设备(有设备时才显示)
if (device != null && if (device != null &&
(state == PairingState.verifying || (state == PairingState.verifying ||
state == PairingState.binding ||
state == PairingState.waitingBle || state == PairingState.waitingBle ||
state == PairingState.completed)) ...[ state == PairingState.completed)) ...[
SizedBox(height: 24.h), SizedBox(height: 24.h),
@ -311,6 +312,8 @@ class PairingView extends GetView<PairingController> {
return '请先通过系统蓝牙连接DeapSound设备'; return '请先通过系统蓝牙连接DeapSound设备';
case PairingState.verifying: case PairingState.verifying:
return '正在验证设备...'; return '正在验证设备...';
case PairingState.binding:
return '设备绑定确认...';
case PairingState.associating: case PairingState.associating:
return '正在建立设备关联...'; return '正在建立设备关联...';
case PairingState.waitingBle: case PairingState.waitingBle:
@ -434,6 +437,8 @@ class PairingView extends GetView<PairingController> {
return Obx(() { return Obx(() {
final showConnectButton = controller.showConnectButton.value; final showConnectButton = controller.showConnectButton.value;
final showRetryButton = controller.showRetryButton.value; final showRetryButton = controller.showRetryButton.value;
final showBindButton = controller.showBindButton.value;
final showRetryChectButton = controller.showRetryChectButton.value;
final showSkipButton = controller.showSkipButton.value; final showSkipButton = controller.showSkipButton.value;
final showAutoCodeInput = controller.showAutoCodeInput.value; final showAutoCodeInput = controller.showAutoCodeInput.value;
final showSkipBindingButton = controller.showSkipBindingButton.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) if (showRetryButton)
ElevatedButton( 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; iconData = Icons.verified_user;
iconColor = Colors.orange; iconColor = Colors.orange;
break; break;
case PairingState.binding:
iconData = Icons.blind;
iconColor = Colors.blue;
break;
case PairingState.associating: case PairingState.associating:
iconData = Icons.link; iconData = Icons.link;
iconColor = Colors.blue; iconColor = Colors.blue;
@ -195,6 +198,7 @@ class PairingView extends GetView<PairingController> {
/// 构建按钮 /// 构建按钮
Widget _buildButtons(bool isDarkMode) { Widget _buildButtons(bool isDarkMode) {
final showConnectButton = controller.showConnectButton.value; final showConnectButton = controller.showConnectButton.value;
final showBindButton = controller.showBindButton.value;
final showRetryButton = controller.showRetryButton.value; final showRetryButton = controller.showRetryButton.value;
final showAutoCodeInput = controller.showAutoCodeInput.value; final showAutoCodeInput = controller.showAutoCodeInput.value;
final showSkipButton = controller.showSkipButton.value; final showSkipButton = controller.showSkipButton.value;
@ -209,6 +213,19 @@ class PairingView extends GetView<PairingController> {
isDarkMode, isDarkMode,
), ),
// 绑定按钮
if (showBindButton)
TextButton(
onPressed: controller.confirmBindDevice,
child: Text(
'绑定',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
),
if (showRetryButton) if (showRetryButton)
_buildPrimaryButton( _buildPrimaryButton(
'重试', '重试',
@ -217,6 +234,7 @@ class PairingView extends GetView<PairingController> {
), ),
SizedBox(height: 12.h), SizedBox(height: 12.h),
if (showAutoCodeInput) if (showAutoCodeInput)
// 自动输入代码按钮 // 自动输入代码按钮
SizedBox( SizedBox(

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

@ -505,8 +505,8 @@ class SettingsView extends GetView<SettingsController> {
// 清除配对设备 // 清除配对设备
_buildSimpleNavigationSetting( _buildSimpleNavigationSetting(
title: '清除配对设备', title: '设备管理',
subtitle: '清除所有BLE配对设备记录', subtitle: '设备管理列表',
icon: Icons.bluetooth_disabled, icon: Icons.bluetooth_disabled,
iconBgColor: isDarkMode iconBgColor: isDarkMode
? Colors.red[900]!.withOpacity(0.3) ? Colors.red[900]!.withOpacity(0.3)
@ -514,9 +514,7 @@ class SettingsView extends GetView<SettingsController> {
iconColor: isDarkMode ? Colors.red[300]! : Colors.red[600]!, iconColor: isDarkMode ? Colors.red[300]! : Colors.red[600]!,
titleColor: isDarkMode ? Colors.white : null, titleColor: isDarkMode ? Colors.white : null,
subtitleColor: isDarkMode ? Colors.white70 : null, subtitleColor: isDarkMode ? Colors.white70 : null,
onTap: () { onTap: () => Get.toNamed(Routes.devices),
controller.clearBleDevices();
},
isDarkMode: isDarkMode, isDarkMode: isDarkMode,
), ),
Divider( 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 'package:get/get.dart';
import '../modules/feedback/bindings/feedback_bindings.dart'; import '../modules/feedback/bindings/feedback_bindings.dart';
import '../modules/feedback/views/feedback_view.dart'; import '../modules/feedback/views/feedback_view.dart';
@ -242,5 +244,10 @@ abstract class AppPages {
transition: Transition.rightToLeft, transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250), 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 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 return true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "启动扫描失败: ${e.message}", e) Log.e(TAG, "启动扫描失败: ${e.message}", e)
isScanning = false
return false return false
} }
} }
@ -255,6 +256,7 @@ object BleService {
return true return true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "停止扫描失败: ${e.message}", e) Log.e(TAG, "停止扫描失败: ${e.message}", e)
isScanning = false
return 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") Log.d(TAG, "getDeviceInfo $info")
result.success(info) result.success(info)
} }
"disconnect" -> {
BleService.disconnect();
result.success(null)
}
else -> { else -> {
result.notImplemented() 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 package com.yunqiinnovation.music_service
import android.annotation.SuppressLint
import android.app.Notification import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
@ -13,7 +14,9 @@ import android.media.AudioManager
import android.media.MediaPlayer import android.media.MediaPlayer
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.IBinder import android.os.IBinder
import android.os.Looper
import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat 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 const val DELETE_ACTION = "com.yunqiinnovation.music_service.DELETE_NOTIFICATION"
private val isRunning = AtomicBoolean(false) private val isRunning = AtomicBoolean(false)
private var currentIdInternal: Int? = null private var currentIdInternal: String? = null
private var currentStatus = false private var currentStatus = false
private var playlist = mutableListOf<Map<String,Any>>() private var playlist = mutableListOf<Map<String,Any>>()
private var currentIndex = 0 private var currentIndex = 0
@ -45,38 +48,38 @@ class MusicService : MediaBrowserServiceCompat() {
fun isServiceRunning() = isRunning.get() fun isServiceRunning() = isRunning.get()
fun isPlaying() = currentStatus fun isPlaying() = currentStatus
fun getCurrentId(): Int? = currentIdInternal fun getCurrentId(): String? = currentIdInternal
// 获取播放进度信息 // // 获取播放进度信息
fun getProgress(): Map<String, Any> { // fun getProgress(): Map<String, Any> {
return if (instance != null && instance!!.mediaPlayer != null) { // return if (instance != null && instance!!.mediaPlayer != null) {
val player = instance!!.mediaPlayer!! // val player = instance!!.mediaPlayer!!
try { // try {
val currentPosition = if (player.isPlaying || currentStatus) player.currentPosition else 0 // val currentPosition = if (player.isPlaying || currentStatus) player.currentPosition else 0
val duration = player.duration // val duration = player.duration
val progress = if (duration > 0) currentPosition.toDouble() / duration.toDouble() else 0.0 // val progress = if (duration > 0) currentPosition.toDouble() / duration.toDouble() else 0.0
mapOf( // mapOf(
"currentPosition" to currentPosition, // "currentPosition" to currentPosition,
"duration" to duration, // "duration" to duration,
"progress" to progress // "progress" to progress
) // )
} catch (e: Exception) { // } catch (e: Exception) {
Log.e(TAG, "获取播放进度失败: ${e.message}") // Log.e(TAG, "获取播放进度失败: ${e.message}")
mapOf( // mapOf(
"currentPosition" to 0, // "currentPosition" to 0,
"duration" to 0, // "duration" to 0,
"progress" to 0.0 // "progress" to 0.0
) // )
} // }
} else { // } else {
mapOf( // mapOf(
"currentPosition" to 0, // "currentPosition" to 0,
"duration" to 0, // "duration" to 0,
"progress" to 0.0 // "progress" to 0.0
) // )
} // }
} // }
private var instance: MusicService? = null private var instance: MusicService? = null
private var flutterChannel: io.flutter.plugin.common.MethodChannel? = 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 title = song["title"] as? String ?: song["name"] as? String ?: "未知标题"
val artist = song["artist"] as? String ?: song["sgener"] as? String ?: "未知歌手" val artist = song["artist"] as? String ?: song["sgener"] as? String ?: "未知歌手"
val coverUrl = song["coverUrl"] as? String ?: song["image"] 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() 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_TITLE, title)
.putString(android.support.v4.media.MediaMetadataCompat.METADATA_KEY_ARTIST, artist) .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 mediaSession: MediaSessionCompat
private lateinit var audioManager: AudioManager private lateinit var audioManager: AudioManager
private var mediaPlayer: MediaPlayer? = null private var mediaPlayer: MediaPlayer? = null
private val progressHandler = Handler(Looper.getMainLooper())
private lateinit var progressUpdater: Runnable
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -191,11 +197,35 @@ class MusicService : MediaBrowserServiceCompat() {
registerReceiver(controlReceiver, filter, Context.RECEIVER_NOT_EXPORTED) registerReceiver(controlReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
} else { } else {
registerReceiver(controlReceiver, filter) registerReceiver(controlReceiver, filter)
}
// 初始化进度更新任务
progressUpdater = Runnable {
updateProgress()
progressHandler.postDelayed(progressUpdater, 1000) // 每秒更新一次
} }
isReceiverRegistered = true isReceiverRegistered = true
Log.i(TAG, "MediaService 启动完成") 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 { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i(TAG, "onStartCommand: $intent") Log.i(TAG, "onStartCommand: $intent")
intent?.action?.takeIf { it == Intent.ACTION_MEDIA_BUTTON }?.let { intent?.action?.takeIf { it == Intent.ACTION_MEDIA_BUTTON }?.let {
@ -209,8 +239,10 @@ class MusicService : MediaBrowserServiceCompat() {
"play" -> intent.getSerializableExtra("song")?.let { url -> "play" -> intent.getSerializableExtra("song")?.let { url ->
val songMap = intent.getSerializableExtra("song") as? Map<String, Any> val songMap = intent.getSerializableExtra("song") as? Map<String, Any>
songMap?.let { song -> 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 } val existingIndex = playlist.indexOfFirst { it["id"] == songId }
if (existingIndex == -1) { if (existingIndex == -1) {
playlist.add(song) playlist.add(song)
@ -323,9 +355,9 @@ class MusicService : MediaBrowserServiceCompat() {
} }
override fun onSeekTo(pos: Long) { override fun onSeekTo(pos: Long) {
Log.i(TAG, "onSeekTo 通知: ${pos}ms") Log.i(TAG, "onSeekTo 通知: ${pos}ms")
seekTo(pos.toInt()) // seekTo(pos.toInt())
// 通知Flutter层 // // 通知Flutter层
flutterChannel?.invokeMethod("onMediaSessionSeekTo", mapOf("position" to pos.toInt())) // flutterChannel?.invokeMethod("onMediaSessionSeekTo", mapOf("position" to pos.toInt()))
} }
override fun onStop() { override fun onStop() {
Log.i(TAG, "onStop 通知!") Log.i(TAG, "onStop 通知!")
@ -518,22 +550,28 @@ class MusicService : MediaBrowserServiceCompat() {
private fun play(song: Map<String,Any>) { private fun play(song: Map<String,Any>) {
val url = song["url"] as? String ?: return val url = song["url"] as? String ?: return
Log.i(TAG, "开始播放: $url") 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 title = song["title"] as? String ?: "未知标题"
val artist = song["artist"] as? String ?: "未知歌手" val artist = song["artist"] as? String ?: "未知歌手"
val coverUrl = song["coverUrl"] as String val coverUrl = song["coverUrl"] as String
mediaPlayer?.release() mediaPlayer?.release()
currentStatus = true currentStatus = true
currentIdInternal = id currentIdInternal = id
Log.i(TAG, "开始播放play currentIdInternal: $currentIdInternal")
mediaPlayer = MediaPlayer().apply { mediaPlayer = MediaPlayer().apply {
setDataSource(url) setDataSource(url)
setAudioStreamType(AudioManager.STREAM_MUSIC) setAudioStreamType(AudioManager.STREAM_MUSIC)
setOnPreparedListener { setOnPreparedListener {
start() start()
updateNotification(title,artist,coverUrl) updateNotification(title,artist,coverUrl)
// 启动进度更新
progressHandler.post(progressUpdater)
Log.i(TAG, "开始播放: $url") Log.i(TAG, "开始播放: $url")
} }
setOnCompletionListener { playNextOrStop() } setOnCompletionListener { // 停止进度更新
progressHandler.removeCallbacks(progressUpdater)
playNextOrStop() }
setOnErrorListener { _, what, extra -> setOnErrorListener { _, what, extra ->
Log.e(TAG, "播放错误 what=$what extra=$extra") Log.e(TAG, "播放错误 what=$what extra=$extra")
true true
@ -547,17 +585,18 @@ class MusicService : MediaBrowserServiceCompat() {
mediaPlayer?.takeIf { it.isPlaying }?.apply { mediaPlayer?.takeIf { it.isPlaying }?.apply {
pause() pause()
currentStatus = false currentStatus = false
mediaSession.setPlaybackState(
PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PAUSED, currentPosition.toLong(), 1.0f)
.build()
)
val song = playlist[currentIndex] val song = playlist[currentIndex]
val title = song["title"] as? String ?: "未知标题" val title = song["title"] as? String ?: "未知标题"
val artist = song["artist"] as? String ?: "未知歌手" val artist = song["artist"] as? String ?: "未知歌手"
val coverUrl = song["coverUrl"] 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) updateNotification(title,artist,coverUrl)
setPlaybackState(PlaybackStateCompat.STATE_PAUSED); setPlaybackState(PlaybackStateCompat.STATE_PAUSED);
// 停止进度更新
progressHandler.removeCallbacks(progressUpdater)
Log.i(TAG, "已暂停") Log.i(TAG, "已暂停")
} }
@ -567,17 +606,18 @@ class MusicService : MediaBrowserServiceCompat() {
mediaPlayer?.takeIf { !it.isPlaying }?.apply { mediaPlayer?.takeIf { !it.isPlaying }?.apply {
start() start()
currentStatus = true currentStatus = true
mediaSession.setPlaybackState(
PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PLAYING, currentPosition.toLong(), 1.0f)
.build()
)
val song = playlist[currentIndex] val song = playlist[currentIndex]
val title = song["title"] as? String ?: "未知标题" val title = song["title"] as? String ?: "未知标题"
val artist = song["artist"] as? String ?: "未知歌手" val artist = song["artist"] as? String ?: "未知歌手"
val coverUrl = song["coverUrl"] 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) updateNotification(title,artist,coverUrl)
setPlaybackState(PlaybackStateCompat.STATE_PLAYING); setPlaybackState(PlaybackStateCompat.STATE_PLAYING);
// 重启进度更新
progressHandler.post(progressUpdater)
// Log.i(TAG, "继续播放") // Log.i(TAG, "继续播放")
} }
} }
@ -635,6 +675,8 @@ class MusicService : MediaBrowserServiceCompat() {
unregisterReceiver(controlReceiver) unregisterReceiver(controlReceiver)
isReceiverRegistered = false isReceiverRegistered = false
} }
// 停止进度更新
progressHandler.removeCallbacks(progressUpdater)
mediaPlayer?.release() mediaPlayer?.release()
stopForeground(true) stopForeground(true)
isRunning.set(false) 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) result.success(status)
} }
"getProgress" -> { // "getProgress" -> {
val progressInfo = MusicService.getProgress() // // val progressInfo = MusicService.getProgress()
result.success(progressInfo) // result.success(progressInfo)
} // }
"seekTo" -> { "seekTo" -> {
val position = call.argument<Int>("position") val position = call.argument<Int>("position")
if (position != null) { if (position != null) {

14
local_plugins/music_service/lib/music_service.dart

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