Browse Source

上传qq音乐

newdev_shunjiawei
liwei1dao 1 year ago
parent
commit
cd2a4aac5b
  1. 5
      android/build.gradle.kts
  2. 2
      lib/core/translations/language/tl_ph.dart
  3. 2
      lib/core/translations/language/vi_vn.dart
  4. 56
      lib/modules/agent/views/message_bubble.dart
  5. 234
      lib/modules/agent/views/qqmusic_card.dart
  6. 4
      lib/modules/login/controllers/login_controller.dart
  7. 9
      lib/modules/qqmusic/bindings/qqmusic_playlist_binding.dart
  8. 216
      lib/modules/qqmusic/controllers/qqmusic_playlist_controller.dart
  9. 49
      lib/modules/qqmusic/controllers/song_model.dart
  10. 542
      lib/modules/qqmusic/views/qqmusic_playlist_view.dart
  11. 0
      lib/modules/qqmusic_test/bindings/qqmusic_binding.dart
  12. 0
      lib/modules/qqmusic_test/controllers/qqmusic_controller.dart
  13. 0
      lib/modules/qqmusic_test/models/song_model.dart
  14. 0
      lib/modules/qqmusic_test/views/qqmusic_view.dart
  15. 4
      lib/modules/splash/controllers/splash_controller.dart
  16. 10
      lib/routes/app_pages.dart
  17. 12
      local_plugins/chat_api/analysis_options.yaml
  18. 1
      local_plugins/chat_api/android/build.gradle.kts
  19. 14
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt
  20. 488
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt
  21. 110
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt
  22. 24
      local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicManager.kt
  23. 467
      local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt
  24. 382
      local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QqMusicPlugin.kt

5
android/build.gradle.kts

@ -10,8 +10,11 @@ allprojects {
} }
maven { url = uri("https://maven.pkg.jetbrains.space/public/p/ktor/eap") } maven { url = uri("https://maven.pkg.jetbrains.space/public/p/ktor/eap") }
// 修改为绝对路径,避免相对路径计算错误
flatDir {
dirs(file("${rootProject.projectDir}/../local_plugins/qq_music/android/libs"))
}
} }
} }
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()

2
lib/core/translations/language/tl_ph.dart

