29 changed files with 1813 additions and 1334 deletions
@ -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: |
||||
File diff suppressed because it is too large
@ -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(), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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, |
||||
|
), |
||||
|
), |
||||
|
)), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -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, // 允许重新创建 |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
@ -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'); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
Loading…
Reference in new issue