24 changed files with 2313 additions and 428 deletions
@ -0,0 +1,234 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|||
import 'package:get/get.dart'; |
|||
import '../../../routes/app_routes.dart'; |
|||
import '../controllers/agent_controller.dart'; |
|||
import 'circular_progress_button.dart'; |
|||
|
|||
/// 构建音乐项目 |
|||
Widget buildQQMusicItem( |
|||
BuildContext context, String id, String name, String singer, String image) { |
|||
final controller = Get.find<AgentController>(); // 获取控制器 |
|||
final isDarkMode = Theme.of(context).brightness == Brightness.dark; |
|||
Map<String, dynamic> song = { |
|||
'id': id, |
|||
'title': name, |
|||
'artist': singer, |
|||
'coverUrl': image, |
|||
}; |
|||
|
|||
controller.musicStatus(); //刷新状态 |
|||
return Container( |
|||
padding: EdgeInsets.all(14.w), |
|||
decoration: BoxDecoration( |
|||
color: isDarkMode ? const Color(0xFF1E1E1E) : Colors.white, |
|||
borderRadius: BorderRadius.circular(12.r), |
|||
boxShadow: [ |
|||
BoxShadow( |
|||
color: Colors.black.withValues(alpha: isDarkMode ? 0.2 : 0.03), |
|||
blurRadius: 3, |
|||
spreadRadius: 1, |
|||
offset: const Offset(0, 1), |
|||
), |
|||
], |
|||
), |
|||
child: Row( |
|||
children: [ |
|||
Container( |
|||
width: 50.w, |
|||
height: 50.h, |
|||
decoration: BoxDecoration( |
|||
color: isDarkMode ? Colors.grey[800] : Colors.grey[100], |
|||
borderRadius: BorderRadius.circular(8.r), |
|||
image: image.isNotEmpty |
|||
? DecorationImage( |
|||
image: NetworkImage(image), |
|||
fit: BoxFit.cover, |
|||
) |
|||
: null, |
|||
), |
|||
child: image.isEmpty |
|||
? Icon( |
|||
Icons.music_note, |
|||
size: 18.sp, |
|||
color: Theme.of(context).primaryColor, |
|||
) |
|||
: null, |
|||
), |
|||
SizedBox(width: 12.w), |
|||
Expanded( |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Text( |
|||
name, |
|||
style: TextStyle( |
|||
fontSize: 13.sp, |
|||
fontWeight: FontWeight.w500, |
|||
color: isDarkMode |
|||
? Colors.white.withValues(alpha: 0.9) |
|||
: Colors.grey[800], |
|||
), |
|||
overflow: TextOverflow.ellipsis, |
|||
), |
|||
SizedBox(height: 2.h), |
|||
Text( |
|||
singer, |
|||
style: TextStyle( |
|||
fontSize: 12.sp, |
|||
color: isDarkMode ? Colors.grey[400] : Colors.grey[600], |
|||
), |
|||
overflow: TextOverflow.ellipsis, |
|||
), |
|||
], |
|||
), |
|||
), |
|||
Obx(() { |
|||
final isThisPlaying = |
|||
controller.isPlaying.value && controller.currentId.value == id; |
|||
|
|||
// 只有当前播放的歌曲才显示进度 |
|||
final currentProgress = |
|||
isThisPlaying ? controller.progress.value : 0.0; |
|||
|
|||
return CircularProgressButton( |
|||
isPlaying: isThisPlaying, |
|||
progress: currentProgress, |
|||
primaryColor: Theme.of(context).primaryColor, |
|||
backgroundColor: Colors.grey, |
|||
size: 36.0, |
|||
strokeWidth: 2.0, |
|||
onPlayPause: () async { |
|||
try { |
|||
if (controller.isPlaying.value) { |
|||
if (controller.currentId.value == id) { |
|||
// 当前歌曲正在播放,暂停它 |
|||
await controller.musicPause(); |
|||
} else { |
|||
// 其他歌曲正在播放,播放这首歌 |
|||
await controller.musicPlay(song); |
|||
} |
|||
} else { |
|||
if (controller.currentId.value == id) { |
|||
// 当前歌曲已暂停,恢复播放 |
|||
await controller.musicResume(); |
|||
} else { |
|||
// 播放新歌曲 |
|||
await controller.musicPlay(song); |
|||
} |
|||
} |
|||
} catch (e) { |
|||
// 静默处理错误,避免影响用户体验 |
|||
} |
|||
}, |
|||
onSeek: isThisPlaying |
|||
? (seekProgress) { |
|||
// 只有当前播放的歌曲才允许跳转 |
|||
final targetPosition = |
|||
(controller.duration.value * seekProgress).round(); |
|||
controller.seekToPosition(targetPosition); |
|||
} |
|||
: null, |
|||
); |
|||
}), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
|
|||
/// 构建音乐内容卡片 |
|||
Widget buildQQMusicContent( |
|||
BuildContext context, List<Map<String, dynamic>> playlist) { |
|||
final isDarkMode = Theme.of(context).brightness == Brightness.dark; |
|||
final controller = Get.find<AgentController>(); // 获取控制器 |
|||
controller.updateCurrentPlaylistContext(playlist, controller.currentId.value); |
|||
return Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Text( |
|||
'recommendedMusic'.tr, //以下是一些推荐的音乐: |
|||
style: TextStyle( |
|||
color: |
|||
isDarkMode ? Colors.white.withValues(alpha: 0.9) : Colors.black87, |
|||
fontSize: 14, |
|||
height: 1.4, |
|||
fontWeight: FontWeight.w400, |
|||
), |
|||
), |
|||
const SizedBox(height: 12), |
|||
Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Row( |
|||
children: [ |
|||
Icon( |
|||
Icons.music_note, |
|||
size: 16, |
|||
// ignore: deprecated_member_use |
|||
color: Theme.of(context).primaryColor.withOpacity(0.7), |
|||
), |
|||
const SizedBox(width: 6), |
|||
Text( |
|||
'playList'.tr, //播放列表 |
|||
style: TextStyle( |
|||
fontSize: 13, |
|||
fontWeight: FontWeight.w600, |
|||
color: Theme.of(context).brightness == Brightness.dark |
|||
? Colors.grey[300] |
|||
: Colors.grey[700], |
|||
), |
|||
), |
|||
], |
|||
), |
|||
const SizedBox(height: 10), |
|||
|
|||
// 动态生成音乐条目 |
|||
...playlist.asMap().entries.take(4).map((entry) { |
|||
final index = entry.key; |
|||
final item = entry.value; |
|||
final mid = item["mid"]; |
|||
final name = item["name"] ?? "未知歌曲"; |
|||
final singer = item["singer"] ?? "未知歌手"; |
|||
final image = item["pic"] ?? ""; |
|||
return Column( |
|||
children: [ |
|||
buildQQMusicItem(context, mid, name, singer, image), |
|||
if (index != playlist.length - 1) |
|||
Divider( |
|||
height: 1, |
|||
thickness: 1, |
|||
color: Theme.of(context).brightness == Brightness.dark |
|||
? Colors.grey[800] |
|||
: Colors.grey[100], |
|||
), |
|||
], |
|||
); |
|||
}).toList(), |
|||
const SizedBox(height: 8), |
|||
Center( |
|||
child: TextButton( |
|||
onPressed: () { |
|||
// 跳转到完整歌单页面,传递完整的歌单数据 |
|||
Get.toNamed(Routes.qqmusic, arguments: playlist); |
|||
}, |
|||
style: TextButton.styleFrom( |
|||
minimumSize: Size.zero, |
|||
padding: |
|||
const EdgeInsets.symmetric(horizontal: 12, vertical: 8), |
|||
tapTargetSize: MaterialTapTargetSize.shrinkWrap, |
|||
), |
|||
child: Text( |
|||
'moreMusic'.tr, //更多音乐, |
|||
style: TextStyle( |
|||
fontSize: 12, |
|||
color: Theme.of(context).primaryColor, |
|||
fontWeight: FontWeight.w500, |
|||
), |
|||
), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
], |
|||
); |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
import 'package:get/get.dart'; |
|||
import '../controllers/qqmusic_playlist_controller.dart'; |
|||
|
|||
class QQMusicPlaylistBinding extends Bindings { |
|||
@override |
|||
void dependencies() { |
|||
Get.lazyPut<QQMusicPlaylistController>(() => QQMusicPlaylistController()); |
|||
} |
|||
} |
|||
@ -0,0 +1,216 @@ |
|||
import 'package:get/get.dart'; |
|||
import 'package:qq_music/qq_music.dart'; |
|||
import 'package:voitrans/data/services/qq_musoc.dart'; |
|||
import 'package:voitrans/modules/qqmusic/controllers/song_model.dart'; |
|||
|
|||
class QQMusicPlaylistController extends GetxController { |
|||
final QqMusocService _qqMusic = Get.find<QqMusocService>(); |
|||
|
|||
// 登录状态 |
|||
final isLoggedIn = false.obs; |
|||
final userInfo = Rxn<QQMusicLoginResult>(); |
|||
|
|||
// 歌单数据 |
|||
final playlist = <SongModel>[].obs; |
|||
|
|||
final currentSong = Rxn<SongModel>(); |
|||
final isPlaying = false.obs; |
|||
final currentPosition = 0.obs; |
|||
final totalDuration = 0.obs; |
|||
final currentIndex = 0.obs; |
|||
|
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
// 从路由参数获取歌单数据 |
|||
final List<Map<String, dynamic>> playlistData = Get.arguments ?? []; |
|||
final songs = playlistData.map((songData) { |
|||
return SongModel( |
|||
mid: songData['mid'] ?? '', |
|||
name: songData['name'] ?? '', |
|||
artist: songData['singer'] ?? '', |
|||
album: songData['album'] ?? '', |
|||
albumPic: songData['pic'], |
|||
duration: songData['duration'] ?? 0, |
|||
); |
|||
}).toList(); |
|||
currentSong.value = songs[0]; |
|||
playlist.assignAll(songs); |
|||
} |
|||
|
|||
/// 播放歌曲 |
|||
Future<void> playSong(SongModel song, {List<SongModel>? songList}) async { |
|||
try { |
|||
currentSong.value = song; |
|||
|
|||
if (songList != null) { |
|||
playlist.assignAll(songList); |
|||
currentIndex.value = songList.indexOf(song); |
|||
} else { |
|||
playlist.assignAll([song]); |
|||
currentIndex.value = 0; |
|||
} |
|||
|
|||
// 获取歌曲MID列表 |
|||
final songMids = playlist.map((s) => s.mid).toList(); |
|||
final playIndex = currentIndex.value; |
|||
|
|||
// 调用QQ音乐播放接口 |
|||
final result = await _qqMusic.playSongs( |
|||
songMids: songMids, |
|||
index: playIndex, |
|||
); |
|||
|
|||
if (result['success'] == true) { |
|||
isPlaying.value = true; |
|||
totalDuration.value = song.duration; |
|||
currentPosition.value = 0; |
|||
Get.snackbar('开始播放', song.name); |
|||
} else { |
|||
Get.snackbar('播放失败', result['error'] ?? '未知错误'); |
|||
} |
|||
} catch (e) { |
|||
Get.snackbar('播放失败', e.toString()); |
|||
} |
|||
} |
|||
|
|||
/// 暂停/继续播放 |
|||
Future<void> togglePlayPause() async { |
|||
try { |
|||
if (isPlaying.value) { |
|||
final result = await _qqMusic.pauseMusic(); |
|||
if (result['success'] == true) { |
|||
isPlaying.value = false; |
|||
} |
|||
} else { |
|||
final result = await _qqMusic.resumeMusic(); |
|||
if (result['success'] == true) { |
|||
isPlaying.value = true; |
|||
} |
|||
} |
|||
} catch (e) { |
|||
Get.snackbar('操作失败', e.toString()); |
|||
} |
|||
} |
|||
|
|||
/// 上一首 |
|||
Future<void> previousSong() async { |
|||
try { |
|||
final result = await _qqMusic.skipToPrevious(); |
|||
if (result['success'] == true) { |
|||
if (playlist.isNotEmpty) { |
|||
int newIndex = currentIndex.value - 1; |
|||
if (newIndex < 0) newIndex = playlist.length - 1; |
|||
currentIndex.value = newIndex; |
|||
currentSong.value = playlist[newIndex]; |
|||
} |
|||
await _updateCurrentSong(); |
|||
} |
|||
} catch (e) { |
|||
Get.snackbar('切换失败', e.toString()); |
|||
} |
|||
} |
|||
|
|||
/// 下一首 |
|||
Future<void> nextSong() async { |
|||
try { |
|||
final result = await _qqMusic.skipToNext(); |
|||
if (result['success'] == true) { |
|||
if (playlist.isNotEmpty) { |
|||
int newIndex = currentIndex.value + 1; |
|||
if (newIndex >= playlist.length) newIndex = 0; |
|||
currentIndex.value = newIndex; |
|||
currentSong.value = playlist[newIndex]; |
|||
} |
|||
await _updateCurrentSong(); |
|||
} |
|||
} catch (e) { |
|||
Get.snackbar('切换失败', e.toString()); |
|||
} |
|||
} |
|||
|
|||
/// 停止播放 |
|||
Future<void> stopMusic() async { |
|||
try { |
|||
final result = await _qqMusic.stopMusic(); |
|||
if (result['success'] == true) { |
|||
isPlaying.value = false; |
|||
currentPosition.value = 0; |
|||
} |
|||
} catch (e) { |
|||
Get.snackbar('停止失败', e.toString()); |
|||
} |
|||
} |
|||
|
|||
/// 更新当前播放歌曲信息 |
|||
Future<void> _updateCurrentSong() async { |
|||
try { |
|||
final result = await _qqMusic.getCurrentSong(); |
|||
if (result['success'] == true && result['song'] != null) { |
|||
final songData = result['song']; |
|||
final song = SongModel( |
|||
mid: songData['mid'] ?? '', |
|||
name: songData['name'] ?? '', |
|||
artist: songData['singer'] ?? '', |
|||
album: songData['album'] ?? '', |
|||
albumPic: songData['pic'], |
|||
duration: songData['duration'] ?? 0, |
|||
); |
|||
currentSong.value = song; |
|||
} |
|||
} catch (e) { |
|||
print('更新当前歌曲信息失败: $e'); |
|||
} |
|||
} |
|||
|
|||
/// 启动播放状态监控 |
|||
void _startPlaybackStateMonitor() { |
|||
// 每秒检查一次播放状态和时间 |
|||
Stream.periodic(const Duration(seconds: 1)).listen((_) async { |
|||
if (isLoggedIn.value) { |
|||
await _updatePlaybackState(); |
|||
await _updatePlayTime(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
/// 更新播放状态 |
|||
Future<void> _updatePlaybackState() async { |
|||
try { |
|||
final result = await _qqMusic.getPlaybackState(); |
|||
if (result['success'] == true && result['state'] != null) { |
|||
final state = result['state']; |
|||
// QQ音乐播放状态:0-停止,1-播放,2-暂停 |
|||
isPlaying.value = state == 1; |
|||
} |
|||
} catch (e) { |
|||
// 静默处理错误,避免频繁弹窗 |
|||
} |
|||
} |
|||
|
|||
/// 更新播放时间 |
|||
Future<void> _updatePlayTime() async { |
|||
try { |
|||
final result = await _qqMusic.getPlayTime(); |
|||
if (result['success'] == true) { |
|||
currentPosition.value = result['currentTime'] ?? 0; |
|||
totalDuration.value = result['totalTime'] ?? 0; |
|||
} |
|||
} catch (e) { |
|||
// 静默处理错误,避免频繁弹窗 |
|||
} |
|||
} |
|||
|
|||
/// 跳转到指定位置(暂时保留,QQ音乐可能不支持) |
|||
void seekTo(int position) { |
|||
currentPosition.value = position; |
|||
// QQ音乐SDK可能不支持跳转功能 |
|||
Get.snackbar('提示', '当前版本不支持跳转功能'); |
|||
} |
|||
|
|||
@override |
|||
void onClose() { |
|||
// 清理资源 |
|||
super.onClose(); |
|||
} |
|||
} |
|||
@ -0,0 +1,49 @@ |
|||
class SongModel { |
|||
final String mid; |
|||
final String name; |
|||
final String artist; |
|||
final String album; |
|||
final String albumPic; |
|||
final int duration; |
|||
final bool isFavorite; |
|||
|
|||
SongModel({ |
|||
required this.mid, |
|||
required this.name, |
|||
required this.artist, |
|||
required this.album, |
|||
required this.albumPic, |
|||
required this.duration, |
|||
this.isFavorite = false, |
|||
}); |
|||
|
|||
factory SongModel.fromJson(Map<String, dynamic> json) { |
|||
return SongModel( |
|||
mid: json['mid'] ?? '', |
|||
name: json['name'] ?? '', |
|||
artist: json['artist'] ?? '', |
|||
album: json['album'] ?? '', |
|||
albumPic: json['albumPic'] ?? '', |
|||
duration: json['duration'] ?? 0, |
|||
isFavorite: json['isFavorite'] ?? false, |
|||
); |
|||
} |
|||
|
|||
Map<String, dynamic> toJson() { |
|||
return { |
|||
'mid': mid, |
|||
'name': name, |
|||
'artist': artist, |
|||
'album': album, |
|||
'albumPic': albumPic, |
|||
'duration': duration, |
|||
'isFavorite': isFavorite, |
|||
}; |
|||
} |
|||
|
|||
String get durationText { |
|||
final minutes = duration ~/ 60; |
|||
final seconds = duration % 60; |
|||
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; |
|||
} |
|||
} |
|||
@ -0,0 +1,542 @@ |
|||
import 'package:get/get.dart'; |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|||
import 'package:voitrans/modules/qqmusic/controllers/qqmusic_playlist_controller.dart'; |
|||
import 'package:voitrans/modules/qqmusic/controllers/song_model.dart'; |
|||
import '../../../core/utils/logger.dart'; |
|||
import '../../agent/controllers/agent_controller.dart'; |
|||
import '../../agent/views/circular_progress_button.dart'; |
|||
|
|||
class QqmusicPlaylistView extends GetView<QQMusicPlaylistController> { |
|||
const QqmusicPlaylistView({Key? key}) : super(key: key); |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
ScreenUtil.init(context, designSize: const Size(375, 812)); // 添加设计尺寸参考 |
|||
|
|||
final isDarkMode = Theme.of(context).brightness == Brightness.dark; |
|||
|
|||
return Scaffold( |
|||
appBar: AppBar( |
|||
leading: IconButton( |
|||
icon: const Icon(Icons.arrow_back_ios, size: 20), // iOS风格箭头 |
|||
onPressed: () => Navigator.pop(context), |
|||
), |
|||
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, |
|||
foregroundColor: isDarkMode ? Colors.white : Colors.black, |
|||
elevation: 0, |
|||
), |
|||
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], |
|||
body: controller.playlist.isEmpty |
|||
? Center( |
|||
child: Column( |
|||
mainAxisAlignment: MainAxisAlignment.center, |
|||
children: [ |
|||
Icon( |
|||
Icons.music_off, |
|||
size: 64.w, |
|||
color: Colors.grey[400], |
|||
), |
|||
SizedBox(height: 16.h), |
|||
Text( |
|||
'noMusic'.tr, // 暂无音乐 |
|||
style: TextStyle( |
|||
fontSize: 16.sp, |
|||
color: Colors.grey[600], |
|||
), |
|||
), |
|||
], |
|||
), |
|||
) |
|||
: Column( |
|||
children: [ |
|||
// 上部:当前播放音乐详情 - 响应式高度 |
|||
Container( |
|||
height: MediaQuery.of(context).size.height * 0.38, // 增大高度比例 |
|||
child: _buildCurrentPlayingSection(isDarkMode), |
|||
), |
|||
// 中部:播放全部按钮 |
|||
_buildPlayAllSection(isDarkMode), |
|||
// 下部:歌单列表 - 使用剩余空间 |
|||
Expanded( |
|||
child: _buildPlaylistSection(isDarkMode), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
|
|||
// 构建当前播放音乐详情部分 |
|||
Widget _buildCurrentPlayingSection(bool isDarkMode) { |
|||
return SingleChildScrollView( |
|||
// 添加滚动支持 |
|||
child: Obx(() { |
|||
return Container( |
|||
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 12.h), |
|||
child: Column( |
|||
mainAxisAlignment: MainAxisAlignment.center, |
|||
children: [ |
|||
// 封面图片 - 响应式尺寸 |
|||
_buildCoverImage( |
|||
controller.currentSong.value!.albumPic, isDarkMode, |
|||
size: 110.w), |
|||
SizedBox(height: 12.h), |
|||
// 歌曲信息 |
|||
_buildSongInfo(controller.currentSong.value!.name, |
|||
controller.currentSong.value!.artist, isDarkMode), |
|||
SizedBox(height: 16.h), |
|||
// 进度条 |
|||
_buildProgressBar(isDarkMode), |
|||
SizedBox(height: 15.h), |
|||
// 播放控制按钮 |
|||
_buildPlayControls(isDarkMode), |
|||
], |
|||
), |
|||
); |
|||
}), |
|||
); |
|||
} |
|||
|
|||
// 构建播放全部按钮部分 |
|||
Widget _buildPlayAllSection(bool isDarkMode) { |
|||
return Container( |
|||
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 12.h), |
|||
child: SizedBox( |
|||
width: double.infinity, |
|||
height: 48.h, |
|||
child: ElevatedButton.icon( |
|||
onPressed: () async { |
|||
if (controller.playlist.isNotEmpty) {} |
|||
}, |
|||
icon: Icon(Icons.play_arrow, color: Colors.white, size: 20.w), |
|||
label: Text( |
|||
'playAll'.tr, // 播放全部 |
|||
style: TextStyle( |
|||
fontSize: 15.sp, |
|||
fontWeight: FontWeight.w600, |
|||
color: Colors.white, |
|||
), |
|||
), |
|||
style: ElevatedButton.styleFrom( |
|||
backgroundColor: Theme.of(Get.context!).primaryColor, |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(14.r), |
|||
), |
|||
elevation: 3, |
|||
shadowColor: Theme.of(Get.context!).primaryColor.withAlpha(102), |
|||
), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
// 构建歌单列表部分 |
|||
Widget _buildPlaylistSection(bool isDarkMode) { |
|||
return ListView.builder( |
|||
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 8.h), |
|||
itemCount: controller.playlist.length, |
|||
itemBuilder: (context, index) { |
|||
final song = controller.playlist[index]; |
|||
return _buildMusicItem( |
|||
context, |
|||
song, |
|||
isDarkMode, |
|||
); |
|||
}, |
|||
); |
|||
} |
|||
|
|||
Widget _buildMusicItem( |
|||
BuildContext context, |
|||
SongModel song, |
|||
bool isDarkMode, |
|||
) { |
|||
return GestureDetector( |
|||
onTap: () async { |
|||
await controller.playSong(song); |
|||
}, |
|||
child: Container( |
|||
margin: EdgeInsets.only(bottom: 10.h), |
|||
padding: EdgeInsets.all(14.w), |
|||
decoration: BoxDecoration( |
|||
color: isDarkMode ? Colors.grey[800] : Colors.white, |
|||
borderRadius: BorderRadius.circular(14.r), |
|||
boxShadow: [ |
|||
BoxShadow( |
|||
color: Colors.black.withOpacity(isDarkMode ? 0.25 : 0.05), |
|||
blurRadius: 4, |
|||
offset: const Offset(0, 2), |
|||
), |
|||
], |
|||
), |
|||
child: Row( |
|||
children: [ |
|||
// 封面图片 |
|||
Container( |
|||
width: 52.w, |
|||
height: 52.w, |
|||
decoration: BoxDecoration( |
|||
borderRadius: BorderRadius.circular(10.r), |
|||
color: isDarkMode ? Colors.grey[700] : Colors.grey[200], |
|||
boxShadow: [ |
|||
BoxShadow( |
|||
color: Colors.black.withOpacity(isDarkMode ? 0.2 : 0.08), |
|||
blurRadius: 2, |
|||
offset: const Offset(0, 1), |
|||
), |
|||
], |
|||
), |
|||
child: ClipRRect( |
|||
borderRadius: BorderRadius.circular(10.r), |
|||
child: Image.network( |
|||
song.albumPic ?? '', |
|||
fit: BoxFit.cover, |
|||
errorBuilder: (context, error, stackTrace) { |
|||
return Icon( |
|||
Icons.music_note, |
|||
color: isDarkMode ? Colors.grey[500] : Colors.grey[600], |
|||
size: 24.w, |
|||
); |
|||
}, |
|||
), |
|||
)), |
|||
SizedBox(width: 14.w), |
|||
// 歌曲信息 |
|||
Expanded( |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
mainAxisAlignment: MainAxisAlignment.center, |
|||
children: [ |
|||
Text( |
|||
song.name, |
|||
style: TextStyle( |
|||
fontSize: 15.5.sp, |
|||
fontWeight: FontWeight.w600, |
|||
color: isDarkMode ? Colors.white : Colors.black87, |
|||
height: 1.2, |
|||
), |
|||
maxLines: 1, |
|||
overflow: TextOverflow.ellipsis, |
|||
), |
|||
SizedBox(height: 3.h), |
|||
Text( |
|||
song.artist, |
|||
style: TextStyle( |
|||
fontSize: 13.sp, |
|||
color: isDarkMode ? Colors.grey[400] : Colors.grey[600], |
|||
height: 1.1, |
|||
), |
|||
maxLines: 1, |
|||
overflow: TextOverflow.ellipsis, |
|||
), |
|||
], |
|||
), |
|||
), |
|||
SizedBox(width: 14.w), |
|||
// 播放按钮 |
|||
Obx(() { |
|||
final currentIdInt = song.mid; |
|||
final isThisPlaying = controller.isPlaying.value && |
|||
controller.currentIndex.value == currentIdInt; |
|||
final currentProgress = isThisPlaying |
|||
? controller.totalDuration.value > 0 |
|||
? controller.currentPosition.value / |
|||
controller.totalDuration.value |
|||
: 0.0 |
|||
: 0.0; |
|||
return CircularProgressButton( |
|||
isPlaying: isThisPlaying, |
|||
progress: currentProgress, |
|||
primaryColor: Theme.of(context).primaryColor, |
|||
backgroundColor: Colors.grey, |
|||
size: 40.w, |
|||
strokeWidth: 2.5, |
|||
onPlayPause: () async { |
|||
try { |
|||
Logger.d('MusicPlaylistView', |
|||
'播放按钮被点击,当前播放状态: ${controller.isPlaying.value}'); |
|||
Logger.d('MusicPlaylistView', |
|||
'当前歌曲ID: ${controller.currentIndex.value}, 点击歌曲ID: $currentIdInt'); |
|||
if (controller.isPlaying.value) { |
|||
if (controller.currentIndex.value == currentIdInt) { |
|||
Logger.d('MusicPlaylistView', '暂停当前播放的歌曲'); |
|||
await controller.togglePlayPause(); |
|||
} else { |
|||
Logger.d('MusicPlaylistView', '播放新歌曲'); |
|||
await controller.playSong(song); |
|||
} |
|||
} else { |
|||
Logger.d('MusicPlaylistView', '开始播放歌曲'); |
|||
await controller.playSong(song); |
|||
} |
|||
} catch (e) { |
|||
Logger.e('MusicPlaylistView', '播放音乐时发生错误: $e'); |
|||
} |
|||
}, |
|||
onSeek: isThisPlaying ? (seekProgress) {} : null, |
|||
); |
|||
}), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
// 构建封面图片 |
|||
Widget _buildCoverImage(String coverUrl, bool isDarkMode, |
|||
{required double size}) { |
|||
return Container( |
|||
width: size, |
|||
height: size, |
|||
decoration: BoxDecoration( |
|||
borderRadius: BorderRadius.circular(16.r), |
|||
boxShadow: [ |
|||
BoxShadow( |
|||
color: Colors.black.withOpacity(isDarkMode ? 0.3 : 0.1), |
|||
blurRadius: 15, |
|||
spreadRadius: 2, |
|||
offset: const Offset(0, 6), |
|||
), |
|||
], |
|||
), |
|||
child: ClipRRect( |
|||
borderRadius: BorderRadius.circular(16.r), |
|||
child: coverUrl.isNotEmpty |
|||
? Image.network( |
|||
coverUrl, |
|||
fit: BoxFit.cover, |
|||
errorBuilder: (context, error, stackTrace) { |
|||
return _buildDefaultCover(isDarkMode, size: size); |
|||
}, |
|||
) |
|||
: _buildDefaultCover(isDarkMode, size: size), |
|||
), |
|||
); |
|||
} |
|||
|
|||
// 构建默认封面 |
|||
Widget _buildDefaultCover(bool isDarkMode, {required double size}) { |
|||
return Container( |
|||
color: isDarkMode ? Colors.grey[800] : Colors.grey[300], |
|||
child: Icon( |
|||
Icons.music_note, |
|||
size: size * 0.55, |
|||
color: isDarkMode ? Colors.grey[600] : Colors.grey[500], |
|||
), |
|||
); |
|||
} |
|||
|
|||
// 构建歌曲信息 |
|||
Widget _buildSongInfo(String title, String artist, bool isDarkMode) { |
|||
return Column( |
|||
children: [ |
|||
Text( |
|||
title, |
|||
style: TextStyle( |
|||
fontSize: 18.sp, |
|||
fontWeight: FontWeight.bold, |
|||
color: isDarkMode ? Colors.white : Colors.black87, |
|||
), |
|||
textAlign: TextAlign.center, |
|||
maxLines: 2, |
|||
overflow: TextOverflow.ellipsis, |
|||
), |
|||
SizedBox(height: 6.h), |
|||
Text( |
|||
artist, |
|||
style: TextStyle( |
|||
fontSize: 14.sp, |
|||
color: isDarkMode ? Colors.grey[400] : Colors.grey[600], |
|||
), |
|||
textAlign: TextAlign.center, |
|||
maxLines: 1, |
|||
overflow: TextOverflow.ellipsis, |
|||
), |
|||
], |
|||
); |
|||
} |
|||
|
|||
// 构建进度条 |
|||
Widget _buildProgressBar(bool isDarkMode) { |
|||
return Obx(() { |
|||
return Row( |
|||
children: [ |
|||
SizedBox( |
|||
width: 40.w, |
|||
child: Text( |
|||
_formatDuration(controller.currentPosition.value), |
|||
style: TextStyle( |
|||
fontSize: 12.sp, |
|||
color: isDarkMode ? Colors.grey[400] : Colors.grey[600], |
|||
fontFeatures: const [FontFeature.tabularFigures()], |
|||
), |
|||
textAlign: TextAlign.left, |
|||
), |
|||
), |
|||
SizedBox(width: 8.w), |
|||
Expanded( |
|||
child: SliderTheme( |
|||
data: SliderThemeData( |
|||
activeTrackColor: Theme.of(Get.context!).primaryColor, |
|||
inactiveTrackColor: |
|||
isDarkMode ? Colors.grey[700] : Colors.grey[300], |
|||
thumbColor: Theme.of(Get.context!).primaryColor, |
|||
overlayColor: |
|||
Theme.of(Get.context!).primaryColor.withOpacity(0.2), |
|||
trackHeight: 4.h, |
|||
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8), |
|||
overlayShape: const RoundSliderOverlayShape(overlayRadius: 14), |
|||
), |
|||
child: Slider( |
|||
value: controller.totalDuration.value > 0 |
|||
? controller.currentPosition.value / |
|||
controller.totalDuration.value |
|||
: 0, |
|||
onChanged: (value) {}, |
|||
), |
|||
), |
|||
), |
|||
SizedBox(width: 8.w), |
|||
SizedBox( |
|||
width: 40.w, |
|||
child: Text( |
|||
_formatDuration(controller.currentSong.value!.duration), |
|||
style: TextStyle( |
|||
fontSize: 12.sp, |
|||
color: isDarkMode ? Colors.grey[400] : Colors.grey[600], |
|||
fontFeatures: const [FontFeature.tabularFigures()], |
|||
), |
|||
textAlign: TextAlign.right, |
|||
), |
|||
), |
|||
], |
|||
); |
|||
}); |
|||
} |
|||
|
|||
// 构建播放控制按钮 |
|||
Widget _buildPlayControls(bool isDarkMode) { |
|||
return Obx(() { |
|||
return Row( |
|||
mainAxisAlignment: MainAxisAlignment.center, |
|||
children: [ |
|||
IconButton( |
|||
onPressed: () {}, |
|||
iconSize: 32.w, |
|||
padding: EdgeInsets.all(8.w), // 增大触摸区域 |
|||
icon: Icon( |
|||
Icons.skip_previous, |
|||
color: isDarkMode ? Colors.white70 : Colors.black54, |
|||
), |
|||
), |
|||
SizedBox(width: 16.w), |
|||
Container( |
|||
width: 56.w, |
|||
height: 56.w, |
|||
decoration: BoxDecoration( |
|||
color: Theme.of(Get.context!).primaryColor, |
|||
shape: BoxShape.circle, |
|||
boxShadow: [ |
|||
BoxShadow( |
|||
color: Theme.of(Get.context!).primaryColor.withOpacity(0.3), |
|||
blurRadius: 10, |
|||
spreadRadius: 1, |
|||
offset: const Offset(0, 3), |
|||
), |
|||
], |
|||
), |
|||
child: IconButton( |
|||
onPressed: () async { |
|||
await controller.togglePlayPause(); |
|||
}, |
|||
padding: EdgeInsets.zero, |
|||
icon: Icon( |
|||
controller.isPlaying.value ? Icons.pause : Icons.play_arrow, |
|||
size: 28.w, |
|||
color: Colors.white, |
|||
), |
|||
), |
|||
), |
|||
SizedBox(width: 16.w), |
|||
IconButton( |
|||
onPressed: () async { |
|||
await controller.nextSong(); |
|||
}, |
|||
iconSize: 32.w, |
|||
padding: EdgeInsets.all(8.w), // 增大触摸区域 |
|||
icon: Icon( |
|||
Icons.skip_next, |
|||
color: isDarkMode ? Colors.white70 : Colors.black54, |
|||
), |
|||
), |
|||
], |
|||
); |
|||
}); |
|||
} |
|||
|
|||
// ...(其他辅助方法保持不变,如_formatDuration, _togglePlayPause等) |
|||
// 格式化时间显示 |
|||
String _formatDuration(int milliseconds) { |
|||
final duration = Duration(milliseconds: milliseconds); |
|||
final minutes = duration.inMinutes; |
|||
final seconds = duration.inSeconds % 60; |
|||
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; |
|||
} |
|||
|
|||
// 播放/暂停切换 |
|||
Future<void> _togglePlayPause(AgentController controller) async { |
|||
if (controller.isPlaying.value) { |
|||
await controller.musicPause(); |
|||
} else { |
|||
await controller.musicResume(); |
|||
} |
|||
} |
|||
|
|||
// 播放上一曲 |
|||
Future<void> _playPrevious( |
|||
AgentController controller, List<Map<String, dynamic>> playlist) async { |
|||
// 防止重复点击导致的状态混乱 |
|||
if (controller.isLoadingMusic.value) { |
|||
return; |
|||
} |
|||
|
|||
try { |
|||
// 设置加载状态,防止重复操作 |
|||
controller.isLoadingMusic.value = true; |
|||
|
|||
// 更新播放列表上下文 |
|||
controller.updateCurrentPlaylistContext( |
|||
playlist, controller.currentId.value); |
|||
|
|||
// 执行上一曲操作 |
|||
await controller.playPrevious(); |
|||
} finally { |
|||
// 确保加载状态被重置 |
|||
controller.isLoadingMusic.value = false; |
|||
} |
|||
} |
|||
|
|||
// 播放下一曲 |
|||
Future<void> _playNext( |
|||
AgentController controller, List<Map<String, dynamic>> playlist) async { |
|||
// 防止重复点击导致的状态混乱 |
|||
if (controller.isLoadingMusic.value) { |
|||
return; |
|||
} |
|||
|
|||
try { |
|||
// 设置加载状态,防止重复操作 |
|||
controller.isLoadingMusic.value = true; |
|||
|
|||
// 更新播放列表上下文 |
|||
controller.updateCurrentPlaylistContext( |
|||
playlist, controller.currentId.value); |
|||
|
|||
// 执行下一曲操作 |
|||
await controller.playNext(); |
|||
} finally { |
|||
// 确保加载状态被重置 |
|||
controller.isLoadingMusic.value = false; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
include: ../../analysis_options.yaml |
|||
|
|||
analyzer: |
|||
exclude: |
|||
- "**/*.g.dart" |
|||
- "**/*.freezed.dart" |
|||
- "build/**" |
|||
- ".dart_tool/**" |
|||
language: |
|||
strict-casts: false |
|||
strict-inference: false |
|||
strict-raw-types: false |
|||
@ -0,0 +1,488 @@ |
|||
package com.yunqiinnovation.chat_api |
|||
|
|||
import android.content.Context |
|||
import android.util.Log |
|||
import kotlinx.coroutines.delay |
|||
import com.yunqiinnovation.qq_music.QQMusicSingleton |
|||
/** |
|||
* QQ音乐系统功能处理器 |
|||
* 负责注册QQ音乐相关的MCP函数 |
|||
*/ |
|||
class QQMusicSystemFunctionHandler(private val context: Context? = null) { |
|||
companion object { |
|||
const val TAG = "QQMusicSystemFunctionHandler" |
|||
} |
|||
|
|||
/** |
|||
* 注册所有QQ音乐相关函数到MCPClient |
|||
*/ |
|||
fun registerAllFunctions(client: MCPClient) { |
|||
try { |
|||
// Only register QQ Music search function |
|||
client.registerLocalFunction( |
|||
"qqmusic_search_songs", |
|||
"在QQ音乐中搜索歌曲并播放,支持歌曲名和歌手名", |
|||
mapOf( |
|||
"type" to "object", |
|||
"properties" to mapOf( |
|||
"keyword" to mapOf( |
|||
"type" to "string", |
|||
"description" to "搜索关键词,歌曲名或歌手名" |
|||
) |
|||
), |
|||
"required" to listOf("keyword") |
|||
), |
|||
QQMusicSearchHandler(context) |
|||
) |
|||
|
|||
// QQ音乐播放歌曲 |
|||
// client.registerLocalFunction( |
|||
// "qqmusic_play_songs", |
|||
// "播放QQ音乐歌曲列表", |
|||
// mapOf( |
|||
// "type" to "object", |
|||
// "properties" to mapOf( |
|||
// "songMids" to mapOf( |
|||
// "type" to "array", |
|||
// "description" to "歌曲ID列表", |
|||
// "items" to mapOf("type" to "string") |
|||
// ), |
|||
// "index" to mapOf( |
|||
// "type" to "integer", |
|||
// "description" to "开始播放的歌曲索引,默认为0" |
|||
// ) |
|||
// ), |
|||
// "required" to listOf("songMids") |
|||
// ), |
|||
// QQMusicPlaySongsHandler(context) |
|||
// ) |
|||
|
|||
// QQ音乐播放控制 |
|||
// client.registerLocalFunction( |
|||
// "qqmusic_play", |
|||
// "播放QQ音乐", |
|||
// mapOf( |
|||
// "type" to "object", |
|||
// "properties" to emptyMap<String, Any>(), |
|||
// "required" to emptyList<String>() |
|||
// ), |
|||
// QQMusicPlayHandler(context) |
|||
// ) |
|||
|
|||
client.registerLocalFunction( |
|||
"qqmusic_pause", |
|||
"暂停QQ音乐", |
|||
mapOf( |
|||
"type" to "object", |
|||
"properties" to emptyMap<String, Any>(), |
|||
"required" to emptyList<String>() |
|||
), |
|||
QQMusicPauseHandler(context) |
|||
) |
|||
|
|||
client.registerLocalFunction( |
|||
"qqmusic_resume", |
|||
"恢复播放QQ音乐", |
|||
mapOf( |
|||
"type" to "object", |
|||
"properties" to emptyMap<String, Any>(), |
|||
"required" to emptyList<String>() |
|||
), |
|||
QQMusicResumeHandler(context) |
|||
) |
|||
|
|||
client.registerLocalFunction( |
|||
"qqmusic_stop", |
|||
"停止QQ音乐播放", |
|||
mapOf( |
|||
"type" to "object", |
|||
"properties" to emptyMap<String, Any>(), |
|||
"required" to emptyList<String>() |
|||
), |
|||
QQMusicStopHandler(context) |
|||
) |
|||
|
|||
client.registerLocalFunction( |
|||
"qqmusic_next", |
|||
"QQ音乐下一首", |
|||
mapOf( |
|||
"type" to "object", |
|||
"properties" to emptyMap<String, Any>(), |
|||
"required" to emptyList<String>() |
|||
), |
|||
QQMusicNextHandler(context) |
|||
) |
|||
|
|||
client.registerLocalFunction( |
|||
"qqmusic_previous", |
|||
"QQ音乐上一首", |
|||
mapOf( |
|||
"type" to "object", |
|||
"properties" to emptyMap<String, Any>(), |
|||
"required" to emptyList<String>() |
|||
), |
|||
QQMusicPreviousHandler(context) |
|||
) |
|||
|
|||
// QQ音乐状态查询 |
|||
client.registerLocalFunction( |
|||
"qqmusic_get_playback_state", |
|||
"获取QQ音乐播放状态", |
|||
mapOf( |
|||
"type" to "object", |
|||
"properties" to emptyMap<String, Any>(), |
|||
"required" to emptyList<String>() |
|||
), |
|||
QQMusicGetPlaybackStateHandler(context) |
|||
) |
|||
|
|||
client.registerLocalFunction( |
|||
"qqmusic_get_current_song", |
|||
"获取QQ音乐当前播放歌曲信息", |
|||
mapOf( |
|||
"type" to "object", |
|||
"properties" to emptyMap<String, Any>(), |
|||
"required" to emptyList<String>() |
|||
), |
|||
QQMusicGetCurrentSongHandler(context) |
|||
) |
|||
|
|||
client.registerLocalFunction( |
|||
"qqmusic_get_play_time", |
|||
"获取QQ音乐播放时间信息", |
|||
mapOf( |
|||
"type" to "object", |
|||
"properties" to emptyMap<String, Any>(), |
|||
"required" to emptyList<String>() |
|||
), |
|||
QQMusicGetPlayTimeHandler(context) |
|||
) |
|||
|
|||
Log.d(TAG, "QQ音乐所有功能注册完成") |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "注册QQ音乐功能失败: ${e.message}", e) |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
private class QQMusicLoginHandler(private val context: Context?) : FunctionHandler { |
|||
override suspend fun handle(arguments: Map<String, Any>): String { |
|||
return try { |
|||
val qqMusicSingleton = QQMusicSingleton.getInstance() |
|||
var result = "" |
|||
|
|||
qqMusicSingleton.login { loginResult -> |
|||
result = if (loginResult.isSuccess) { |
|||
val loginData = loginResult.getOrNull() |
|||
"{\"success\": true, \"message\": \"登录成功\", \"data\": {\"openId\": \"${loginData?.openId}\", \"openToken\": \"${loginData?.openToken}\", \"expireTime\": \"${loginData?.expireTime}\"}}" |
|||
} else { |
|||
val error = loginResult.exceptionOrNull() |
|||
"{\"success\": false, \"message\": \"登录失败: ${error?.message}\"}" |
|||
} |
|||
} |
|||
|
|||
// 等待回调完成 |
|||
delay(3000) |
|||
result.ifEmpty { "{\"success\": false, \"message\": \"登录超时\"}" } |
|||
} catch (e: Exception) { |
|||
Log.e("QQMusicLoginHandler", "QQ音乐登录失败", e) |
|||
"{\"success\": false, \"message\": \"登录异常: ${e.message}\"}" |
|||
} |
|||
} |
|||
} |
|||
|
|||
private class QQMusicSearchHandler(private val context: Context?) : FunctionHandler { |
|||
override suspend fun handle(arguments: Map<String, Any>): String { |
|||
return try { |
|||
val keyword = arguments["keyword"] as? String ?: return "{\"success\": false, \"message\": \"缺少搜索关键词\"}" |
|||
val qqMusicSingleton = QQMusicSingleton.getInstance() |
|||
var result = "" |
|||
|
|||
// 直接调用搜索,内部会自动处理登录检查 |
|||
qqMusicSingleton.searchSongs(keyword) { searchResult -> |
|||
result = if (searchResult.isSuccess) { |
|||
val songs = searchResult.getOrNull() ?: emptyList() |
|||
val songsJson = songs.joinToString(",") { song -> |
|||
val songMap = song as Map<String, Any?> |
|||
// 添加调试日志 |
|||
Log.d("QQMusicSearchHandler", "原始歌曲数据: $songMap") |
|||
// 修正字段名映射 |
|||
val mid = songMap["mid"] ?: "unknown" |
|||
val name = songMap["name"] ?: "unknown" |
|||
val artist = songMap["singer"] ?: "unknown" |
|||
val album = songMap["album"] ?: "" |
|||
val pic = songMap["pic"] ?: "" |
|||
val duration = songMap["duration"] ?: 0 |
|||
"{\"mid\": \"$mid\", \"name\": \"$name\", \"artist\": \"$artist\", \"album\": \"$album\", \"duration\":$duration,\"pic\": \"$pic\"}" |
|||
} |
|||
"{\"success\": true, \"message\": \"搜索成功\", \"data\": [$songsJson]}" |
|||
} else { |
|||
val error = searchResult.exceptionOrNull() |
|||
"{\"success\": false, \"message\": \"搜索失败: ${error?.message}\"}" |
|||
} |
|||
} |
|||
|
|||
// 等待回调完成 |
|||
delay(5000) |
|||
result.ifEmpty { "{\"success\": false, \"message\": \"搜索超时\"}" } |
|||
} catch (e: Exception) { |
|||
Log.e("QQMusicSearchHandler", "QQ音乐搜索失败", e) |
|||
"{\"success\": false, \"message\": \"搜索异常: ${e.message}\"}" |
|||
} |
|||
} |
|||
} |
|||
|
|||
private class QQMusicPlaySongsHandler(private val context: Context?) : FunctionHandler { |
|||
override suspend fun handle(arguments: Map<String, Any>): String { |
|||
return try { |
|||
@Suppress("UNCHECKED_CAST") |
|||
val songMids = arguments["songMids"] as? List<String> ?: return "{\"success\": false, \"message\": \"缺少歌曲ID列表\"}" |
|||
val index = (arguments["index"] as? Number)?.toInt() ?: 0 |
|||
val qqMusicSingleton = QQMusicSingleton.getInstance() |
|||
var result = "" |
|||
|
|||
qqMusicSingleton.playSongs(songMids, index) { playResult -> |
|||
result = if (playResult.isSuccess) { |
|||
"{\"success\": true, \"message\": \"开始播放歌曲列表\"}" |
|||
} else { |
|||
val error = playResult.exceptionOrNull() |
|||
"{\"success\": false, \"message\": \"播放失败: ${error?.message}\"}" |
|||
} |
|||
} |
|||
|
|||
// 等待回调完成 |
|||
delay(3000) |
|||
result.ifEmpty { "{\"success\": false, \"message\": \"播放超时\"}" } |
|||
} catch (e: Exception) { |
|||
Log.e("QQMusicPlaySongsHandler", "QQ音乐播放歌曲失败", e) |
|||
"{\"success\": false, \"message\": \"播放异常: ${e.message}\"}" |
|||
} |
|||
} |
|||
} |
|||
|
|||
private class QQMusicPlayHandler(private val context: Context?) : FunctionHandler { |
|||
override suspend fun handle(arguments: Map<String, Any>): String { |
|||
return try { |
|||
val qqMusicSingleton = QQMusicSingleton.getInstance() |
|||
var result = "" |
|||
|
|||
qqMusicSingleton.playMusic { playResult -> |
|||
result = if (playResult.isSuccess) { |
|||
"{\"success\": true, \"message\": \"开始播放\"}" |
|||
} else { |
|||
val error = playResult.exceptionOrNull() |
|||
"{\"success\": false, \"message\": \"播放失败: ${error?.message}\"}" |
|||
} |
|||
} |
|||
|
|||
delay(2000) |
|||
result.ifEmpty { "{\"success\": false, \"message\": \"播放超时\"}" } |
|||
} catch (e: Exception) { |
|||
Log.e("QQMusicPlayHandler", "QQ音乐播放失败", e) |
|||
"{\"success\": false, \"message\": \"播放异常: ${e.message}\"}" |
|||
} |
|||
} |
|||
} |
|||
|
|||
private class QQMusicPauseHandler(private val context: Context?) : FunctionHandler { |
|||
override suspend fun handle(arguments: Map<String, Any>): String { |
|||
return try { |
|||
val qqMusicSingleton = QQMusicSingleton.getInstance() |
|||
var result = "" |
|||
|
|||
qqMusicSingleton.pauseMusic { pauseResult -> |
|||
result = if (pauseResult.isSuccess) { |
|||
"{\"success\": true, \"message\": \"暂停播放\"}" |
|||
} else { |
|||
val error = pauseResult.exceptionOrNull() |
|||
"{\"success\": false, \"message\": \"暂停失败: ${error?.message}\"}" |
|||
} |
|||
} |
|||
|
|||
delay(2000) |
|||
result.ifEmpty { "{\"success\": false, \"message\": \"暂停超时\"}" } |
|||
} catch (e: Exception) { |
|||
Log.e("QQMusicPauseHandler", "QQ音乐暂停失败", e) |
|||
"{\"success\": false, \"message\": \"暂停异常: ${e.message}\"}" |
|||
} |
|||
} |
|||
} |
|||
|
|||
private class QQMusicResumeHandler(private val context: Context?) : FunctionHandler { |
|||
override suspend fun handle(arguments: Map<String, Any>): String { |
|||
return try { |
|||
val qqMusicSingleton = QQMusicSingleton.getInstance() |
|||
var result = "" |
|||
|
|||
qqMusicSingleton.resumeMusic { resumeResult -> |
|||
result = if (resumeResult.isSuccess) { |
|||
"{\"success\": true, \"message\": \"恢复播放\"}" |
|||
} else { |
|||
val error = resumeResult.exceptionOrNull() |
|||
"{\"success\": false, \"message\": \"恢复播放失败: ${error?.message}\"}" |
|||
} |
|||
} |
|||
|
|||
delay(2000) |
|||
result.ifEmpty { "{\"success\": false, \"message\": \"恢复播放超时\"}" } |
|||
} catch (e: Exception) { |
|||
Log.e("QQMusicResumeHandler", "QQ音乐恢复播放失败", e) |
|||
"{\"success\": false, \"message\": \"恢复播放异常: ${e.message}\"}" |
|||
} |
|||
} |
|||
} |
|||
|
|||
private class QQMusicStopHandler(private val context: Context?) : FunctionHandler { |
|||
override suspend fun handle(arguments: Map<String, Any>): String { |
|||
return try { |
|||
val qqMusicSingleton = QQMusicSingleton.getInstance() |
|||
var result = "" |
|||
|
|||
qqMusicSingleton.stopMusic { stopResult -> |
|||
result = if (stopResult.isSuccess) { |
|||
"{\"success\": true, \"message\": \"停止播放\"}" |
|||
} else { |
|||
val error = stopResult.exceptionOrNull() |
|||
"{\"success\": false, \"message\": \"停止失败: ${error?.message}\"}" |
|||
} |
|||
} |
|||
|
|||
delay(2000) |
|||
result.ifEmpty { "{\"success\": false, \"message\": \"停止超时\"}" } |
|||
} catch (e: Exception) { |
|||
Log.e("QQMusicStopHandler", "QQ音乐停止失败", e) |
|||
"{\"success\": false, \"message\": \"停止异常: ${e.message}\"}" |
|||
} |
|||
} |
|||
} |
|||
|
|||
private class QQMusicNextHandler(private val context: Context?) : FunctionHandler { |
|||
override suspend fun handle(arguments: Map<String, Any>): String { |
|||
return try { |
|||
val qqMusicSingleton = QQMusicSingleton.getInstance() |
|||
var result = "" |
|||
|
|||
qqMusicSingleton.skipToNext { nextResult -> |
|||
result = if (nextResult.isSuccess) { |
|||
"{\"success\": true, \"message\": \"切换到下一首\"}" |
|||
} else { |
|||
val error = nextResult.exceptionOrNull() |
|||
"{\"success\": false, \"message\": \"切换失败: ${error?.message}\"}" |
|||
} |
|||
} |
|||
|
|||
delay(2000) |
|||
result.ifEmpty { "{\"success\": false, \"message\": \"切换超时\"}" } |
|||
} catch (e: Exception) { |
|||
Log.e("QQMusicNextHandler", "QQ音乐下一首失败", e) |
|||
"{\"success\": false, \"message\": \"切换异常: ${e.message}\"}" |
|||
} |
|||
} |
|||
} |
|||
|
|||
private class QQMusicPreviousHandler(private val context: Context?) : FunctionHandler { |
|||
override suspend fun handle(arguments: Map<String, Any>): String { |
|||
return try { |
|||
val qqMusicSingleton = QQMusicSingleton.getInstance() |
|||
var result = "" |
|||
|
|||
qqMusicSingleton.skipToPrevious { previousResult -> |
|||
result = if (previousResult.isSuccess) { |
|||
"{\"success\": true, \"message\": \"切换到上一首\"}" |
|||
} else { |
|||
val error = previousResult.exceptionOrNull() |
|||
"{\"success\": false, \"message\": \"切换失败: ${error?.message}\"}" |
|||
} |
|||
} |
|||
|
|||
delay(2000) |
|||
result.ifEmpty { "{\"success\": false, \"message\": \"切换超时\"}" } |
|||
} catch (e: Exception) { |
|||
Log.e("QQMusicPreviousHandler", "QQ音乐上一首失败", e) |
|||
"{\"success\": false, \"message\": \"切换异常: ${e.message}\"}" |
|||
} |
|||
} |
|||
} |
|||
|
|||
private class QQMusicGetPlaybackStateHandler(private val context: Context?) : FunctionHandler { |
|||
override suspend fun handle(arguments: Map<String, Any>): String { |
|||
return try { |
|||
val qqMusicSingleton = QQMusicSingleton.getInstance() |
|||
var result = "" |
|||
|
|||
qqMusicSingleton.getPlaybackState { stateResult -> |
|||
result = if (stateResult.isSuccess) { |
|||
val state = stateResult.getOrNull() ?: emptyMap() |
|||
val stateJson = state.entries.joinToString(",") { (key, value) -> |
|||
"\"$key\": \"$value\"" |
|||
} |
|||
"{\"success\": true, \"message\": \"获取播放状态成功\", \"data\": {$stateJson}}" |
|||
} else { |
|||
val error = stateResult.exceptionOrNull() |
|||
"{\"success\": false, \"message\": \"获取播放状态失败: ${error?.message}\"}" |
|||
} |
|||
} |
|||
|
|||
delay(2000) |
|||
result.ifEmpty { "{\"success\": false, \"message\": \"获取播放状态超时\"}" } |
|||
} catch (e: Exception) { |
|||
Log.e("CahtApi", "获取QQ音乐播放状态失败", e) |
|||
"{\"success\": false, \"message\": \"获取播放状态异常: ${e.message}\"}" |
|||
} |
|||
} |
|||
} |
|||
|
|||
private class QQMusicGetCurrentSongHandler(private val context: Context?) : FunctionHandler { |
|||
override suspend fun handle(arguments: Map<String, Any>): String { |
|||
return try { |
|||
val qqMusicSingleton = QQMusicSingleton.getInstance() |
|||
var result = "" |
|||
|
|||
qqMusicSingleton.getCurrentSong { songResult -> |
|||
result = if (songResult.isSuccess) { |
|||
val song = songResult.getOrNull() ?: emptyMap() |
|||
val songJson = song.entries.joinToString(",") { (key, value) -> |
|||
"\"$key\": \"$value\"" |
|||
} |
|||
"{\"success\": true, \"message\": \"获取当前歌曲成功\", \"data\": {$songJson}}" |
|||
} else { |
|||
val error = songResult.exceptionOrNull() |
|||
"{\"success\": false, \"message\": \"获取当前歌曲失败: ${error?.message}\"}" |
|||
} |
|||
} |
|||
|
|||
delay(2000) |
|||
result.ifEmpty { "{\"success\": false, \"message\": \"获取当前歌曲超时\"}" } |
|||
} catch (e: Exception) { |
|||
Log.e("QQMusicLoginHandler", "获取QQ音乐当前歌曲失败", e) |
|||
"{\"success\": false, \"message\": \"获取当前歌曲异常: ${e.message}\"}" |
|||
} |
|||
} |
|||
} |
|||
|
|||
private class QQMusicGetPlayTimeHandler(private val context: Context?) : FunctionHandler { |
|||
override suspend fun handle(arguments: Map<String, Any>): String { |
|||
return try { |
|||
val qqMusicSingleton = QQMusicSingleton.getInstance() |
|||
var result = "" |
|||
|
|||
qqMusicSingleton.getPlayTime { timeResult -> |
|||
result = if (timeResult.isSuccess) { |
|||
val timeInfo = timeResult.getOrNull() |
|||
"{\"success\": true, \"message\": \"获取播放时间成功\", \"data\": {\"totalTime\": ${timeInfo?.totalTime}, \"currentTime\": ${timeInfo?.currentTime}}}" |
|||
} else { |
|||
val error = timeResult.exceptionOrNull() |
|||
"{\"success\": false, \"message\": \"获取播放时间失败: ${error?.message}\"}" |
|||
} |
|||
} |
|||
|
|||
delay(2000) |
|||
result.ifEmpty { "{\"success\": false, \"message\": \"获取播放时间超时\"}" } |
|||
} catch (e: Exception) { |
|||
Log.e("QQMusicLoginHandler", "获取QQ音乐播放时间失败", e) |
|||
"{\"success\": false, \"message\": \"获取播放时间异常: ${e.message}\"}" |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,467 @@ |
|||
package com.yunqiinnovation.qq_music |
|||
|
|||
import android.app.Activity |
|||
import android.content.Context |
|||
import android.content.SharedPreferences |
|||
import android.os.Handler |
|||
import android.os.Looper |
|||
import android.util.Log |
|||
import com.tencent.qqmusic.third.api.contract.* |
|||
import java.text.SimpleDateFormat |
|||
import java.util.* |
|||
|
|||
/** |
|||
* QQ音乐单例管理器 |
|||
* 提供统一的音乐播放接口,可以被Plugin和MCP Client共同使用 |
|||
*/ |
|||
class QQMusicSingleton private constructor() { |
|||
|
|||
companion object { |
|||
private const val TAG = "QQMusicSingleton" |
|||
private const val PREF_NAME = "qq_music_login" |
|||
private const val KEY_OPEN_ID = "open_id" |
|||
private const val KEY_OPEN_TOKEN = "open_token" |
|||
private const val KEY_EXPIRE_TIME = "expire_time" |
|||
|
|||
@Volatile |
|||
private var INSTANCE: QQMusicSingleton? = null |
|||
|
|||
fun getInstance(): QQMusicSingleton { |
|||
return INSTANCE ?: synchronized(this) { |
|||
INSTANCE ?: QQMusicSingleton().also { INSTANCE = it } |
|||
} |
|||
} |
|||
} |
|||
|
|||
private var activity: Activity? = null |
|||
private var loginManager: QQMusicLoginManager? = null |
|||
private var musicManager: QQMusicManager? = null |
|||
private val handler = Handler(Looper.getMainLooper()) |
|||
private var sharedPreferences: SharedPreferences? = null |
|||
private var currentLoginResult: LoginResult? = null |
|||
|
|||
/** |
|||
* 初始化单例,设置Activity上下文 |
|||
*/ |
|||
fun initialize(activity: Activity) { |
|||
this.activity = activity |
|||
this.sharedPreferences = activity.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) |
|||
loadSavedLoginInfo() |
|||
Log.d(TAG, "QQ音乐单例初始化完成") |
|||
} |
|||
|
|||
/** |
|||
* 检查是否已登录且未过期 |
|||
*/ |
|||
fun isLoggedIn(): Boolean { |
|||
val loginResult = currentLoginResult ?: return false |
|||
|
|||
try { |
|||
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) |
|||
val expireDate = dateFormat.parse(loginResult.expireTime) |
|||
val currentDate = Date() |
|||
|
|||
val isValid = expireDate != null && currentDate.before(expireDate) |
|||
Log.d(TAG, "登录状态检查: ${if (isValid) "有效" else "已过期"}") |
|||
return isValid |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "解析过期时间失败: ${e.message}") |
|||
return false |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取当前登录信息 |
|||
*/ |
|||
fun getCurrentLoginInfo(): LoginResult? { |
|||
return if (isLoggedIn()) currentLoginResult else null |
|||
} |
|||
|
|||
/** |
|||
* 登录接口 |
|||
*/ |
|||
fun login(callback: (Result<LoginResult>) -> Unit) { |
|||
// 先检查是否已经登录且未过期 |
|||
if (isLoggedIn()) { |
|||
Log.d(TAG, "用户已登录且未过期,直接返回登录信息") |
|||
callback(Result.success(currentLoginResult!!)) |
|||
return |
|||
} |
|||
|
|||
val currentActivity = activity |
|||
if (currentActivity == null) { |
|||
callback(Result.failure(Exception("Activity not available"))) |
|||
return |
|||
} |
|||
|
|||
Log.d(TAG, "开始新的登录流程") |
|||
loginManager = QQMusicLoginManager(currentActivity) |
|||
loginManager?.startLogin(object : QQMusicLoginManager.LoginCallback { |
|||
override fun onSuccess(openId: String, openToken: String, expireTime: String) { |
|||
handler.post { |
|||
val result = LoginResult(openId, openToken, expireTime) |
|||
currentLoginResult = result |
|||
saveLoginInfo(result) |
|||
Log.d(TAG, "登录成功并已保存: $result") |
|||
callback(Result.success(result)) |
|||
} |
|||
} |
|||
|
|||
override fun onError(errorCode: String, errorMessage: String) { |
|||
handler.post { |
|||
Log.e(TAG, "登录失败: $errorCode - $errorMessage") |
|||
callback(Result.failure(Exception("$errorCode: $errorMessage"))) |
|||
} |
|||
} |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* 登出 |
|||
*/ |
|||
fun logout() { |
|||
currentLoginResult = null |
|||
clearSavedLoginInfo() |
|||
Log.d(TAG, "用户已登出,清除本地登录信息") |
|||
} |
|||
|
|||
/** |
|||
* 搜索歌曲(带自动登录检查) |
|||
*/ |
|||
fun searchSongs( |
|||
keyword: String, |
|||
searchType: Int = Data.SearchType.SEARCH_TYPE_SONG, |
|||
callback: (Result<List<Map<String, Any?>>>) -> Unit |
|||
) { |
|||
val currentActivity = activity |
|||
if (currentActivity == null) { |
|||
callback(Result.failure(Exception("Activity not available"))) |
|||
return |
|||
} |
|||
|
|||
// 检查登录状态 |
|||
if (!isLoggedIn()) { |
|||
Log.d(TAG, "搜索前检测到未登录,开始自动登录") |
|||
login { loginResult -> |
|||
if (loginResult.isSuccess) { |
|||
Log.d(TAG, "自动登录成功,继续搜索") |
|||
performSearch(keyword, searchType, callback) |
|||
} else { |
|||
callback(Result.failure(Exception("自动登录失败: ${loginResult.exceptionOrNull()?.message}"))) |
|||
} |
|||
} |
|||
} else { |
|||
performSearch(keyword, searchType, callback) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 执行搜索操作 |
|||
*/ |
|||
private fun performSearch( |
|||
keyword: String, |
|||
searchType: Int, |
|||
callback: (Result<List<Map<String, Any?>>>) -> Unit |
|||
) { |
|||
val currentActivity = activity ?: return |
|||
|
|||
ensureMusicManager(currentActivity) |
|||
musicManager?.searchSongs(keyword, searchType, object : QQMusicManager.SearchCallback { |
|||
override fun onSuccess(songs: List<Map<String, Any?>>) { |
|||
handler.post { |
|||
Log.d(TAG, "搜索成功: 找到 ${songs.size} 首歌曲") |
|||
callback(Result.success(songs)) |
|||
} |
|||
} |
|||
|
|||
override fun onError(errorCode: Int, errorMessage: String) { |
|||
handler.post { |
|||
Log.e(TAG, "搜索失败: code=$errorCode, message=$errorMessage") |
|||
// 如果是登录相关错误,清除本地登录信息 |
|||
if (errorMessage.contains("登录") || errorMessage.contains("授权")) { |
|||
Log.d(TAG, "检测到登录相关错误,清除本地登录信息") |
|||
logout() |
|||
} |
|||
callback(Result.failure(Exception("搜索失败: $errorMessage"))) |
|||
} |
|||
} |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* 保存登录信息到本地 |
|||
*/ |
|||
private fun saveLoginInfo(loginResult: LoginResult) { |
|||
sharedPreferences?.edit()?.apply { |
|||
putString(KEY_OPEN_ID, loginResult.openId) |
|||
putString(KEY_OPEN_TOKEN, loginResult.openToken) |
|||
putString(KEY_EXPIRE_TIME, loginResult.expireTime) |
|||
apply() |
|||
} |
|||
Log.d(TAG, "登录信息已保存到本地") |
|||
} |
|||
|
|||
/** |
|||
* 从本地加载登录信息 |
|||
*/ |
|||
private fun loadSavedLoginInfo() { |
|||
val prefs = sharedPreferences ?: return |
|||
|
|||
val openId = prefs.getString(KEY_OPEN_ID, null) |
|||
val openToken = prefs.getString(KEY_OPEN_TOKEN, null) |
|||
val expireTime = prefs.getString(KEY_EXPIRE_TIME, null) |
|||
|
|||
if (openId != null && openToken != null && expireTime != null) { |
|||
currentLoginResult = LoginResult(openId, openToken, expireTime) |
|||
Log.d(TAG, "从本地加载登录信息成功") |
|||
|
|||
// 检查是否过期 |
|||
if (!isLoggedIn()) { |
|||
Log.d(TAG, "本地登录信息已过期,清除") |
|||
clearSavedLoginInfo() |
|||
currentLoginResult = null |
|||
} |
|||
} else { |
|||
Log.d(TAG, "本地无保存的登录信息") |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 清除本地保存的登录信息 |
|||
*/ |
|||
private fun clearSavedLoginInfo() { |
|||
sharedPreferences?.edit()?.apply { |
|||
remove(KEY_OPEN_ID) |
|||
remove(KEY_OPEN_TOKEN) |
|||
remove(KEY_EXPIRE_TIME) |
|||
apply() |
|||
} |
|||
Log.d(TAG, "本地登录信息已清除") |
|||
} |
|||
|
|||
/** |
|||
* 播放歌曲列表 |
|||
*/ |
|||
fun playSongs( |
|||
songMids: List<String>, |
|||
index: Int = 0, |
|||
callback: (Result<Unit>) -> Unit |
|||
) { |
|||
val currentActivity = activity |
|||
if (currentActivity == null) { |
|||
callback(Result.failure(Exception("Activity not available"))) |
|||
return |
|||
} |
|||
|
|||
ensureMusicManager(currentActivity) |
|||
musicManager?.playSongs(songMids, index, object : QQMusicManager.PlayCallback { |
|||
override fun onSuccess() { |
|||
handler.post { |
|||
Log.d(TAG, "播放歌曲列表成功") |
|||
callback(Result.success(Unit)) |
|||
} |
|||
} |
|||
|
|||
override fun onError(errorCode: Int, errorMessage: String) { |
|||
handler.post { |
|||
Log.e(TAG, "播放失败: $errorMessage") |
|||
callback(Result.failure(Exception("播放失败: $errorMessage"))) |
|||
} |
|||
} |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* 播放音乐 |
|||
*/ |
|||
fun playMusic(callback: (Result<Unit>) -> Unit) { |
|||
executePlaybackAction("播放") { musicManager, playCallback -> |
|||
musicManager.playMusic(playCallback) |
|||
}(callback) |
|||
} |
|||
|
|||
/** |
|||
* 暂停音乐 |
|||
*/ |
|||
fun pauseMusic(callback: (Result<Unit>) -> Unit) { |
|||
executePlaybackAction("暂停") { musicManager, playCallback -> |
|||
musicManager.pauseMusic(playCallback) |
|||
}(callback) |
|||
} |
|||
|
|||
/** |
|||
* 恢复播放 |
|||
*/ |
|||
fun resumeMusic(callback: (Result<Unit>) -> Unit) { |
|||
executePlaybackAction("恢复播放") { musicManager, playCallback -> |
|||
musicManager.resumeMusic(playCallback) |
|||
}(callback) |
|||
} |
|||
|
|||
/** |
|||
* 停止音乐 |
|||
*/ |
|||
fun stopMusic(callback: (Result<Unit>) -> Unit) { |
|||
executePlaybackAction("停止") { musicManager, playCallback -> |
|||
musicManager.stopMusic(playCallback) |
|||
}(callback) |
|||
} |
|||
|
|||
/** |
|||
* 下一首 |
|||
*/ |
|||
fun skipToNext(callback: (Result<Unit>) -> Unit) { |
|||
executePlaybackAction("下一首") { musicManager, playCallback -> |
|||
musicManager.skipToNext(playCallback) |
|||
}(callback) |
|||
} |
|||
|
|||
/** |
|||
* 上一首 |
|||
*/ |
|||
fun skipToPrevious(callback: (Result<Unit>) -> Unit) { |
|||
executePlaybackAction("上一首") { musicManager, playCallback -> |
|||
musicManager.skipToPrevious(playCallback) |
|||
}(callback) |
|||
} |
|||
|
|||
/** |
|||
* 获取播放状态 |
|||
*/ |
|||
fun getPlaybackState(callback: (Result<Map<String, Any?>>) -> Unit) { |
|||
executeInfoAction("获取播放状态") { musicManager, infoCallback -> |
|||
musicManager.getPlaybackState(infoCallback) |
|||
}(callback) |
|||
} |
|||
|
|||
/** |
|||
* 获取当前歌曲信息 |
|||
*/ |
|||
fun getCurrentSong(callback: (Result<Map<String, Any?>>) -> Unit) { |
|||
executeInfoAction("获取当前歌曲") { musicManager, infoCallback -> |
|||
musicManager.getCurrentSong(infoCallback) |
|||
}(callback) |
|||
} |
|||
|
|||
/** |
|||
* 获取播放时间 |
|||
*/ |
|||
fun getPlayTime(callback: (Result<PlayTimeInfo>) -> Unit) { |
|||
val currentActivity = activity |
|||
if (currentActivity == null) { |
|||
callback(Result.failure(Exception("Activity not available"))) |
|||
return |
|||
} |
|||
|
|||
ensureMusicManager(currentActivity) |
|||
musicManager?.getPlayTime(object : QQMusicManager.InfoCallback { |
|||
override fun onSuccess(data: Map<String, Any?>) { |
|||
handler.post { |
|||
val totalTime = data["totalTime"] as? Long ?: 0L |
|||
val currentTime = data["currentTime"] as? Long ?: 0L |
|||
val playTimeInfo = PlayTimeInfo(totalTime, currentTime) |
|||
Log.d(TAG, "获取播放时间成功: $playTimeInfo") |
|||
callback(Result.success(playTimeInfo)) |
|||
} |
|||
} |
|||
|
|||
override fun onError(errorCode: Int, errorMessage: String) { |
|||
handler.post { |
|||
Log.e(TAG, "获取播放时间失败: $errorMessage") |
|||
callback(Result.failure(Exception("获取播放时间失败: $errorMessage"))) |
|||
} |
|||
} |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* 清理资源 |
|||
*/ |
|||
fun cleanup() { |
|||
loginManager?.cleanup() |
|||
loginManager = null |
|||
musicManager?.cleanup() |
|||
musicManager = null |
|||
activity = null |
|||
Log.d(TAG, "QQ音乐单例资源清理完成") |
|||
} |
|||
|
|||
// 私有辅助方法 |
|||
private fun ensureMusicManager(activity: Activity) { |
|||
if (musicManager == null) { |
|||
musicManager = QQMusicManager(activity) |
|||
} |
|||
} |
|||
|
|||
private fun executePlaybackAction( |
|||
actionName: String, |
|||
action: (QQMusicManager, QQMusicManager.PlayCallback) -> Unit |
|||
): (callback: (Result<Unit>) -> Unit) -> Unit { |
|||
return lambda@{ callback -> |
|||
val currentActivity = activity |
|||
if (currentActivity == null) { |
|||
callback(Result.failure(Exception("Activity not available"))) |
|||
return@lambda |
|||
} |
|||
|
|||
ensureMusicManager(currentActivity) |
|||
action(musicManager!!, object : QQMusicManager.PlayCallback { |
|||
override fun onSuccess() { |
|||
handler.post { |
|||
Log.d(TAG, "${actionName}成功") |
|||
callback(Result.success(Unit)) |
|||
} |
|||
} |
|||
|
|||
override fun onError(errorCode: Int, errorMessage: String) { |
|||
handler.post { |
|||
Log.e(TAG, "${actionName}失败: $errorMessage") |
|||
callback(Result.failure(Exception("${actionName}失败: $errorMessage"))) |
|||
} |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
private fun executeInfoAction( |
|||
actionName: String, |
|||
action: (QQMusicManager, QQMusicManager.InfoCallback) -> Unit |
|||
): (callback: (Result<Map<String, Any?>>) -> Unit) -> Unit { |
|||
return lambda@{ callback -> |
|||
val currentActivity = activity |
|||
if (currentActivity == null) { |
|||
callback(Result.failure(Exception("Activity not available"))) |
|||
return@lambda |
|||
} |
|||
|
|||
ensureMusicManager(currentActivity) |
|||
action(musicManager!!, object : QQMusicManager.InfoCallback { |
|||
override fun onSuccess(data: Map<String, Any?>) { |
|||
handler.post { |
|||
Log.d(TAG, "${actionName}成功") |
|||
callback(Result.success(data)) |
|||
} |
|||
} |
|||
|
|||
override fun onError(errorCode: Int, errorMessage: String) { |
|||
handler.post { |
|||
Log.e(TAG, "${actionName}失败: $errorMessage") |
|||
callback(Result.failure(Exception("${actionName}失败: $errorMessage"))) |
|||
} |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
// 数据类定义 |
|||
data class LoginResult( |
|||
val openId: String, |
|||
val openToken: String, |
|||
val expireTime: String |
|||
) |
|||
|
|||
data class PlayTimeInfo( |
|||
val totalTime: Long, |
|||
val currentTime: Long |
|||
) |
|||
} |
|||
Loading…
Reference in new issue