@ -768,7 +768,7 @@ const Map<String, String> tlPH = {
'pleaseGrantNecessaryPermissions': 'pleaseGrantNecessaryPermissions':
'Pakibigay ang mga kinakailangang pahintulot', 'Pakibigay ang mga kinakailangang pahintulot',
'checkFailedRetry': 'Nabigo ang pagsuri, pakisubukang muli', 'checkFailedRetry': 'Nabigo ang pagsuri, pakisubukang muli',
'pleaseConnectDeepSoundDevice': 'Pakikonekta ang isang DeepSound device', 'pleaseConnectDeepSoundDevice': 'Pakikonekta ang isang Voitrans device',
'verificationFailedRetry': 'Nabigo ang pag-verify, pakisubukang muli', 'verificationFailedRetry': 'Nabigo ang pag-verify, pakisubukang muli',
'invalidDeviceAddress': 'Hindi wastong address ng device', 'invalidDeviceAddress': 'Hindi wastong address ng device',
'bleDeviceAssociationFailed': 'Nabigo ang pag-uugnay ng BLE device', 'bleDeviceAssociationFailed': 'Nabigo ang pag-uugnay ng BLE device',

2
lib/core/translations/language/vi_vn.dart

@ -744,7 +744,7 @@ const Map<String, String> viVN = {
'retryAuthorization': 'Ủy quyền lại', 'retryAuthorization': 'Ủy quyền lại',
'pleaseGrantNecessaryPermissions': 'Vui lòng cấp các quyền cần thiết', 'pleaseGrantNecessaryPermissions': 'Vui lòng cấp các quyền cần thiết',
'checkFailedRetry': 'Kiểm tra thất bại, vui lòng thử lại', 'checkFailedRetry': 'Kiểm tra thất bại, vui lòng thử lại',
'pleaseConnectDeepSoundDevice': 'Vui lòng kết nối thiết bị DeepSound', 'pleaseConnectDeepSoundDevice': 'Vui lòng kết nối thiết bị Voitrans',
'verificationFailedRetry': 'Xác minh thất bại, vui lòng thử lại', 'verificationFailedRetry': 'Xác minh thất bại, vui lòng thử lại',
'invalidDeviceAddress': 'Địa chỉ thiết bị không hợp lệ', 'invalidDeviceAddress': 'Địa chỉ thiết bị không hợp lệ',
'bleDeviceAssociationFailed': 'Liên kết thiết bị BLE thất bại', 'bleDeviceAssociationFailed': 'Liên kết thiết bị BLE thất bại',

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

@ -1,3 +1,5 @@
import 'package:voitrans/modules/agent/views/qqmusic_card.dart';
import '../../../modules/agent/views/navigation_card.dart'; import '../../../modules/agent/views/navigation_card.dart';
import 'weather_card.dart'; import 'weather_card.dart';
import 'music_card.dart'; import 'music_card.dart';
@ -224,30 +226,42 @@ class MessageBubble extends GetView<AgentController> {
// 判断消息内容,显示特殊卡片 // 判断消息内容,显示特殊卡片
if (!isUser && meta != null) { if (!isUser && meta != null) {
// 判断消息类型,显示不同的卡片 // 判断消息类型,显示不同的卡片
if (meta!.keys.contains('music_play')) { // if (meta!.keys.contains('music_play')) {
final card_music = // final card_music =
meta!["music_play"]['card_music'] as Map<String, dynamic>?; // meta!["music_play"]['card_music'] as Map<String, dynamic>?;
if (card_music != null) { // if (card_music != null) {
final musicId = card_music['id']; // final musicId = card_music['id'];
final musicName = card_music['name'] as String? ?? '未知歌曲'; // final musicName = card_music['name'] as String? ?? '未知歌曲';
final musicUrl = card_music['url'] as String? ?? ''; // final musicUrl = card_music['url'] as String? ?? '';
final musicSinger = card_music['sgener'] as String? ?? '未知歌手'; // final musicSinger = card_music['sgener'] as String? ?? '未知歌手';
final musicImage = card_music['image'] as String? ?? ''; // final musicImage = card_music['image'] as String? ?? '';
final ts = card_music['ts'] as int? ?? 0; // final ts = card_music['ts'] as int? ?? 0;
print("music_play card_music: $card_music"); // //音乐卡片
//音乐卡片 // return buildMusicItem(context, musicId, musicName, musicUrl,
return buildMusicItem(context, musicId, musicName, musicUrl, // musicSinger, musicImage, ts);
musicSinger, musicImage, ts); // }
} // } else if (meta!.keys.contains('music_playlist')) {
} else if (meta!.keys.contains('music_playlist')) { // final card_musiclist =
final card_musiclist = // meta!["music_playlist"]['card_musiclist'] as Map<String, dynamic>?;
meta!["music_playlist"]['card_musiclist'] as Map<String, dynamic>?; // if (card_musiclist != null) {
if (card_musiclist != null) { // final rawList = card_musiclist['musics'] as List? ?? [];
final rawList = card_musiclist['musics'] as List? ?? []; // final playlist =
// rawList.map((e) => Map<String, dynamic>.from(e)).toList();
// //音乐播放列表
// return buildMusicContent(context, playlist);
// }
// } else
if (meta!.keys.contains('qqmusic_search_songs')) {
final qqmusic_search_songs =
meta!['qqmusic_search_songs'] as Map<String, dynamic>;
final data = qqmusic_search_songs['data'];
if (data != null && data is List) {
//qq音乐
final rawList = data as List? ?? [];
final playlist = final playlist =
rawList.map((e) => Map<String, dynamic>.from(e)).toList(); rawList.map((e) => Map<String, dynamic>.from(e)).toList();
//音乐播放列表 //音乐播放列表
return buildMusicContent(context, playlist); return buildQQMusicContent(context, playlist);
} }
} else if (meta!.keys.contains('maps_weather')) { } else if (meta!.keys.contains('maps_weather')) {
final maps_weather = meta!['maps_weather'] as Map<String, dynamic>; final maps_weather = meta!['maps_weather'] as Map<String, dynamic>;

234
lib/modules/agent/views/qqmusic_card.dart

@ -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,
),
),
),
),
],
),
],
);
}

