From cd2a4aac5bf2700cff02a22f1b3a4b2d06df9b7e Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Fri, 1 Aug 2025 10:49:51 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0qq=E9=9F=B3=E4=B9=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/build.gradle.kts | 9 +- lib/core/translations/language/tl_ph.dart | 2 +- lib/core/translations/language/vi_vn.dart | 2 +- lib/modules/agent/views/message_bubble.dart | 56 +- lib/modules/agent/views/qqmusic_card.dart | 234 ++++++++ .../login/controllers/login_controller.dart | 4 +- .../bindings/qqmusic_playlist_binding.dart | 9 + .../qqmusic_playlist_controller.dart | 216 +++++++ .../qqmusic/controllers/song_model.dart | 49 ++ .../qqmusic/views/qqmusic_playlist_view.dart | 542 ++++++++++++++++++ .../bindings/qqmusic_binding.dart | 0 .../controllers/qqmusic_controller.dart | 24 +- .../models/song_model.dart | 0 .../views/qqmusic_view.dart | 0 .../splash/controllers/splash_controller.dart | 4 +- lib/routes/app_pages.dart | 10 +- local_plugins/chat_api/analysis_options.yaml | 12 + .../chat_api/android/build.gradle.kts | 1 + .../com/yunqiinnovation/chat_api/MCPClient.kt | 14 +- .../chat_api/QQMusicSystemFunctionHandler.kt | 488 ++++++++++++++++ .../chat_api/SystemFunctionHandler.kt | 110 ++-- .../qq_music/QQMusicManager.kt | 38 +- .../qq_music/QQMusicSingleton.kt | 467 +++++++++++++++ .../yunqiinnovation/qq_music/QqMusicPlugin.kt | 450 +++++---------- 24 files changed, 2313 insertions(+), 428 deletions(-) create mode 100644 lib/modules/agent/views/qqmusic_card.dart create mode 100644 lib/modules/qqmusic/bindings/qqmusic_playlist_binding.dart create mode 100644 lib/modules/qqmusic/controllers/qqmusic_playlist_controller.dart create mode 100644 lib/modules/qqmusic/controllers/song_model.dart create mode 100644 lib/modules/qqmusic/views/qqmusic_playlist_view.dart rename lib/modules/{qqmusic => qqmusic_test}/bindings/qqmusic_binding.dart (100%) rename lib/modules/{qqmusic => qqmusic_test}/controllers/qqmusic_controller.dart (99%) rename lib/modules/{qqmusic => qqmusic_test}/models/song_model.dart (100%) rename lib/modules/{qqmusic => qqmusic_test}/views/qqmusic_view.dart (100%) create mode 100644 local_plugins/chat_api/analysis_options.yaml create mode 100644 local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt create mode 100644 local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 412f86b15..d86e79d10 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -4,14 +4,17 @@ allprojects { google() mavenCentral() maven { url = uri("https://storage.googleapis.com/download.flutter.io") } - // 添加火山引擎Maven仓库 + // 添加火山引擎Maven仓库 maven { url = uri("https://artifact.bytedance.com/repository/Volcengine/") } 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() diff --git a/lib/core/translations/language/tl_ph.dart b/lib/core/translations/language/tl_ph.dart index 62a9059da..62dc7fb8b 100644 --- a/lib/core/translations/language/tl_ph.dart +++ b/lib/core/translations/language/tl_ph.dart @@ -768,7 +768,7 @@ const Map tlPH = { 'pleaseGrantNecessaryPermissions': 'Pakibigay ang mga kinakailangang pahintulot', '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', 'invalidDeviceAddress': 'Hindi wastong address ng device', 'bleDeviceAssociationFailed': 'Nabigo ang pag-uugnay ng BLE device', diff --git a/lib/core/translations/language/vi_vn.dart b/lib/core/translations/language/vi_vn.dart index f8958a6c1..bb79fd9bb 100644 --- a/lib/core/translations/language/vi_vn.dart +++ b/lib/core/translations/language/vi_vn.dart @@ -744,7 +744,7 @@ const Map viVN = { 'retryAuthorization': 'Ủy quyền lại', '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', - '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', 'invalidDeviceAddress': 'Địa chỉ thiết bị không hợp lệ', 'bleDeviceAssociationFailed': 'Liên kết thiết bị BLE thất bại', diff --git a/lib/modules/agent/views/message_bubble.dart b/lib/modules/agent/views/message_bubble.dart index b8903cc5f..70e4f87c9 100644 --- a/lib/modules/agent/views/message_bubble.dart +++ b/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 'weather_card.dart'; import 'music_card.dart'; @@ -224,30 +226,42 @@ class MessageBubble extends GetView { // 判断消息内容,显示特殊卡片 if (!isUser && meta != null) { // 判断消息类型,显示不同的卡片 - if (meta!.keys.contains('music_play')) { - final card_music = - meta!["music_play"]['card_music'] as Map?; - if (card_music != null) { - final musicId = card_music['id']; - final musicName = card_music['name'] as String? ?? '未知歌曲'; - final musicUrl = card_music['url'] as String? ?? ''; - final musicSinger = card_music['sgener'] as String? ?? '未知歌手'; - final musicImage = card_music['image'] as String? ?? ''; - final ts = card_music['ts'] as int? ?? 0; - print("music_play card_music: $card_music"); - //音乐卡片 - return buildMusicItem(context, musicId, musicName, musicUrl, - musicSinger, musicImage, ts); - } - } else if (meta!.keys.contains('music_playlist')) { - final card_musiclist = - meta!["music_playlist"]['card_musiclist'] as Map?; - if (card_musiclist != null) { - final rawList = card_musiclist['musics'] as List? ?? []; + // if (meta!.keys.contains('music_play')) { + // final card_music = + // meta!["music_play"]['card_music'] as Map?; + // if (card_music != null) { + // final musicId = card_music['id']; + // final musicName = card_music['name'] as String? ?? '未知歌曲'; + // final musicUrl = card_music['url'] as String? ?? ''; + // final musicSinger = card_music['sgener'] as String? ?? '未知歌手'; + // final musicImage = card_music['image'] as String? ?? ''; + // final ts = card_music['ts'] as int? ?? 0; + // //音乐卡片 + // return buildMusicItem(context, musicId, musicName, musicUrl, + // musicSinger, musicImage, ts); + // } + // } else if (meta!.keys.contains('music_playlist')) { + // final card_musiclist = + // meta!["music_playlist"]['card_musiclist'] as Map?; + // if (card_musiclist != null) { + // final rawList = card_musiclist['musics'] as List? ?? []; + // final playlist = + // rawList.map((e) => Map.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; + final data = qqmusic_search_songs['data']; + if (data != null && data is List) { + //qq音乐 + final rawList = data as List? ?? []; final playlist = rawList.map((e) => Map.from(e)).toList(); //音乐播放列表 - return buildMusicContent(context, playlist); + return buildQQMusicContent(context, playlist); } } else if (meta!.keys.contains('maps_weather')) { final maps_weather = meta!['maps_weather'] as Map; diff --git a/lib/modules/agent/views/qqmusic_card.dart b/lib/modules/agent/views/qqmusic_card.dart new file mode 100644 index 000000000..25dc78daf --- /dev/null +++ b/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(); // 获取控制器 + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + Map 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> playlist) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + final controller = Get.find(); // 获取控制器 + 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, + ), + ), + ), + ), + ], + ), + ], + ); +} diff --git a/lib/modules/login/controllers/login_controller.dart b/lib/modules/login/controllers/login_controller.dart index 3cd59f5fd..1ffb49ca0 100644 --- a/lib/modules/login/controllers/login_controller.dart +++ b/lib/modules/login/controllers/login_controller.dart @@ -236,8 +236,8 @@ class LoginController extends GetxController { @override void onClose() { - contactController.dispose(); - verificationCodeController.dispose(); + // contactController.dispose(); + // verificationCodeController.dispose(); super.onClose(); } diff --git a/lib/modules/qqmusic/bindings/qqmusic_playlist_binding.dart b/lib/modules/qqmusic/bindings/qqmusic_playlist_binding.dart new file mode 100644 index 000000000..04c53d816 --- /dev/null +++ b/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()); + } +} diff --git a/lib/modules/qqmusic/controllers/qqmusic_playlist_controller.dart b/lib/modules/qqmusic/controllers/qqmusic_playlist_controller.dart new file mode 100644 index 000000000..d17465a16 --- /dev/null +++ b/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(); + + // 登录状态 + final isLoggedIn = false.obs; + final userInfo = Rxn(); + + // 歌单数据 + final playlist = [].obs; + + final currentSong = Rxn(); + final isPlaying = false.obs; + final currentPosition = 0.obs; + final totalDuration = 0.obs; + final currentIndex = 0.obs; + + @override + void onInit() { + super.onInit(); + // 从路由参数获取歌单数据 + final List> 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 playSong(SongModel song, {List? 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 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 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 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 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 _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 _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 _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(); + } +} diff --git a/lib/modules/qqmusic/controllers/song_model.dart b/lib/modules/qqmusic/controllers/song_model.dart new file mode 100644 index 000000000..e28e9017f --- /dev/null +++ b/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 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 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')}'; + } +} diff --git a/lib/modules/qqmusic/views/qqmusic_playlist_view.dart b/lib/modules/qqmusic/views/qqmusic_playlist_view.dart new file mode 100644 index 000000000..561891e74 --- /dev/null +++ b/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 { + 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 _togglePlayPause(AgentController controller) async { + if (controller.isPlaying.value) { + await controller.musicPause(); + } else { + await controller.musicResume(); + } + } + + // 播放上一曲 + Future _playPrevious( + AgentController controller, List> 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 _playNext( + AgentController controller, List> 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; + } + } +} diff --git a/lib/modules/qqmusic/bindings/qqmusic_binding.dart b/lib/modules/qqmusic_test/bindings/qqmusic_binding.dart similarity index 100% rename from lib/modules/qqmusic/bindings/qqmusic_binding.dart rename to lib/modules/qqmusic_test/bindings/qqmusic_binding.dart diff --git a/lib/modules/qqmusic/controllers/qqmusic_controller.dart b/lib/modules/qqmusic_test/controllers/qqmusic_controller.dart similarity index 99% rename from lib/modules/qqmusic/controllers/qqmusic_controller.dart rename to lib/modules/qqmusic_test/controllers/qqmusic_controller.dart index a828389cc..41b6f29d5 100644 --- a/lib/modules/qqmusic/controllers/qqmusic_controller.dart +++ b/lib/modules/qqmusic_test/controllers/qqmusic_controller.dart @@ -42,14 +42,14 @@ class QQMusicController extends GetxController { Future login() async { try { print('开始QQ音乐登录...'); - + Get.dialog( const Center( child: CircularProgressIndicator(), ), barrierDismissible: false, ); - + // 添加超时处理 final result = await _qqMusic.login().timeout( const Duration(seconds: 30), @@ -58,11 +58,11 @@ class QQMusicController extends GetxController { return {'success': false, 'error': '登录超时,请重试'}; }, ); - + print('QQ音乐登录结果: $result'); - + Get.back(); // 关闭加载对话框 - + if (result['success'] == true) { userInfo.value = QQMusicLoginResult.fromMap(result); isLoggedIn.value = true; @@ -92,11 +92,11 @@ class QQMusicController extends GetxController { /// 搜索歌曲 Future searchSongs(String keyword) async { if (keyword.trim().isEmpty) return; - + print('开始搜索歌曲: $keyword'); searchKeyword.value = keyword; isSearching.value = true; - + try { // 检查登录状态 if (!isLoggedIn.value) { @@ -104,7 +104,7 @@ class QQMusicController extends GetxController { Get.snackbar('搜索失败', '请先登录QQ音乐'); return; } - + print('调用搜索接口...'); final result = await _qqMusic.searchSongs(keyword: keyword).timeout( const Duration(seconds: 15), @@ -113,13 +113,13 @@ class QQMusicController extends GetxController { return {'success': false, 'error': '搜索超时,请重试'}; }, ); - + print('搜索结果: $result'); - + if (result['success'] == true && result['songs'] != null) { final List songsData = result['songs']; print('找到 ${songsData.length} 首歌曲'); - + final songs = songsData.map((songData) { return SongModel( mid: songData['mid'] ?? '', @@ -130,7 +130,7 @@ class QQMusicController extends GetxController { duration: songData['duration'] ?? 0, ); }).toList(); - + searchResults.assignAll(songs); print('搜索成功,更新UI'); } else { diff --git a/lib/modules/qqmusic/models/song_model.dart b/lib/modules/qqmusic_test/models/song_model.dart similarity index 100% rename from lib/modules/qqmusic/models/song_model.dart rename to lib/modules/qqmusic_test/models/song_model.dart diff --git a/lib/modules/qqmusic/views/qqmusic_view.dart b/lib/modules/qqmusic_test/views/qqmusic_view.dart similarity index 100% rename from lib/modules/qqmusic/views/qqmusic_view.dart rename to lib/modules/qqmusic_test/views/qqmusic_view.dart diff --git a/lib/modules/splash/controllers/splash_controller.dart b/lib/modules/splash/controllers/splash_controller.dart index ce47bff7e..ba834cdc1 100644 --- a/lib/modules/splash/controllers/splash_controller.dart +++ b/lib/modules/splash/controllers/splash_controller.dart @@ -151,9 +151,9 @@ class SplashController extends GetxController { } if (isLoggedIn) { await getAppConfig(); - Get.offAllNamed(Routes.qqmusic); + Get.offAllNamed(Routes.pairing); } else { - Get.offAllNamed(Routes.qqmusic); // 跳转到登录页 + Get.offAllNamed(Routes.login); // 跳转到登录页 } } diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index 20fdfb6ab..b6df617fb 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -1,7 +1,9 @@ import 'package:get/get.dart'; import 'package:qq_music/qq_music.dart'; -import 'package:voitrans/modules/qqmusic/bindings/qqmusic_binding.dart'; -import 'package:voitrans/modules/qqmusic/views/qqmusic_view.dart'; +import 'package:voitrans/modules/qqmusic/bindings/qqmusic_playlist_binding.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/views/ble_view.dart'; @@ -280,8 +282,8 @@ abstract class AppPages { ), GetPage( name: Routes.qqmusic, - page: () => const QQMusicView(), - binding: QQMusicBinding(), + page: () => const QqmusicPlaylistView(), + binding: QQMusicPlaylistBinding(), ), ]; } diff --git a/local_plugins/chat_api/analysis_options.yaml b/local_plugins/chat_api/analysis_options.yaml new file mode 100644 index 000000000..21afadf78 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/local_plugins/chat_api/android/build.gradle.kts b/local_plugins/chat_api/android/build.gradle.kts index d855ffd06..1d3abb7cf 100644 --- a/local_plugins/chat_api/android/build.gradle.kts +++ b/local_plugins/chat_api/android/build.gradle.kts @@ -60,4 +60,5 @@ dependencies { add("compileOnly", project(":location_service")) + add("compileOnly", project(":qq_music")) } \ No newline at end of file diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt index c727a8f82..20f6796df 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt +++ b/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 { initializeSystemFunctions() + initializeQQMusiceFunctions() } /** @@ -67,7 +68,17 @@ class MCPClient(private val context: Context? = null) : AutoCloseable { 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服务器 * 直接接收完整的JSON配置字符串 @@ -372,5 +383,4 @@ class MCPClient(private val context: Context? = null) : AutoCloseable { return list } - } \ No newline at end of file diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt new file mode 100644 index 000000000..ccd706d2a --- /dev/null +++ b/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(), + // "required" to emptyList() + // ), + // QQMusicPlayHandler(context) + // ) + + client.registerLocalFunction( + "qqmusic_pause", + "暂停QQ音乐", + mapOf( + "type" to "object", + "properties" to emptyMap(), + "required" to emptyList() + ), + QQMusicPauseHandler(context) + ) + + client.registerLocalFunction( + "qqmusic_resume", + "恢复播放QQ音乐", + mapOf( + "type" to "object", + "properties" to emptyMap(), + "required" to emptyList() + ), + QQMusicResumeHandler(context) + ) + + client.registerLocalFunction( + "qqmusic_stop", + "停止QQ音乐播放", + mapOf( + "type" to "object", + "properties" to emptyMap(), + "required" to emptyList() + ), + QQMusicStopHandler(context) + ) + + client.registerLocalFunction( + "qqmusic_next", + "QQ音乐下一首", + mapOf( + "type" to "object", + "properties" to emptyMap(), + "required" to emptyList() + ), + QQMusicNextHandler(context) + ) + + client.registerLocalFunction( + "qqmusic_previous", + "QQ音乐上一首", + mapOf( + "type" to "object", + "properties" to emptyMap(), + "required" to emptyList() + ), + QQMusicPreviousHandler(context) + ) + + // QQ音乐状态查询 + client.registerLocalFunction( + "qqmusic_get_playback_state", + "获取QQ音乐播放状态", + mapOf( + "type" to "object", + "properties" to emptyMap(), + "required" to emptyList() + ), + QQMusicGetPlaybackStateHandler(context) + ) + + client.registerLocalFunction( + "qqmusic_get_current_song", + "获取QQ音乐当前播放歌曲信息", + mapOf( + "type" to "object", + "properties" to emptyMap(), + "required" to emptyList() + ), + QQMusicGetCurrentSongHandler(context) + ) + + client.registerLocalFunction( + "qqmusic_get_play_time", + "获取QQ音乐播放时间信息", + mapOf( + "type" to "object", + "properties" to emptyMap(), + "required" to emptyList() + ), + 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 { + 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 { + 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 + // 添加调试日志 + 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 { + return try { + @Suppress("UNCHECKED_CAST") + val songMids = arguments["songMids"] as? List ?: 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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}\"}" + } + } +} \ No newline at end of file diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt index 4ecb86ffe..462bf7401 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt +++ b/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) ) - // 注册媒体播放功能 - client.registerLocalFunction( - name = "media_play", - description = "播放媒体", - parameters = mapOf( - "type" to "object", - "properties" to emptyMap(), - "required" to emptyList() - ), - handler = MediaPlayHandler(context) - ) + // // 注册媒体播放功能 + // client.registerLocalFunction( + // name = "media_play", + // description = "播放媒体", + // parameters = mapOf( + // "type" to "object", + // "properties" to emptyMap(), + // "required" to emptyList() + // ), + // handler = MediaPlayHandler(context) + // ) - // 注册媒体暂停功能 - client.registerLocalFunction( - name = "media_pause", - description = "暂停媒体播放", - parameters = mapOf( - "type" to "object", - "properties" to emptyMap(), - "required" to emptyList() - ), - handler = MediaPauseHandler(context) - ) - // 注册媒体暂停功能 - client.registerLocalFunction( - name = "media_close", - description = "关闭媒体播放", - parameters = mapOf( - "type" to "object", - "properties" to emptyMap(), - "required" to emptyList() - ), - handler = MediaCloseHandler(context) - ) - // 注册媒体上一首功能 - client.registerLocalFunction( - name = "media_previous", - description = "播放上一首", - parameters = mapOf( - "type" to "object", - "properties" to emptyMap(), - "required" to emptyList() - ), - handler = MediaPreviousHandler(context) - ) + // // 注册媒体暂停功能 + // client.registerLocalFunction( + // name = "media_pause", + // description = "暂停媒体播放", + // parameters = mapOf( + // "type" to "object", + // "properties" to emptyMap(), + // "required" to emptyList() + // ), + // handler = MediaPauseHandler(context) + // ) + // // 注册媒体暂停功能 + // client.registerLocalFunction( + // name = "media_close", + // description = "关闭媒体播放", + // parameters = mapOf( + // "type" to "object", + // "properties" to emptyMap(), + // "required" to emptyList() + // ), + // handler = MediaCloseHandler(context) + // ) + // // 注册媒体上一首功能 + // client.registerLocalFunction( + // name = "media_previous", + // description = "播放上一首", + // parameters = mapOf( + // "type" to "object", + // "properties" to emptyMap(), + // "required" to emptyList() + // ), + // handler = MediaPreviousHandler(context) + // ) - // 注册媒体下一首功能 - client.registerLocalFunction( - name = "media_next", - description = "播放下一首", - parameters = mapOf( - "type" to "object", - "properties" to emptyMap(), - "required" to emptyList() - ), - handler = MediaNextHandler(context) - ) + // // 注册媒体下一首功能 + // client.registerLocalFunction( + // name = "media_next", + // description = "播放下一首", + // parameters = mapOf( + // "type" to "object", + // "properties" to emptyMap(), + // "required" to emptyList() + // ), + // handler = MediaNextHandler(context) + // ) // 注册打开录音机功能 client.registerLocalFunction( diff --git a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicManager.kt b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicManager.kt index 5482419bc..ac4c4ff27 100644 --- a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicManager.kt +++ b/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 CommonCmd.init(Config.BIND_PLATFORM) - // 发送hi消息 - sayHi() + // 延迟发送hi消息,确保Binder连接稳定 + Handler(Looper.getMainLooper()).postDelayed({ + sayHi() + }, 500) // 延迟500ms } override fun onServiceDisconnected(name: ComponentName?) { @@ -64,14 +66,32 @@ class QQMusicManager(private val activity: Activity) { } private fun sayHi() { - if (!isBindQQMusicService || qqmusicApi == null) return - - val bundle = Bundle() - bundle.putInt(Keys.API_PARAM_KEY_SDK_VERSION, CommonCmd.SDK_VERSION) - bundle.putString(Keys.API_PARAM_KEY_PLATFORM_TYPE, Config.BIND_PLATFORM) + if (!isBindQQMusicService || qqmusicApi == null) { + Log.w(TAG, "QQ音乐服务未连接,无法发送hi消息") + return + } - val result = qqmusicApi?.execute("hi", bundle) - Log.d(TAG, "sayHi ret:" + result?.getInt(Keys.API_RETURN_KEY_CODE)) + try { + val bundle = Bundle() + bundle.putInt(Keys.API_PARAM_KEY_SDK_VERSION, CommonCmd.SDK_VERSION) + bundle.putString(Keys.API_PARAM_KEY_PLATFORM_TYPE, Config.BIND_PLATFORM) + + val result = qqmusicApi?.execute("hi", bundle) + 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() { diff --git a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt new file mode 100644 index 000000000..62f43e661 --- /dev/null +++ b/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) -> 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>>) -> 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>>) -> Unit + ) { + val currentActivity = activity ?: return + + ensureMusicManager(currentActivity) + musicManager?.searchSongs(keyword, searchType, object : QQMusicManager.SearchCallback { + override fun onSuccess(songs: List>) { + 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, + index: Int = 0, + callback: (Result) -> 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) { + executePlaybackAction("播放") { musicManager, playCallback -> + musicManager.playMusic(playCallback) + }(callback) + } + + /** + * 暂停音乐 + */ + fun pauseMusic(callback: (Result) -> Unit) { + executePlaybackAction("暂停") { musicManager, playCallback -> + musicManager.pauseMusic(playCallback) + }(callback) + } + + /** + * 恢复播放 + */ + fun resumeMusic(callback: (Result) -> Unit) { + executePlaybackAction("恢复播放") { musicManager, playCallback -> + musicManager.resumeMusic(playCallback) + }(callback) + } + + /** + * 停止音乐 + */ + fun stopMusic(callback: (Result) -> Unit) { + executePlaybackAction("停止") { musicManager, playCallback -> + musicManager.stopMusic(playCallback) + }(callback) + } + + /** + * 下一首 + */ + fun skipToNext(callback: (Result) -> Unit) { + executePlaybackAction("下一首") { musicManager, playCallback -> + musicManager.skipToNext(playCallback) + }(callback) + } + + /** + * 上一首 + */ + fun skipToPrevious(callback: (Result) -> Unit) { + executePlaybackAction("上一首") { musicManager, playCallback -> + musicManager.skipToPrevious(playCallback) + }(callback) + } + + /** + * 获取播放状态 + */ + fun getPlaybackState(callback: (Result>) -> Unit) { + executeInfoAction("获取播放状态") { musicManager, infoCallback -> + musicManager.getPlaybackState(infoCallback) + }(callback) + } + + /** + * 获取当前歌曲信息 + */ + fun getCurrentSong(callback: (Result>) -> Unit) { + executeInfoAction("获取当前歌曲") { musicManager, infoCallback -> + musicManager.getCurrentSong(infoCallback) + }(callback) + } + + /** + * 获取播放时间 + */ + fun getPlayTime(callback: (Result) -> 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) { + 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 { + 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>) -> 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) { + 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 + ) +} diff --git a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QqMusicPlugin.kt b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QqMusicPlugin.kt index 02ee2f5fd..7a0b9fe4c 100644 --- a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QqMusicPlugin.kt +++ b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QqMusicPlugin.kt @@ -1,8 +1,5 @@ package com.yunqiinnovation.qq_music -import android.os.Handler -import android.os.Looper -import android.util.Log import android.app.Activity import io.flutter.embedding.engine.plugins.FlutterPlugin 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.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result -// 添加这行导入 import com.tencent.qqmusic.third.api.contract.* /** QqMusicPlugin */ class QqMusicPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { private lateinit var channel: MethodChannel private var activity: Activity? = null - private var loginManager: QQMusicLoginManager? = null - private var musicManager: QQMusicManager? = null + private val qqMusicSingleton = QQMusicSingleton.getInstance() override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.qq_music") @@ -29,40 +24,158 @@ class QqMusicPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { override fun onMethodCall(call: MethodCall, result: Result) { when (call.method) { "login" -> { - handleLogin(result) + qqMusicSingleton.login { kotlinResult -> + kotlinResult.fold( + onSuccess = { loginResult -> + result.success(mapOf( + "success" to true, + "openId" to loginResult.openId, + "openToken" to loginResult.openToken, + "expireTime" to loginResult.expireTime + )) + }, + onFailure = { error -> + result.error("LOGIN_FAILED", error.message, null) + } + ) + } } "searchSongs" -> { - handleSearchSongs(call, result) + val keyword = call.argument("keyword") + val searchType = call.argument("searchType") ?: Data.SearchType.SEARCH_TYPE_SONG + + if (keyword == null) { + result.error("INVALID_ARGUMENT", "keyword is required", null) + return + } + + qqMusicSingleton.searchSongs(keyword, searchType) { kotlinResult -> + kotlinResult.fold( + onSuccess = { songs -> + result.success(mapOf( + "success" to true, + "songs" to songs + )) + }, + onFailure = { error -> + result.error("SEARCH_FAILED", error.message, null) + } + ) + } } "playSongs" -> { - handlePlaySongs(call, result) + val songMids = call.argument>("songMids") + val index = call.argument("index") ?: 0 + + if (songMids == null || songMids.isEmpty()) { + result.error("INVALID_ARGUMENT", "songMids is required", null) + return + } + + qqMusicSingleton.playSongs(songMids, index) { kotlinResult -> + kotlinResult.fold( + onSuccess = { + result.success(mapOf("success" to true)) + }, + onFailure = { error -> + result.error("PLAY_FAILED", error.message, null) + } + ) + } } "playMusic" -> { - handlePlayMusic(result) + qqMusicSingleton.playMusic { kotlinResult -> + kotlinResult.fold( + onSuccess = { result.success(mapOf("success" to true)) }, + onFailure = { error -> result.error("PLAY_FAILED", error.message, null) } + ) + } } "pauseMusic" -> { - handlePauseMusic(result) + qqMusicSingleton.pauseMusic { kotlinResult -> + kotlinResult.fold( + onSuccess = { result.success(mapOf("success" to true)) }, + onFailure = { error -> result.error("PAUSE_FAILED", error.message, null) } + ) + } } "resumeMusic" -> { - handleResumeMusic(result) + qqMusicSingleton.resumeMusic { kotlinResult -> + kotlinResult.fold( + onSuccess = { result.success(mapOf("success" to true)) }, + onFailure = { error -> result.error("RESUME_FAILED", error.message, null) } + ) + } } "stopMusic" -> { - handleStopMusic(result) + qqMusicSingleton.stopMusic { kotlinResult -> + kotlinResult.fold( + onSuccess = { result.success(mapOf("success" to true)) }, + onFailure = { error -> result.error("STOP_FAILED", error.message, null) } + ) + } } "skipToNext" -> { - handleSkipToNext(result) + qqMusicSingleton.skipToNext { kotlinResult -> + kotlinResult.fold( + onSuccess = { result.success(mapOf("success" to true)) }, + onFailure = { error -> result.error("SKIP_FAILED", error.message, null) } + ) + } } "skipToPrevious" -> { - handleSkipToPrevious(result) + qqMusicSingleton.skipToPrevious { kotlinResult -> + kotlinResult.fold( + onSuccess = { result.success(mapOf("success" to true)) }, + onFailure = { error -> result.error("SKIP_FAILED", error.message, null) } + ) + } } "getPlaybackState" -> { - handleGetPlaybackState(result) + qqMusicSingleton.getPlaybackState { kotlinResult -> + kotlinResult.fold( + onSuccess = { data -> + result.success(mapOf( + "success" to true, + "state" to data["state"] + )) + }, + onFailure = { error -> + result.error("GET_STATE_FAILED", error.message, null) + } + ) + } } "getCurrentSong" -> { - handleGetCurrentSong(result) + qqMusicSingleton.getCurrentSong { kotlinResult -> + kotlinResult.fold( + onSuccess = { data -> + result.success(mapOf( + "success" to true, + "song" to data + )) + }, + onFailure = { error -> + result.error("GET_SONG_FAILED", error.message, null) + } + ) + } } "getPlayTime" -> { - handleGetPlayTime(result) + qqMusicSingleton.getPlayTime { kotlinResult -> + kotlinResult.fold( + onSuccess = { playTimeInfo -> + result.success(mapOf( + "success" to true, + "totalTime" to playTimeInfo.totalTime, + "currentTime" to playTimeInfo.currentTime + )) + }, + onFailure = { error -> + result.error("GET_TIME_FAILED", error.message, null) + } + ) + } } "getPlatformVersion" -> { result.success("Android ${android.os.Build.VERSION.RELEASE}") @@ -72,307 +185,14 @@ class QqMusicPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { } } } - - 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, - "openId" to openId, - "openToken" to openToken, - "expireTime" to expireTime - ) - Log.d("QqMusicPlugin", "登录成功回调: $loginResult") - result.success(loginResult) - } - } - - 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 - } - - val keyword = call.argument("keyword") - val searchType = call.argument("searchType") ?: Data.SearchType.SEARCH_TYPE_SONG - - if (keyword == null) { - result.error("INVALID_ARGUMENT", "keyword is required", null) - return - } - - Log.d("QqMusicPlugin", "开始搜索: keyword=$keyword, searchType=$searchType") - - ensureMusicManager(currentActivity) - musicManager?.searchSongs(keyword, searchType, object : QQMusicManager.SearchCallback { - override fun onSuccess(songs: List>) { - // 确保在主线程中执行回调 - Handler(Looper.getMainLooper()).post { - Log.d("QqMusicPlugin", "搜索成功: 找到 ${songs.size} 首歌曲") - result.success(mapOf( - "success" to true, - "songs" to songs - )) - } - } - - override fun onError(errorCode: Int, errorMessage: String) { - // 确保在主线程中执行回调 - Handler(Looper.getMainLooper()).post { - Log.e("QqMusicPlugin", "搜索失败: code=$errorCode, message=$errorMessage") - result.error("SEARCH_FAILED", errorMessage, null) - } - } - }) - } - - 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>("songMids") - val index = call.argument("index") ?: 0 - - if (songMids == null || songMids.isEmpty()) { - result.error("INVALID_ARGUMENT", "songMids is required", null) - return - } - - ensureMusicManager(currentActivity) - musicManager?.playSongs(songMids, index, 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 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)) - } - - override fun onError(errorCode: Int, errorMessage: String) { - result.error("RESUME_FAILED", errorMessage, null) - } - }) - } - - private fun handleStopMusic(result: Result) { - val currentActivity = activity - if (currentActivity == null) { - result.error("NO_ACTIVITY", "Activity not available", null) - return - } - - ensureMusicManager(currentActivity) - musicManager?.stopMusic(object : QQMusicManager.PlayCallback { - override fun onSuccess() { - result.success(mapOf("success" to true)) - } - - override fun onError(errorCode: Int, errorMessage: String) { - result.error("STOP_FAILED", errorMessage, null) - } - }) - } - - private fun handleSkipToNext(result: Result) { - val currentActivity = activity - if (currentActivity == null) { - result.error("NO_ACTIVITY", "Activity not available", null) - return - } - - ensureMusicManager(currentActivity) - musicManager?.skipToNext(object : QQMusicManager.PlayCallback { - override fun onSuccess() { - result.success(mapOf("success" to true)) - } - - override fun onError(errorCode: Int, errorMessage: String) { - result.error("SKIP_FAILED", errorMessage, null) - } - }) - } - - private fun handleSkipToPrevious(result: Result) { - val currentActivity = activity - if (currentActivity == null) { - result.error("NO_ACTIVITY", "Activity not available", null) - return - } - - ensureMusicManager(currentActivity) - musicManager?.skipToPrevious(object : QQMusicManager.PlayCallback { - override fun onSuccess() { - result.success(mapOf("success" to true)) - } - - override fun onError(errorCode: Int, errorMessage: String) { - result.error("SKIP_FAILED", errorMessage, null) - } - }) - } - - private fun handleGetPlaybackState(result: Result) { - val currentActivity = activity - if (currentActivity == null) { - result.error("NO_ACTIVITY", "Activity not available", null) - return - } - - ensureMusicManager(currentActivity) - musicManager?.getPlaybackState(object : QQMusicManager.InfoCallback { - override fun onSuccess(data: Map) { - result.success(mapOf( - "success" to true, - "state" to data["state"] - )) - } - - 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 - } - - ensureMusicManager(currentActivity) - musicManager?.getCurrentSong(object : QQMusicManager.InfoCallback { - override fun onSuccess(data: Map) { - result.success(mapOf( - "success" to true, - "song" to data - )) - } - - override fun onError(errorCode: Int, errorMessage: String) { - result.error("GET_SONG_FAILED", errorMessage, null) - } - }) - } - - private fun handleGetPlayTime(result: Result) { - val currentActivity = activity - if (currentActivity == null) { - result.error("NO_ACTIVITY", "Activity not available", null) - return - } - - ensureMusicManager(currentActivity) - musicManager?.getPlayTime(object : QQMusicManager.InfoCallback { - override fun onSuccess(data: Map) { - result.success(mapOf( - "success" to true, - "totalTime" to data["totalTime"], - "currentTime" to data["currentTime"] - )) - } - - override fun onError(errorCode: Int, errorMessage: String) { - result.error("GET_TIME_FAILED", errorMessage, null) - } - }) - } - - private fun ensureMusicManager(activity: Activity) { - if (musicManager == null) { - musicManager = QQMusicManager(activity) - } - } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { channel.setMethodCallHandler(null) - loginManager?.cleanup() - loginManager = null - musicManager?.cleanup() - musicManager = null } override fun onAttachedToActivity(binding: ActivityPluginBinding) { activity = binding.activity + qqMusicSingleton.initialize(activity!!) } override fun onDetachedFromActivityForConfigChanges() { @@ -381,13 +201,11 @@ class QqMusicPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { activity = binding.activity + qqMusicSingleton.initialize(activity!!) } override fun onDetachedFromActivity() { activity = null - loginManager?.cleanup() - loginManager = null - musicManager?.cleanup() - musicManager = null + qqMusicSingleton.cleanup() } }