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