4
lib/modules/login/controllers/login_controller.dart

@ -236,8 +236,8 @@ class LoginController extends GetxController {
@override @override
void onClose() { void onClose() {
contactController.dispose(); // contactController.dispose();
verificationCodeController.dispose(); // verificationCodeController.dispose();
super.onClose(); super.onClose();
} }

9
lib/modules/qqmusic/bindings/qqmusic_playlist_binding.dart

@ -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());
}
}

216
lib/modules/qqmusic/controllers/qqmusic_playlist_controller.dart

@ -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();
}
}

49
lib/modules/qqmusic/controllers/song_model.dart

@ -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')}';
}
}

542
lib/modules/qqmusic/views/qqmusic_playlist_view.dart

@ -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
lib/modules/qqmusic/bindings/qqmusic_binding.dart → lib/modules/qqmusic_test/bindings/qqmusic_binding.dart

0
lib/modules/qqmusic/controllers/qqmusic_controller.dart → lib/modules/qqmusic_test/controllers/qqmusic_controller.dart

0
lib/modules/qqmusic/models/song_model.dart → lib/modules/qqmusic_test/models/song_model.dart

0
lib/modules/qqmusic/views/qqmusic_view.dart → lib/modules/qqmusic_test/views/qqmusic_view.dart

4
lib/modules/splash/controllers/splash_controller.dart

@ -151,9 +151,9 @@ class SplashController extends GetxController {
} }
if (isLoggedIn) { if (isLoggedIn) {
await getAppConfig(); await getAppConfig();
Get.offAllNamed(Routes.qqmusic); Get.offAllNamed(Routes.pairing);
} else { } else {
Get.offAllNamed(Routes.qqmusic); // 跳转到登录页 Get.offAllNamed(Routes.login); // 跳转到登录页
} }
} }

10
lib/routes/app_pages.dart

@ -1,7 +1,9 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:qq_music/qq_music.dart'; import 'package:qq_music/qq_music.dart';
import 'package:voitrans/modules/qqmusic/bindings/qqmusic_binding.dart'; import 'package:voitrans/modules/qqmusic/bindings/qqmusic_playlist_binding.dart';
import 'package:voitrans/modules/qqmusic/views/qqmusic_view.dart'; import 'package:voitrans/modules/qqmusic/views/qqmusic_playlist_view.dart';
import 'package:voitrans/modules/qqmusic_test/bindings/qqmusic_binding.dart';
import 'package:voitrans/modules/qqmusic_test/views/qqmusic_view.dart';
import '/modules/ble/bindings/ble_binding.dart'; import '/modules/ble/bindings/ble_binding.dart';
import '../modules/ble/views/ble_view.dart'; import '../modules/ble/views/ble_view.dart';
@ -280,8 +282,8 @@ abstract class AppPages {
), ),
GetPage( GetPage(
name: Routes.qqmusic, name: Routes.qqmusic,
page: () => const QQMusicView(), page: () => const QqmusicPlaylistView(),
binding: QQMusicBinding(), binding: QQMusicPlaylistBinding(),
), ),
]; ];
} }

12
local_plugins/chat_api/analysis_options.yaml

@ -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

1
local_plugins/chat_api/android/build.gradle.kts

@ -60,4 +60,5 @@ dependencies {
add("compileOnly", project(":location_service")) add("compileOnly", project(":location_service"))
add("compileOnly", project(":qq_music"))
} }

14
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt

@ -54,6 +54,7 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
init { init {
initializeSystemFunctions() initializeSystemFunctions()
initializeQQMusiceFunctions()
} }
/** /**
@ -67,7 +68,17 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
Log.w(TAG, "Failed to initialize system functions", e) Log.w(TAG, "Failed to initialize system functions", e)
} }
} }
/**
* 初始化音乐函数
*/
private fun initializeQQMusiceFunctions() {
try {
val handler = QQMusicSystemFunctionHandler(context)
handler.registerAllFunctions(this)
} catch (e: Exception) {
Log.w(TAG, "Failed to initialize system functions", e)
}
}
/** /**
* 连接到SSE服务器 * 连接到SSE服务器
* 直接接收完整的JSON配置字符串 * 直接接收完整的JSON配置字符串
@ -372,5 +383,4 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
return list return list
} }
} }

488
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt

@ -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}\"}"
}
}
}

110
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt

@ -153,63 +153,63 @@ class SystemFunctionHandler(private val context: Context? = null) {
handler = GetCurrentLocationHandler(context) handler = GetCurrentLocationHandler(context)
) )
// 注册媒体播放功能 // // 注册媒体播放功能
client.registerLocalFunction( // client.registerLocalFunction(
name = "media_play", // name = "media_play",
description = "播放媒体", // description = "播放媒体",
parameters = mapOf( // parameters = mapOf(
"type" to "object", // "type" to "object",
"properties" to emptyMap<String, Any>(), // "properties" to emptyMap<String, Any>(),
"required" to emptyList<String>() // "required" to emptyList<String>()
), // ),
handler = MediaPlayHandler(context) // handler = MediaPlayHandler(context)
) // )
// 注册媒体暂停功能 // // 注册媒体暂停功能
client.registerLocalFunction( // client.registerLocalFunction(
name = "media_pause", // name = "media_pause",
description = "暂停媒体播放", // description = "暂停媒体播放",
parameters = mapOf( // parameters = mapOf(
"type" to "object", // "type" to "object",
"properties" to emptyMap<String, Any>(), // "properties" to emptyMap<String, Any>(),
"required" to emptyList<String>() // "required" to emptyList<String>()
), // ),
handler = MediaPauseHandler(context) // handler = MediaPauseHandler(context)
) // )
// 注册媒体暂停功能 // // 注册媒体暂停功能
client.registerLocalFunction( // client.registerLocalFunction(
name = "media_close", // name = "media_close",
description = "关闭媒体播放", // description = "关闭媒体播放",
parameters = mapOf( // parameters = mapOf(
"type" to "object", // "type" to "object",
"properties" to emptyMap<String, Any>(), // "properties" to emptyMap<String, Any>(),
"required" to emptyList<String>() // "required" to emptyList<String>()
), // ),
handler = MediaCloseHandler(context) // handler = MediaCloseHandler(context)
) // )
// 注册媒体上一首功能 // // 注册媒体上一首功能
client.registerLocalFunction( // client.registerLocalFunction(
name = "media_previous", // name = "media_previous",
description = "播放上一首", // description = "播放上一首",
parameters = mapOf( // parameters = mapOf(
"type" to "object", // "type" to "object",
"properties" to emptyMap<String, Any>(), // "properties" to emptyMap<String, Any>(),
"required" to emptyList<String>() // "required" to emptyList<String>()
), // ),
handler = MediaPreviousHandler(context) // handler = MediaPreviousHandler(context)
) // )
// 注册媒体下一首功能 // // 注册媒体下一首功能
client.registerLocalFunction( // client.registerLocalFunction(
name = "media_next", // name = "media_next",
description = "播放下一首", // description = "播放下一首",
parameters = mapOf( // parameters = mapOf(
"type" to "object", // "type" to "object",
"properties" to emptyMap<String, Any>(), // "properties" to emptyMap<String, Any>(),
"required" to emptyList<String>() // "required" to emptyList<String>()
), // ),
handler = MediaNextHandler(context) // handler = MediaNextHandler(context)
) // )
// 注册打开录音机功能 // 注册打开录音机功能
client.registerLocalFunction( client.registerLocalFunction(

24
local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicManager.kt

@ -48,8 +48,10 @@ class QQMusicManager(private val activity: Activity) {
// 初始化SDK // 初始化SDK
CommonCmd.init(Config.BIND_PLATFORM) CommonCmd.init(Config.BIND_PLATFORM)
// 发送hi消息 // 延迟发送hi消息,确保Binder连接稳定
Handler(Looper.getMainLooper()).postDelayed({
sayHi() sayHi()
}, 500) // 延迟500ms
} }
override fun onServiceDisconnected(name: ComponentName?) { override fun onServiceDisconnected(name: ComponentName?) {
@ -64,14 +66,32 @@ class QQMusicManager(private val activity: Activity) {
} }
private fun sayHi() { private fun sayHi() {
if (!isBindQQMusicService || qqmusicApi == null) return if (!isBindQQMusicService || qqmusicApi == null) {
Log.w(TAG, "QQ音乐服务未连接,无法发送hi消息")
return
}
try {
val bundle = Bundle() val bundle = Bundle()
bundle.putInt(Keys.API_PARAM_KEY_SDK_VERSION, CommonCmd.SDK_VERSION) bundle.putInt(Keys.API_PARAM_KEY_SDK_VERSION, CommonCmd.SDK_VERSION)
bundle.putString(Keys.API_PARAM_KEY_PLATFORM_TYPE, Config.BIND_PLATFORM) bundle.putString(Keys.API_PARAM_KEY_PLATFORM_TYPE, Config.BIND_PLATFORM)
val result = qqmusicApi?.execute("hi", bundle) val result = qqmusicApi?.execute("hi", bundle)
Log.d(TAG, "sayHi ret:" + result?.getInt(Keys.API_RETURN_KEY_CODE)) Log.d(TAG, "sayHi ret:" + result?.getInt(Keys.API_RETURN_KEY_CODE))
} catch (e: Exception) {
Log.e(TAG, "发送hi消息失败", e)
// 如果是DeadObjectException,尝试重新绑定服务
if (e is android.os.DeadObjectException) {
Log.w(TAG, "检测到DeadObjectException,重新绑定服务")
qqmusicApi = null
isBindQQMusicService = false
// 延迟重新绑定
Handler(Looper.getMainLooper()).postDelayed({
bindQQMusicApiService()
}, 1000)
}
}
} }
private fun bindQQMusicApiService() { private fun bindQQMusicApiService() {

467
local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt

@ -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
)
}

382
local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QqMusicPlugin.kt

@ -1,8 +1,5 @@
package com.yunqiinnovation.qq_music package com.yunqiinnovation.qq_music
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.app.Activity import android.app.Activity
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityAware
@ -11,15 +8,13 @@ import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.MethodChannel.Result
// 添加这行导入
import com.tencent.qqmusic.third.api.contract.* import com.tencent.qqmusic.third.api.contract.*
/** QqMusicPlugin */ /** QqMusicPlugin */
class QqMusicPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { class QqMusicPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
private lateinit var channel: MethodChannel private lateinit var channel: MethodChannel
private var activity: Activity? = null private var activity: Activity? = null
private var loginManager: QQMusicLoginManager? = null private val qqMusicSingleton = QQMusicSingleton.getInstance()
private var musicManager: QQMusicManager? = null
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.qq_music") channel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.qq_music")
@ -29,93 +24,23 @@ class QqMusicPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
override fun onMethodCall(call: MethodCall, result: Result) { override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) { when (call.method) {
"login" -> { "login" -> {
handleLogin(result) qqMusicSingleton.login { kotlinResult ->
} kotlinResult.fold(
"searchSongs" -> { onSuccess = { loginResult ->
handleSearchSongs(call, result) result.success(mapOf(
}
"playSongs" -> {
handlePlaySongs(call, result)
}
"playMusic" -> {
handlePlayMusic(result)
}
"pauseMusic" -> {
handlePauseMusic(result)
}
"resumeMusic" -> {
handleResumeMusic(result)
}
"stopMusic" -> {
handleStopMusic(result)
}
"skipToNext" -> {
handleSkipToNext(result)
}
"skipToPrevious" -> {
handleSkipToPrevious(result)
}
"getPlaybackState" -> {
handleGetPlaybackState(result)
}
"getCurrentSong" -> {
handleGetCurrentSong(result)
}
"getPlayTime" -> {
handleGetPlayTime(result)
}
"getPlatformVersion" -> {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
}
else -> {
result.notImplemented()
}
}
}
private fun handleLogin(result: Result) {
val currentActivity = activity
if (currentActivity == null) {
result.error("NO_ACTIVITY", "Activity not available", null)
return
}
// 创建登录管理器
loginManager = QQMusicLoginManager(currentActivity)
// 开始登录
loginManager?.startLogin(object : QQMusicLoginManager.LoginCallback {
override fun onSuccess(openId: String, openToken: String, expireTime: String) {
// 确保在主线程中执行回调
Handler(Looper.getMainLooper()).post {
val loginResult = mapOf(
"success" to true, "success" to true,
"openId" to openId, "openId" to loginResult.openId,
"openToken" to openToken, "openToken" to loginResult.openToken,
"expireTime" to expireTime "expireTime" to loginResult.expireTime
) ))
Log.d("QqMusicPlugin", "登录成功回调: $loginResult") },
result.success(loginResult) onFailure = { error ->
} result.error("LOGIN_FAILED", error.message, null)
}
override fun onError(errorCode: String, errorMessage: String) {
// 确保在主线程中执行回调
Handler(Looper.getMainLooper()).post {
Log.e("QqMusicPlugin", "登录失败回调: $errorCode - $errorMessage")
result.error(errorCode, errorMessage, null)
}
} }
}) )
} }
private fun handleSearchSongs(call: MethodCall, result: Result) {
val currentActivity = activity
if (currentActivity == null) {
result.error("NO_ACTIVITY", "Activity not available", null)
return
} }
"searchSongs" -> {
val keyword = call.argument<String>("keyword") val keyword = call.argument<String>("keyword")
val searchType = call.argument<Int>("searchType") ?: Data.SearchType.SEARCH_TYPE_SONG val searchType = call.argument<Int>("searchType") ?: Data.SearchType.SEARCH_TYPE_SONG
@ -124,38 +49,21 @@ class QqMusicPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
return return
} }
Log.d("QqMusicPlugin", "开始搜索: keyword=$keyword, searchType=$searchType") qqMusicSingleton.searchSongs(keyword, searchType) { kotlinResult ->
kotlinResult.fold(
ensureMusicManager(currentActivity) onSuccess = { songs ->
musicManager?.searchSongs(keyword, searchType, object : QQMusicManager.SearchCallback {
override fun onSuccess(songs: List<Map<String, Any?>>) {
// 确保在主线程中执行回调
Handler(Looper.getMainLooper()).post {
Log.d("QqMusicPlugin", "搜索成功: 找到 ${songs.size} 首歌曲")
result.success(mapOf( result.success(mapOf(
"success" to true, "success" to true,
"songs" to songs "songs" to songs
)) ))
},
onFailure = { error ->
result.error("SEARCH_FAILED", error.message, null)
} }
)
} }
override fun onError(errorCode: Int, errorMessage: String) {
// 确保在主线程中执行回调
Handler(Looper.getMainLooper()).post {
Log.e("QqMusicPlugin", "搜索失败: code=$errorCode, message=$errorMessage")
result.error("SEARCH_FAILED", errorMessage, null)
}
}
})
} }
"playSongs" -> {
private fun handlePlaySongs(call: MethodCall, result: Result) {
val currentActivity = activity
if (currentActivity == null) {
result.error("NO_ACTIVITY", "Activity not available", null)
return
}
val songMids = call.argument<List<String>>("songMids") val songMids = call.argument<List<String>>("songMids")
val index = call.argument<Int>("index") ?: 0 val index = call.argument<Int>("index") ?: 0
@ -164,215 +72,127 @@ class QqMusicPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
return return
} }
ensureMusicManager(currentActivity) qqMusicSingleton.playSongs(songMids, index) { kotlinResult ->
musicManager?.playSongs(songMids, index, object : QQMusicManager.PlayCallback { kotlinResult.fold(
override fun onSuccess() { onSuccess = {
result.success(mapOf("success" to true))
}
override fun onError(errorCode: Int, errorMessage: String) {
result.error("PLAY_FAILED", errorMessage, null)
}
})
}
private fun handlePlayMusic(result: Result) {
val currentActivity = activity
if (currentActivity == null) {
result.error("NO_ACTIVITY", "Activity not available", null)
return
}
ensureMusicManager(currentActivity)
musicManager?.playMusic(object : QQMusicManager.PlayCallback {
override fun onSuccess() {
result.success(mapOf("success" to true))
}
override fun onError(errorCode: Int, errorMessage: String) {
result.error("PLAY_FAILED", errorMessage, null)
}
})
}
private fun handlePauseMusic(result: Result) {
val currentActivity = activity
if (currentActivity == null) {
result.error("NO_ACTIVITY", "Activity not available", null)
return
}
ensureMusicManager(currentActivity)
musicManager?.pauseMusic(object : QQMusicManager.PlayCallback {
override fun onSuccess() {
result.success(mapOf("success" to true))
}
override fun onError(errorCode: Int, errorMessage: String) {
result.error("PAUSE_FAILED", errorMessage, null)
}
})
}
private fun handleResumeMusic(result: Result) {
val currentActivity = activity
if (currentActivity == null) {
result.error("NO_ACTIVITY", "Activity not available", null)
return
}
ensureMusicManager(currentActivity)
musicManager?.resumeMusic(object : QQMusicManager.PlayCallback {
override fun onSuccess() {
result.success(mapOf("success" to true)) result.success(mapOf("success" to true))
},
onFailure = { error ->
result.error("PLAY_FAILED", error.message, null)
} }
)
override fun onError(errorCode: Int, errorMessage: String) {
result.error("RESUME_FAILED", errorMessage, null)
} }
})
} }
"playMusic" -> {
private fun handleStopMusic(result: Result) { qqMusicSingleton.playMusic { kotlinResult ->
val currentActivity = activity kotlinResult.fold(
if (currentActivity == null) { onSuccess = { result.success(mapOf("success" to true)) },
result.error("NO_ACTIVITY", "Activity not available", null) onFailure = { error -> result.error("PLAY_FAILED", error.message, null) }
return )
} }
ensureMusicManager(currentActivity)
musicManager?.stopMusic(object : QQMusicManager.PlayCallback {
override fun onSuccess() {
result.success(mapOf("success" to true))
} }
"pauseMusic" -> {
override fun onError(errorCode: Int, errorMessage: String) { qqMusicSingleton.pauseMusic { kotlinResult ->
result.error("STOP_FAILED", errorMessage, null) kotlinResult.fold(
onSuccess = { result.success(mapOf("success" to true)) },
onFailure = { error -> result.error("PAUSE_FAILED", error.message, null) }
)
} }
})
} }
"resumeMusic" -> {
private fun handleSkipToNext(result: Result) { qqMusicSingleton.resumeMusic { kotlinResult ->
val currentActivity = activity kotlinResult.fold(
if (currentActivity == null) { onSuccess = { result.success(mapOf("success" to true)) },
result.error("NO_ACTIVITY", "Activity not available", null) onFailure = { error -> result.error("RESUME_FAILED", error.message, null) }
return )
} }
ensureMusicManager(currentActivity)
musicManager?.skipToNext(object : QQMusicManager.PlayCallback {
override fun onSuccess() {
result.success(mapOf("success" to true))
} }
"stopMusic" -> {
override fun onError(errorCode: Int, errorMessage: String) { qqMusicSingleton.stopMusic { kotlinResult ->
result.error("SKIP_FAILED", errorMessage, null) kotlinResult.fold(
onSuccess = { result.success(mapOf("success" to true)) },
onFailure = { error -> result.error("STOP_FAILED", error.message, null) }
)
} }
})
} }
"skipToNext" -> {
private fun handleSkipToPrevious(result: Result) { qqMusicSingleton.skipToNext { kotlinResult ->
val currentActivity = activity kotlinResult.fold(
if (currentActivity == null) { onSuccess = { result.success(mapOf("success" to true)) },
result.error("NO_ACTIVITY", "Activity not available", null) onFailure = { error -> result.error("SKIP_FAILED", error.message, null) }
return )
} }
ensureMusicManager(currentActivity)
musicManager?.skipToPrevious(object : QQMusicManager.PlayCallback {
override fun onSuccess() {
result.success(mapOf("success" to true))
} }
"skipToPrevious" -> {
override fun onError(errorCode: Int, errorMessage: String) { qqMusicSingleton.skipToPrevious { kotlinResult ->
result.error("SKIP_FAILED", errorMessage, null) kotlinResult.fold(
} onSuccess = { result.success(mapOf("success" to true)) },
}) onFailure = { error -> result.error("SKIP_FAILED", error.message, null) }
)
} }
private fun handleGetPlaybackState(result: Result) {
val currentActivity = activity
if (currentActivity == null) {
result.error("NO_ACTIVITY", "Activity not available", null)
return
} }
"getPlaybackState" -> {
ensureMusicManager(currentActivity) qqMusicSingleton.getPlaybackState { kotlinResult ->
musicManager?.getPlaybackState(object : QQMusicManager.InfoCallback { kotlinResult.fold(
override fun onSuccess(data: Map<String, Any?>) { onSuccess = { data ->
result.success(mapOf( result.success(mapOf(
"success" to true, "success" to true,
"state" to data["state"] "state" to data["state"]
)) ))
},
onFailure = { error ->
result.error("GET_STATE_FAILED", error.message, null)
} }
)
override fun onError(errorCode: Int, errorMessage: String) {
result.error("GET_STATE_FAILED", errorMessage, null)
}
})
} }
private fun handleGetCurrentSong(result: Result) {
val currentActivity = activity
if (currentActivity == null) {
result.error("NO_ACTIVITY", "Activity not available", null)
return
} }
"getCurrentSong" -> {
ensureMusicManager(currentActivity) qqMusicSingleton.getCurrentSong { kotlinResult ->
musicManager?.getCurrentSong(object : QQMusicManager.InfoCallback { kotlinResult.fold(
override fun onSuccess(data: Map<String, Any?>) { onSuccess = { data ->
result.success(mapOf( result.success(mapOf(
"success" to true, "success" to true,
"song" to data "song" to data
)) ))
},
onFailure = { error ->
result.error("GET_SONG_FAILED", error.message, null)
} }
)
override fun onError(errorCode: Int, errorMessage: String) {
result.error("GET_SONG_FAILED", errorMessage, null)
} }
})
} }
"getPlayTime" -> {
private fun handleGetPlayTime(result: Result) { qqMusicSingleton.getPlayTime { kotlinResult ->
val currentActivity = activity kotlinResult.fold(
if (currentActivity == null) { onSuccess = { playTimeInfo ->
result.error("NO_ACTIVITY", "Activity not available", null)
return
}
ensureMusicManager(currentActivity)
musicManager?.getPlayTime(object : QQMusicManager.InfoCallback {
override fun onSuccess(data: Map<String, Any?>) {
result.success(mapOf( result.success(mapOf(
"success" to true, "success" to true,
"totalTime" to data["totalTime"], "totalTime" to playTimeInfo.totalTime,
"currentTime" to data["currentTime"] "currentTime" to playTimeInfo.currentTime
)) ))
},
onFailure = { error ->
result.error("GET_TIME_FAILED", error.message, null)
} }
)
override fun onError(errorCode: Int, errorMessage: String) {
result.error("GET_TIME_FAILED", errorMessage, null)
} }
})
} }
"getPlatformVersion" -> {
private fun ensureMusicManager(activity: Activity) { result.success("Android ${android.os.Build.VERSION.RELEASE}")
if (musicManager == null) { }
musicManager = QQMusicManager(activity) else -> {
result.notImplemented()
}
} }
} }
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null) channel.setMethodCallHandler(null)
loginManager?.cleanup()
loginManager = null
musicManager?.cleanup()
musicManager = null
} }
override fun onAttachedToActivity(binding: ActivityPluginBinding) { override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity activity = binding.activity
qqMusicSingleton.initialize(activity!!)
} }
override fun onDetachedFromActivityForConfigChanges() { override fun onDetachedFromActivityForConfigChanges() {
@ -381,13 +201,11 @@ class QqMusicPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity activity = binding.activity
qqMusicSingleton.initialize(activity!!)
} }
override fun onDetachedFromActivity() { override fun onDetachedFromActivity() {
activity = null activity = null
loginManager?.cleanup() qqMusicSingleton.cleanup()
loginManager = null
musicManager?.cleanup()
musicManager = null
} }
} }

Loading…
Cancel
Save