From 98586a6ab82a489cb6bd11c042eb0c3767551747 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Sun, 3 Aug 2025 21:56:55 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/models/song_model.dart | 60 +++++ lib/data/services/qq_musoc.dart | 239 +++++++++++++++++- .../qqmusic_playlist_controller.dart | 235 +++++------------ .../qqmusic/controllers/song_model.dart | 49 ---- .../qqmusic/views/qqmusic_playlist_view.dart | 204 +++++++++++---- .../agent_service/AgentService.kt | 122 ++++++--- .../chat_api/QQMusicSystemFunctionHandler.kt | 50 ++-- .../qq_music/QQMusicManager.kt | 107 ++++++++ .../qq_music/QQMusicSingleton.kt | 231 ++++++++++++----- .../yunqiinnovation/qq_music/QqMusicPlugin.kt | 15 ++ local_plugins/qq_music/lib/qq_music.dart | 10 + 11 files changed, 913 insertions(+), 409 deletions(-) create mode 100644 lib/data/models/song_model.dart delete mode 100644 lib/modules/qqmusic/controllers/song_model.dart diff --git a/lib/data/models/song_model.dart b/lib/data/models/song_model.dart new file mode 100644 index 000000000..a301690ee --- /dev/null +++ b/lib/data/models/song_model.dart @@ -0,0 +1,60 @@ +class SongModel { + final String mid; + final String? id; + final String name; + final String artist; + final String album; + final int duration; // 毫秒 + final String? albumPic; + + SongModel({ + required this.mid, + this.id, + required this.name, + required this.artist, + required this.album, + required this.duration, + this.albumPic, + }); + + factory SongModel.fromJson(Map json) { + return SongModel( + mid: json['mid'] ?? '', + id: json['id'], + name: json['name'] ?? '', + artist: json['artist'] ?? json['singer'] ?? '', + album: json['album'] ?? '', + duration: json['duration'] ?? 0, + albumPic: json['albumPic'] ?? json['pic'] ?? json['coverUri'], + ); + } + + Map toJson() { + return { + 'mid': mid, + 'id': id, + 'name': name, + 'artist': artist, + 'singer': artist, // 兼容性字段 + 'album': album, + 'duration': duration, + 'albumPic': albumPic, + 'pic': albumPic, // 兼容性字段 + 'coverUri': albumPic, // 兼容性字段 + }; + } + + @override + String toString() { + return 'SongModel(mid: $mid, name: $name, artist: $artist, album: $album, duration: $duration)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is SongModel && other.mid == mid; + } + + @override + int get hashCode => mid.hashCode; +} diff --git a/lib/data/services/qq_musoc.dart b/lib/data/services/qq_musoc.dart index d26a90241..c9e5d6ddc 100644 --- a/lib/data/services/qq_musoc.dart +++ b/lib/data/services/qq_musoc.dart @@ -1,7 +1,9 @@ +import 'dart:async'; import 'package:get/get.dart'; import 'package:qq_music/qq_music.dart'; +import '../models/song_model.dart'; -/// QQ音乐服务适配器 +/// QQ音乐服务适配器 - 全局状态管理 class QqMusocService extends GetxService { static QqMusocService get to => Get.find(); @@ -9,19 +11,173 @@ class QqMusocService extends GetxService { // 音乐服务实例 final QqMusic _qqMusic = QqMusic(); + // 全局音乐状态 + final isPlaying = false.obs; + final currentPosition = 0.obs; // 毫秒 + final totalDuration = 0.obs; // 毫秒 + final currentSong = Rxn(); + final playlist = [].obs; + final currentIndex = 0.obs; + + // 状态监听定时器 + Timer? _stateMonitorTimer; + + // 监听器列表 - 用于通知页面状态变化 + final List _listeners = []; + /// 初始化 @override void onInit() { super.onInit(); + print('🎵 [QQMusicService] 全局音乐服务初始化'); + } + + @override + void onClose() { + _stateMonitorTimer?.cancel(); + _listeners.clear(); + print('🎵 [QQMusicService] 全局音乐服务销毁'); + super.onClose(); + } + + /// 添加状态监听器 + void addListener(Function() listener) { + _listeners.add(listener); + } + + /// 移除状态监听器 + void removeListener(Function() listener) { + _listeners.remove(listener); + } + + /// 通知所有监听器 + void _notifyListeners() { + for (final listener in _listeners) { + try { + listener(); + } catch (e) { + print('🔥 [QQMusicService] 监听器回调异常: $e'); + } + } + } + + /// 启动播放状态监控 + void startPlaybackStateMonitor() { + // 取消之前的定时器 + _stateMonitorTimer?.cancel(); + + print('🎵 [QQMusicService] 启动全局状态监控'); + + // 立即执行一次同步 + _syncPlaybackInfo(); + + // 启动定时器,每秒同步一次 + _stateMonitorTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + _syncPlaybackInfo(); + }); + + print('🎵 [QQMusicService] 全局状态监控已启动'); + } + + /// 停止播放状态监控 + void stopPlaybackStateMonitor() { + _stateMonitorTimer?.cancel(); + _stateMonitorTimer = null; + print('⏹️ [QQMusicService] 全局状态监控已停止'); + } + + /// 统一同步播放信息 + Future _syncPlaybackInfo() async { + try { + final result = await _qqMusic.syncCurrentPlayInfo(); + + if (result['success'] == true && result['data'] != null) { + final data = result['data']; + + // 更新播放状态 + final newIsPlaying = data['isPlaying'] ?? false; + final wasPlaying = isPlaying.value; + + if (isPlaying.value != newIsPlaying) { + print( + '🎮 [QQMusicService] 全局播放状态变化: ${isPlaying.value} -> $newIsPlaying'); + isPlaying.value = newIsPlaying; + } + + // 如果从播放状态变为停止状态,自动停止监听 + if (wasPlaying && !newIsPlaying) { + print('⏹️ [QQMusicService] 检测到播放停止,自动停止全局监听'); + stopPlaybackStateMonitor(); + _notifyListeners(); + return; + } + + // 如果当前不在播放状态,也停止监听 + if (!newIsPlaying) { + print('⏸️ [QQMusicService] 当前未在播放,停止全局监听以节省资源'); + stopPlaybackStateMonitor(); + _notifyListeners(); + return; + } + + // 更新播放时间(Android端返回秒,转换为毫秒) + final currentTimeSec = data['currentTime'] ?? 0; + final totalTimeSec = data['totalTime'] ?? 0; + + final newCurrentPosition = (currentTimeSec * 1000).round(); + final newTotalDuration = (totalTimeSec * 1000).round(); + + if (currentPosition.value != newCurrentPosition) { + currentPosition.value = newCurrentPosition; + } + if (totalDuration.value != newTotalDuration) { + totalDuration.value = newTotalDuration; + } + + // 更新当前歌曲信息 + final songData = data['currentSong']; + if (songData != null) { + final newSong = SongModel( + mid: songData['mid'] ?? '', + name: songData['name'] ?? '', + artist: songData['singer'] ?? '', + album: songData['album'] ?? '', + albumPic: songData['coverUri'], + duration: songData['duration'] ?? 0, + ); + + if (currentSong.value?.mid != newSong.mid) { + print( + '🎵 [QQMusicService] 当前歌曲变化: ${currentSong.value?.name} -> ${newSong.name}'); + currentSong.value = newSong; + } + } + + // 通知所有监听器 + _notifyListeners(); + } else { + // 同步失败时停止监听 + print('❌ [QQMusicService] 全局同步失败,停止监听'); + stopPlaybackStateMonitor(); + } + } catch (e) { + print('💥 [QQMusicService] 全局同步异常: $e'); + stopPlaybackStateMonitor(); + } + } + + /// 格式化时间 + 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')}'; } /// 登录QQ音乐 Future> login() async { try { - // 直接调用QQ音乐插件的登录方法 final result = await _qqMusic.login(); - - // 检查登录结果 if (result['success'] == true) { isInitialized = true; return result; @@ -36,7 +192,7 @@ class QqMusocService extends GetxService { /// 搜索歌曲 Future> searchSongs({ required String keyword, - int searchType = 0, // SEARCH_TYPE_SONG + int searchType = 0, }) async { try { final result = await _qqMusic.searchSongs( @@ -49,16 +205,41 @@ class QqMusocService extends GetxService { } } - /// 播放歌曲列表 + /// 播放歌曲列表 - 更新全局状态 Future> playSongs({ required List songMids, int index = 0, + List? songList, + SongModel? currentSongModel, }) async { try { final result = await _qqMusic.playSongs( songMids: songMids, index: index, ); + + if (result['success'] == true) { + // 更新全局状态 + if (songList != null) { + playlist.assignAll(songList); + currentIndex.value = index; + } + + if (currentSongModel != null) { + currentSong.value = currentSongModel; + totalDuration.value = currentSongModel.duration; + } + + isPlaying.value = true; + currentPosition.value = 0; + + // 启动全局状态监听 + print('🎵 [QQMusicService] 开始播放,启动全局状态监听'); + startPlaybackStateMonitor(); + + _notifyListeners(); + } + return result; } catch (e) { return {'success': false, 'error': '播放失败: ${e.toString()}'}; @@ -69,6 +250,11 @@ class QqMusocService extends GetxService { Future> playMusic() async { try { final result = await _qqMusic.playMusic(); + if (result['success'] == true) { + isPlaying.value = true; + startPlaybackStateMonitor(); + _notifyListeners(); + } return result; } catch (e) { return {'success': false, 'error': '播放失败: ${e.toString()}'}; @@ -79,6 +265,10 @@ class QqMusocService extends GetxService { Future> pauseMusic() async { try { final result = await _qqMusic.pauseMusic(); + if (result['success'] == true) { + isPlaying.value = false; + _notifyListeners(); + } return result; } catch (e) { return {'success': false, 'error': '暂停失败: ${e.toString()}'}; @@ -89,6 +279,11 @@ class QqMusocService extends GetxService { Future> resumeMusic() async { try { final result = await _qqMusic.resumeMusic(); + if (result['success'] == true) { + isPlaying.value = true; + startPlaybackStateMonitor(); + _notifyListeners(); + } return result; } catch (e) { return {'success': false, 'error': '继续播放失败: ${e.toString()}'}; @@ -99,6 +294,12 @@ class QqMusocService extends GetxService { Future> stopMusic() async { try { final result = await _qqMusic.stopMusic(); + if (result['success'] == true) { + isPlaying.value = false; + currentPosition.value = 0; + stopPlaybackStateMonitor(); + _notifyListeners(); + } return result; } catch (e) { return {'success': false, 'error': '停止失败: ${e.toString()}'}; @@ -109,6 +310,14 @@ class QqMusocService extends GetxService { Future> skipToNext() async { try { final result = await _qqMusic.skipToNext(); + if (result['success'] == true) { + // 更新当前索引 + if (currentIndex.value < playlist.length - 1) { + currentIndex.value++; + currentSong.value = playlist[currentIndex.value]; + } + _notifyListeners(); + } return result; } catch (e) { return {'success': false, 'error': '下一首失败: ${e.toString()}'}; @@ -119,6 +328,14 @@ class QqMusocService extends GetxService { Future> skipToPrevious() async { try { final result = await _qqMusic.skipToPrevious(); + if (result['success'] == true) { + // 更新当前索引 + if (currentIndex.value > 0) { + currentIndex.value--; + currentSong.value = playlist[currentIndex.value]; + } + _notifyListeners(); + } return result; } catch (e) { return {'success': false, 'error': '上一首失败: ${e.toString()}'}; @@ -154,4 +371,14 @@ class QqMusocService extends GetxService { return {'success': false, 'error': '获取播放时间失败: ${e.toString()}'}; } } + + /// 同步当前播放信息 - 统一接口 + Future> syncCurrentPlayInfo() async { + try { + final result = await _qqMusic.syncCurrentPlayInfo(); + return result; + } catch (e) { + return {'success': false, 'error': '同步播放信息失败: ${e.toString()}'}; + } + } } diff --git a/lib/modules/qqmusic/controllers/qqmusic_playlist_controller.dart b/lib/modules/qqmusic/controllers/qqmusic_playlist_controller.dart index d17465a16..d043f7a28 100644 --- a/lib/modules/qqmusic/controllers/qqmusic_playlist_controller.dart +++ b/lib/modules/qqmusic/controllers/qqmusic_playlist_controller.dart @@ -1,70 +1,74 @@ 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'; +import '../../../data/models/song_model.dart'; +import '../../../data/services/qq_musoc.dart'; class QQMusicPlaylistController extends GetxController { - final QqMusocService _qqMusic = Get.find(); + final QqMusocService _qqMusicService = QqMusocService.to; - // 登录状态 + // 登录相关 final isLoggedIn = false.obs; - final userInfo = Rxn(); + 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; + // 直接使用全局服务的状态 - 修复返回类型 + RxBool get isPlaying => _qqMusicService.isPlaying; + RxInt get currentPosition => _qqMusicService.currentPosition; + RxInt get totalDuration => _qqMusicService.totalDuration; + Rxn get currentSong => _qqMusicService.currentSong; + RxList get playlist => _qqMusicService.playlist; + RxInt get currentIndex => _qqMusicService.currentIndex; @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); + + // 从路由参数获取播放列表 + final arguments = Get.arguments; + if (arguments != null && arguments['playlist'] != null) { + final List playlistData = arguments['playlist']; + final songList = + playlistData.map((item) => SongModel.fromJson(item)).toList(); + _qqMusicService.playlist.assignAll(songList); + + if (songList.isNotEmpty) { + _qqMusicService.currentSong.value = songList.first; + _qqMusicService.currentIndex.value = 0; + } + } + + // 添加状态监听器 + _qqMusicService.addListener(_onMusicStateChanged); + + print('🎵 [QQMusicPlaylistController] 控制器初始化完成'); + } + + @override + void onClose() { + // 移除状态监听器 + _qqMusicService.removeListener(_onMusicStateChanged); + super.onClose(); + } + + /// 音乐状态变化回调 + void _onMusicStateChanged() { + // 触发UI更新 + update(); } /// 播放歌曲 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; + final List finalSongList = songList ?? [song]; + final songMids = finalSongList.map((s) => s.mid).toList(); + final playIndex = finalSongList.indexOf(song); - // 调用QQ音乐播放接口 - final result = await _qqMusic.playSongs( + final result = await _qqMusicService.playSongs( songMids: songMids, index: playIndex, + songList: finalSongList, + currentSongModel: song, ); if (result['success'] == true) { - isPlaying.value = true; - totalDuration.value = song.duration; - currentPosition.value = 0; Get.snackbar('开始播放', song.name); } else { Get.snackbar('播放失败', result['error'] ?? '未知错误'); @@ -76,141 +80,30 @@ class QQMusicPlaylistController extends GetxController { /// 暂停/继续播放 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()); + if (isPlaying.value) { + await _qqMusicService.pauseMusic(); + } else { + await _qqMusicService.resumeMusic(); } } /// 下一首 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()); - } + await _qqMusicService.skipToNext(); } - /// 停止播放 - 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) { - // 静默处理错误,避免频繁弹窗 - } + /// 上一首 + Future previousSong() async { + await _qqMusicService.skipToPrevious(); } - /// 跳转到指定位置(暂时保留,QQ音乐可能不支持) - void seekTo(int position) { - currentPosition.value = position; - // QQ音乐SDK可能不支持跳转功能 - Get.snackbar('提示', '当前版本不支持跳转功能'); + /// 停止播放 + Future stopMusic() async { + await _qqMusicService.stopMusic(); } - @override - void onClose() { - // 清理资源 - super.onClose(); + /// 格式化时间 + String formatDuration(int milliseconds) { + return _qqMusicService.formatDuration(milliseconds); } } diff --git a/lib/modules/qqmusic/controllers/song_model.dart b/lib/modules/qqmusic/controllers/song_model.dart deleted file mode 100644 index e28e9017f..000000000 --- a/lib/modules/qqmusic/controllers/song_model.dart +++ /dev/null @@ -1,49 +0,0 @@ -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 index 561891e74..59da35edc 100644 --- a/lib/modules/qqmusic/views/qqmusic_playlist_view.dart +++ b/lib/modules/qqmusic/views/qqmusic_playlist_view.dart @@ -2,7 +2,7 @@ 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 'package:voitrans/data/models/song_model.dart'; // Use the data model instead import '../../../core/utils/logger.dart'; import '../../agent/controllers/agent_controller.dart'; import '../../agent/views/circular_progress_button.dart'; @@ -12,14 +12,13 @@ class QqmusicPlaylistView extends GetView { @override Widget build(BuildContext context) { - ScreenUtil.init(context, designSize: const Size(375, 812)); // 添加设计尺寸参考 - + 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风格箭头 + icon: const Icon(Icons.arrow_back_ios, size: 20), onPressed: () => Navigator.pop(context), ), backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, @@ -39,7 +38,7 @@ class QqmusicPlaylistView extends GetView { ), SizedBox(height: 16.h), Text( - 'noMusic'.tr, // 暂无音乐 + 'noMusic'.tr, style: TextStyle( fontSize: 16.sp, color: Colors.grey[600], @@ -50,19 +49,21 @@ class QqmusicPlaylistView extends GetView { ) : Column( children: [ - // 上部:当前播放音乐详情 - 响应式高度 + // 上部:当前播放音乐详情 Container( - height: MediaQuery.of(context).size.height * 0.38, // 增大高度比例 + height: MediaQuery.of(context).size.height * 0.38, child: _buildCurrentPlayingSection(isDarkMode), ), // 中部:播放全部按钮 _buildPlayAllSection(isDarkMode), - // 下部:歌单列表 - 使用剩余空间 + // 下部:歌单列表 Expanded( child: _buildPlaylistSection(isDarkMode), ), ], ), + // 添加迷你播放器 + bottomNavigationBar: _buildMiniPlayer(isDarkMode), ); } @@ -77,8 +78,8 @@ class QqmusicPlaylistView extends GetView { mainAxisAlignment: MainAxisAlignment.center, children: [ // 封面图片 - 响应式尺寸 - _buildCoverImage( - controller.currentSong.value!.albumPic, isDarkMode, + _buildCoverImage(controller.currentSong.value!.albumPic ?? '', + isDarkMode, // Handle nullable albumPic size: 110.w), SizedBox(height: 12.h), // 歌曲信息 @@ -356,64 +357,155 @@ class QqmusicPlaylistView extends GetView { ); } - // 构建进度条 + // 构建进度条 - 参考qqmusic_test的简洁设计 Widget _buildProgressBar(bool isDarkMode) { return Obx(() { - return Row( + // 计算进度值,确保在0.0-1.0范围内 + double progressValue = 0.0; + if (controller.totalDuration.value > 0) { + progressValue = + (controller.currentPosition.value / controller.totalDuration.value) + .clamp(0.0, 1.0); + } + + return Column( 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), + // 时间显示 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + _formatDuration(controller.currentPosition.value), + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), ), - child: Slider( - value: controller.totalDuration.value > 0 - ? controller.currentPosition.value / - controller.totalDuration.value - : 0, - onChanged: (value) {}, + Text( + _formatDuration(controller.totalDuration.value), + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), ), - ), + ], ), - 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, - ), + SizedBox(height: 8.h), + // 进度条 - 使用LinearProgressIndicator,更简洁 + LinearProgressIndicator( + value: progressValue, + backgroundColor: isDarkMode ? Colors.grey[700] : Colors.grey[300], + valueColor: AlwaysStoppedAnimation( + Theme.of(Get.context!).primaryColor), + minHeight: 4.h, ), ], ); }); } + // 构建迷你播放器 - 参考qqmusic_test的设计 + Widget _buildMiniPlayer(bool isDarkMode) { + return Obx(() { + if (controller.currentSong.value == null) { + return const SizedBox.shrink(); + } + + final song = controller.currentSong.value!; + return Container( + height: 80.h, + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, -2), + ), + ], + ), + child: Column( + children: [ + // 进度条 + LinearProgressIndicator( + value: controller.totalDuration.value > 0 + ? (controller.currentPosition.value / + controller.totalDuration.value) + .clamp(0.0, 1.0) + : 0, + backgroundColor: Colors.grey[300], + valueColor: AlwaysStoppedAnimation( + Theme.of(Get.context!).primaryColor), + ), + Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Row( + children: [ + // 歌曲信息 + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + song.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14.sp, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + Text( + song.artist, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: isDarkMode + ? Colors.grey[400] + : Colors.grey[600], + fontSize: 12.sp, + ), + ), + ], + ), + ), + // 控制按钮 + Row( + children: [ + IconButton( + icon: const Icon(Icons.skip_previous), + onPressed: controller.previousSong, + color: isDarkMode ? Colors.white : Colors.black, + ), + IconButton( + icon: Icon( + controller.isPlaying.value + ? Icons.pause + : Icons.play_arrow, + ), + onPressed: controller.togglePlayPause, + color: Theme.of(Get.context!).primaryColor, + ), + IconButton( + icon: const Icon(Icons.skip_next), + onPressed: controller.nextSong, + color: isDarkMode ? Colors.white : Colors.black, + ), + ], + ), + ], + ), + ), + ), + ], + ), + ); + }); + } + // 构建播放控制按钮 Widget _buildPlayControls(bool isDarkMode) { return Obx(() { diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 1fdaea5df..bfbad2dfc 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -31,6 +31,8 @@ import com.yunqiinnovation.agent_service.Utils import okhttp3.* import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.toRequestBody +import java.util.concurrent.TimeUnit +import java.io.IOException /** * 代理服务事件监听接口 @@ -803,6 +805,7 @@ object AgentService : CoroutineScope { var userMetadata = JSONObject() var reply = true; //是否回复 var broadcast = true //是否播发 + var nocard = false; // 如果有图片,添加图片信息到元数据 if (hasImage) { @@ -988,13 +991,16 @@ object AgentService : CoroutineScope { aiMetadata = resultmap["meta"] as String; broadcast = resultmap["broadcast"] as Boolean; reply = !(resultmap["notreply"] as Boolean); - sendEvent( - "function_call_result", mapOf( - "function_call" to functionCall.toString(), - "result" to functionCallResult.toString(), - "meta" to aiMetadata, + nocard = resultmap["nocard"] as Boolean; + if (!nocard){ + sendEvent( + "function_call_result", mapOf( + "function_call" to functionCall.toString(), + "result" to functionCallResult.toString(), + "meta" to aiMetadata, + ) ) - ) + } } catch (e: Exception) { Log.e( TAG, @@ -1589,6 +1595,7 @@ object AgentService : CoroutineScope { val metadata: MutableMap = mutableMapOf() var broadcast = true; var notreply = false; + var nocard = false; if (textStr.isNotEmpty()) { val meta = JSONObject(textStr) if (meta.has("broadcast")) { @@ -1597,6 +1604,10 @@ object AgentService : CoroutineScope { if (meta.has("notreply")) { notreply = meta["notreply"] as Boolean } + if (meta.has("nocard")) { + nocard = meta["nocard"] as Boolean + } + metadata[toolname] = meta // 现在可以赋值 metadata["notreply"] = notreply // 现在可以赋值 val metaStr = JSONObject(metadata as Map<*, *>).toString() @@ -1657,13 +1668,13 @@ object AgentService : CoroutineScope { val end = cardNavigation.optString("end", "") processNavigation(start, end); } - return mapOf("meta" to metaStr, "broadcast" to broadcast, "notreply" to notreply); + return mapOf("meta" to metaStr, "broadcast" to broadcast, "notreply" to notreply,"nocard" to nocard); } } catch (e: Exception) { // 捕获其他类型的异常(可选) println("为解析到卡片数据!") } - return mapOf("meta" to "", "broadcast" to true, "notreply" to false); + return mapOf("meta" to "", "broadcast" to true, "notreply" to false,"nocard" to false); } /** @@ -1728,33 +1739,80 @@ object AgentService : CoroutineScope { /* - * 发送统计事件 + * 发送统计事件 - 安全版本 * */ - fun sendStatisticsEvent(actions:Map) { - val client = OkHttpClient() + fun sendStatisticsEvent(actions: Map) { + // 参数验证 + if (actions.isEmpty()) { + Log.w(TAG, "统计事件数据为空,跳过发送") + return + } + + // 检查服务器配置 + if (serverurl.isEmpty()) { + Log.w(TAG, "服务器地址未配置,跳过统计事件发送") + return + } + + if (usertoken.isEmpty()) { + Log.w(TAG, "用户令牌未配置,跳过统计事件发送") + return + } - // 外层 JSON 对象 - val jsonObject = JSONObject() - val actionsObject = JSONObject() + // 使用协程异步处理,避免阻塞主线程 + launch(Dispatchers.IO) { + try { + // 配置带超时的 OkHttpClient + val client = OkHttpClient.Builder() + .connectTimeout(5, TimeUnit.SECONDS) + .writeTimeout(5, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .build() + + // 构建 JSON 数据 + val jsonObject = JSONObject() + val actionsObject = JSONObject() + + for ((key, value) in actions) { + actionsObject.put(key, value) + } - for ((key, value) in actions) { - actionsObject.put(key, value) - } + jsonObject.put("actions", actionsObject) + + val json = jsonObject.toString() + val mediaType = "application/json".toMediaType() + val body = json.toRequestBody(mediaType) + + // 构建请求 + val request = Request.Builder() + .url("$serverurl/api/home/user_pushaction") + .addHeader("Authorization", usertoken) + .addHeader("Content-Type", "application/json") + .post(body) + .build() + + // 使用异步调用 + client.newCall(request).enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + Log.w(TAG, "发送统计事件失败: ${e.message}") + // 不抛出异常,只记录日志 + } - jsonObject.put("actions", actionsObject) - - val json = jsonObject.toString() - val mediaType = "application/json".toMediaType() - val body = json.toRequestBody(mediaType) - - val request = Request.Builder() - .url("$serverurl/api/home/user_pushaction") - .addHeader("Authorization", usertoken) // 添加 Token - .post(body) - .build() - Thread { - val response = client.newCall(request).execute() - // println("sendStatisticsEvent Response: ${response.body?.string()}") - }.start() + override fun onResponse(call: Call, response: Response) { + response.use { + if (response.isSuccessful) { + Log.d(TAG, "统计事件发送成功") + } else { + Log.w(TAG, "统计事件发送失败,HTTP状态码: ${response.code}") + } + } + } + }) + + } catch (e: Exception) { + // 捕获所有异常,防止崩溃 + Log.e(TAG, "发送统计事件时发生异常: ${e.message}", e) + } + } } } \ 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 index 5ad158337..8bb74a317 100644 --- 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 @@ -21,7 +21,7 @@ class QQMusicSystemFunctionHandler(private val context: Context? = null) { // Only register QQ Music search function client.registerLocalFunction( "qqmusic_search_songs", - "在QQ音乐中搜索内容,支持歌曲、专辑、歌手等搜索类型", + "在QQ音乐中搜索歌曲,支持歌曲、专辑、歌手等搜索类型", mapOf( "type" to "object", "properties" to mapOf( @@ -31,7 +31,7 @@ class QQMusicSystemFunctionHandler(private val context: Context? = null) { ), "searchType" to mapOf( "type" to "integer", - "description" to "搜索类型:0-歌曲,1-专辑,2-歌手", + "description" to "搜索类型:0-歌名搜索,1-歌手搜索,2-歌曲类型搜索", "default" to 0 ) ), @@ -63,16 +63,16 @@ class QQMusicSystemFunctionHandler(private val context: Context? = null) { ) // QQ音乐播放控制 - // client.registerLocalFunction( - // "qqmusic_play", - // "播放QQ音乐", - // mapOf( - // "type" to "object", - // "properties" to emptyMap(), - // "required" to emptyList() - // ), - // QQMusicPlayHandler(context) - // ) + client.registerLocalFunction( + "qqmusic_play", + "播放QQ音乐", + mapOf( + "type" to "object", + "properties" to emptyMap(), + "required" to emptyList() + ), + QQMusicPlayHandler(context) + ) client.registerLocalFunction( "qqmusic_pause", @@ -162,7 +162,6 @@ class QQMusicSystemFunctionHandler(private val context: Context? = null) { ), QQMusicGetPlayTimeHandler(context) ) - Log.d(TAG, "QQ音乐所有功能注册完成") } catch (e: Exception) { Log.e(TAG, "注册QQ音乐功能失败: ${e.message}", e) @@ -171,7 +170,6 @@ class QQMusicSystemFunctionHandler(private val context: Context? = null) { } - private class QQMusicLoginHandler(private val context: Context?) : FunctionHandler { override suspend fun handle(arguments: Map): String { return try { @@ -207,10 +205,10 @@ private class QQMusicSearchHandler(private val context: Context?) : FunctionHand var result = "" // 直接调用搜索,内部会自动处理登录检查 - qqMusicSingleton.searchSongs(keyword, searchType) { searchResult -> + qqMusicSingleton.searchSongs(keyword) { searchResult -> result = if (searchResult.isSuccess) { val songs = searchResult.getOrNull() ?: emptyList() - + // 根据搜索类型处理结果 val processedSongs = when (searchType) { 0 -> { // 歌曲搜索,只返回第一个结果 @@ -220,7 +218,7 @@ private class QQMusicSearchHandler(private val context: Context?) : FunctionHand songs } } - + val songsJson = processedSongs.joinToString(",") { song -> val songMap = song as Map // 添加调试日志 @@ -234,15 +232,7 @@ private class QQMusicSearchHandler(private val context: Context?) : FunctionHand val duration = songMap["duration"] ?: 0 "{\"mid\": \"$mid\", \"name\": \"$name\", \"artist\": \"$artist\", \"album\": \"$album\", \"duration\":$duration,\"pic\": \"$pic\"}" } - - val searchTypeText = when (searchType) { - 0 -> "歌曲" - 1 -> "专辑" - 2 -> "歌手" - else -> "内容" - } - - "{\"success\": true, \"message\": \"${searchTypeText}搜索成功\", \"searchType\": $searchType, \"data\": [$songsJson]}" + "{\"success\": true, \"message\": \"搜索成功\", \"data\": [$songsJson]}" } else { val error = searchResult.exceptionOrNull() "{\"success\": false, \"message\": \"搜索失败: ${error?.message}\"}" @@ -270,19 +260,19 @@ private class QQMusicPlaySongsHandler(private val context: Context?) : FunctionH qqMusicSingleton.playSongs(songMids, index) { playResult -> result = if (playResult.isSuccess) { - "{\"success\": true, \"message\": \"开始播放歌曲列表\"}" + "{\"nocard\": true, \"success\": true, \"message\": \"开始播放歌曲列表\"}" } else { val error = playResult.exceptionOrNull() - "{\"success\": false, \"message\": \"播放失败: ${error?.message}\"}" + "{\"nocard\": true, \"success\": false, \"message\": \"播放失败: ${error?.message}\"}" } } // 等待回调完成 delay(3000) - result.ifEmpty { "{\"success\": false, \"message\": \"播放超时\"}" } + result.ifEmpty { "{\"nocard\": true, \"success\": false, \"message\": \"播放超时\"}" } } catch (e: Exception) { Log.e("QQMusicPlaySongsHandler", "QQ音乐播放歌曲失败", e) - "{\"success\": false, \"message\": \"播放异常: ${e.message}\"}" + "{\"nocard\": true, \"success\": false, \"message\": \"播放异常: ${e.message}\"}" } } } 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 ac4c4ff27..90d78f91a 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 @@ -406,6 +406,113 @@ class QQMusicManager(private val activity: Activity) { } } + /** + * 同步当前播放信息 - 一次性获取所有播放状态 + */ + fun syncCurrentPlayInfo(callback: InfoCallback) { + if (!isBindQQMusicService || qqmusicApi == null) { + Log.w(TAG, "QQ音乐服务未连接,无法同步播放信息") + callback.onError(-1, "QQ音乐服务未连接") + return + } + + try { + Log.d(TAG, "开始同步当前播放信息") + + // 获取当前歌曲信息 + val songResult = qqmusicApi?.execute("getCurrentSong", null) + val songCode = songResult?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 + + // 获取播放状态 + val stateResult = qqmusicApi?.execute("getPlaybackState", null) + val stateCode = stateResult?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 + + // 获取当前播放时间 + val currTimeResult = qqmusicApi?.execute("getCurrTime", null) + val currTimeCode = currTimeResult?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 + + // 获取总时长 + val totalTimeResult = qqmusicApi?.execute("getTotalTime", null) + val totalTimeCode = totalTimeResult?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 + + Log.d(TAG, "API调用结果 - song: $songCode, state: $stateCode, currTime: $currTimeCode, totalTime: $totalTimeCode") + + // 构建返回数据 + val syncData = mutableMapOf() + + // 处理歌曲信息 + if (songCode == ErrorCodes.ERROR_OK) { + val songJson = songResult?.getString(Keys.API_RETURN_KEY_DATA) + if (!songJson.isNullOrEmpty()) { + try { + val songData = JsonParser.parseString(songJson).asJsonObject + syncData["currentSong"] = mapOf( + "id" to (songData.get("id")?.asString ?: ""), + "mid" to (songData.get("mid")?.asString ?: ""), + "name" to (songData.get("title")?.asString ?: ""), + "singer" to (songData.get("singer")?.asJsonObject?.get("title")?.asString ?: ""), + "album" to (songData.get("album")?.asJsonObject?.get("title")?.asString ?: ""), + "duration" to (songData.get("duration")?.asLong ?: 0L), + "coverUri" to (songData.get("album")?.asJsonObject?.get("coverUri")?.asString ?: "") + ) + Log.d(TAG, "歌曲信息同步成功: ${songData.get("title")?.asString}") + } catch (e: Exception) { + Log.e(TAG, "解析歌曲信息失败", e) + syncData["currentSong"] = null + } + } else { + syncData["currentSong"] = null + } + } else { + syncData["currentSong"] = null + } + + // 处理播放状态 - 使用正确的常量 + if (stateCode == ErrorCodes.ERROR_OK) { + val playState = stateResult?.getInt(Keys.API_RETURN_KEY_DATA) ?: 0 + val isPlaying = when (playState) { + PlayState.STARTED -> true + PlayState.PREPARED -> true + PlayState.BUFFERING -> true + PlayState.PAUSED -> false + PlayState.STOPPED -> false + PlayState.ERROR -> false + else -> false + } + syncData["isPlaying"] = isPlaying + syncData["playState"] = playState + Log.d(TAG, "播放状态同步成功: $playState (isPlaying: $isPlaying)") + } else { + syncData["isPlaying"] = false + syncData["playState"] = PlayState.STOPPED + } + + // 处理播放时间 + if (currTimeCode == ErrorCodes.ERROR_OK && totalTimeCode == ErrorCodes.ERROR_OK) { + val currentTime = (currTimeResult?.getLong(Keys.API_RETURN_KEY_DATA) ?: 0) / 1000 + val totalTime = (totalTimeResult?.getLong(Keys.API_RETURN_KEY_DATA) ?: 0) / 1000 + + syncData["currentTime"] = currentTime + syncData["totalTime"] = totalTime + + Log.d(TAG, "播放时间同步成功: ${currentTime}s / ${totalTime}s") + } else { + syncData["currentTime"] = 0L + syncData["totalTime"] = 0L + } + + // 添加同步时间戳 + syncData["syncTimestamp"] = System.currentTimeMillis() + + Log.d(TAG, "播放信息同步完成") + callback.onSuccess(syncData) + + } catch (e: Exception) { + Log.e(TAG, "同步播放信息异常", e) + callback.onError(-1, "同步播放信息异常: ${e.message}") + } + } + /** * 获取播放状态 */ 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 index 62f43e661..54af8bf91 100644 --- 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 @@ -22,6 +22,11 @@ class QQMusicSingleton private constructor() { private const val KEY_OPEN_ID = "open_id" private const val KEY_OPEN_TOKEN = "open_token" private const val KEY_EXPIRE_TIME = "expire_time" + private const val LOGIN_WAIT_DELAY_MS = 1000L + // 添加搜索重试相关常量 + private const val SEARCH_TIMEOUT_MS = 50000L + private const val MAX_RETRY_COUNT = 3 + private const val RETRY_DELAY_MS = 1000L @Volatile private var INSTANCE: QQMusicSingleton? = null @@ -39,6 +44,8 @@ class QQMusicSingleton private constructor() { private val handler = Handler(Looper.getMainLooper()) private var sharedPreferences: SharedPreferences? = null private var currentLoginResult: LoginResult? = null + // 添加搜索超时跟踪 + private val searchTimeouts = mutableMapOf() /** * 初始化单例,设置Activity上下文 @@ -126,12 +133,24 @@ class QQMusicSingleton private constructor() { } /** - * 搜索歌曲(带自动登录检查) + * 搜索歌曲(带自动登录检查和重试机制) */ fun searchSongs( keyword: String, searchType: Int = Data.SearchType.SEARCH_TYPE_SONG, callback: (Result>>) -> Unit + ) { + searchWithRetry(keyword, searchType, 0, callback) + } + + /** + * 带重试的搜索实现 + */ + private fun searchWithRetry( + keyword: String, + searchType: Int, + retryCount: Int, + callback: (Result>>) -> Unit ) { val currentActivity = activity if (currentActivity == null) { @@ -144,101 +163,99 @@ class QQMusicSingleton private constructor() { Log.d(TAG, "搜索前检测到未登录,开始自动登录") login { loginResult -> if (loginResult.isSuccess) { - Log.d(TAG, "自动登录成功,继续搜索") - performSearch(keyword, searchType, callback) + Log.d(TAG, "自动登录成功,等待QQ音乐服务连接...") + handler.postDelayed({ + Log.d(TAG, "等待完成,开始搜索") + performSearchWithTimeout(keyword, searchType, retryCount, callback) + }, LOGIN_WAIT_DELAY_MS) } else { callback(Result.failure(Exception("自动登录失败: ${loginResult.exceptionOrNull()?.message}"))) } } } else { - performSearch(keyword, searchType, callback) + performSearchWithTimeout(keyword, searchType, retryCount, callback) } } /** - * 执行搜索操作 + * 执行带超时的搜索操作 */ - private fun performSearch( + private fun performSearchWithTimeout( keyword: String, searchType: Int, + retryCount: Int, callback: (Result>>) -> Unit ) { val currentActivity = activity ?: return + val searchKey = "${keyword}_${System.currentTimeMillis()}" + var completed = false + + Log.d(TAG, "开始搜索: $keyword (第${retryCount + 1}次)") + + // 设置超时 + val timeoutRunnable = Runnable { + if (!completed) { + completed = true + searchTimeouts.remove(searchKey) + Log.w(TAG, "搜索超时: $keyword") + + if (retryCount < MAX_RETRY_COUNT) { + Log.d(TAG, "准备重试: ${retryCount + 1}/$MAX_RETRY_COUNT") + handler.postDelayed({ + searchWithRetry(keyword, searchType, retryCount + 1, callback) + }, RETRY_DELAY_MS) + } else { + callback(Result.failure(Exception("搜索超时,已重试${MAX_RETRY_COUNT}次"))) + } + } + } + + searchTimeouts[searchKey] = timeoutRunnable + handler.postDelayed(timeoutRunnable, SEARCH_TIMEOUT_MS) 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)) + if (!completed) { + completed = true + searchTimeouts.remove(searchKey)?.let { handler.removeCallbacks(it) } + 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() + if (!completed) { + completed = true + searchTimeouts.remove(searchKey)?.let { handler.removeCallbacks(it) } + Log.e(TAG, "搜索失败: code=$errorCode, message=$errorMessage") + + // 如果是登录相关错误,清除本地登录信息 + if (errorMessage.contains("登录") || errorMessage.contains("授权")) { + Log.d(TAG, "检测到登录相关错误,清除本地登录信息") + logout() + } + + // 网络相关错误才重试 + if (retryCount < MAX_RETRY_COUNT && + (errorMessage.contains("网络") || errorMessage.contains("超时") || + errorMessage.contains("连接") || errorCode == -1)) { + Log.d(TAG, "网络错误,准备重试: ${retryCount + 1}/$MAX_RETRY_COUNT") + handler.postDelayed({ + searchWithRetry(keyword, searchType, retryCount + 1, callback) + }, RETRY_DELAY_MS) + } else { + callback(Result.failure(Exception("搜索失败: $errorMessage"))) + } } - 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, "本地登录信息已清除") - } - /** * 播放歌曲列表 */ @@ -374,10 +391,43 @@ class QQMusicSingleton private constructor() { }) } + /** + * 同步当前播放信息 - 统一接口 + */ + fun syncCurrentPlayInfo(callback: (Result>) -> Unit) { + val currentActivity = activity + if (currentActivity == null) { + callback(Result.failure(Exception("Activity not available"))) + return + } + + ensureMusicManager(currentActivity) + musicManager?.syncCurrentPlayInfo(object : QQMusicManager.InfoCallback { + override fun onSuccess(data: Map) { + handler.post { + Log.d(TAG, "同步播放信息成功") + callback(Result.success(data)) + } + } + + override fun onError(errorCode: Int, errorMessage: String) { + handler.post { + Log.e(TAG, "同步播放信息失败: $errorMessage") + callback(Result.failure(Exception("同步播放信息失败: $errorMessage"))) + } + } + }) + } + /** * 清理资源 */ fun cleanup() { + // 清理搜索超时任务 + searchTimeouts.values.forEach { handler.removeCallbacks(it) } + searchTimeouts.clear() + + // 清理管理器 loginManager?.cleanup() loginManager = null musicManager?.cleanup() @@ -386,6 +436,57 @@ class QQMusicSingleton private constructor() { Log.d(TAG, "QQ音乐单例资源清理完成") } + /** + * 保存登录信息到本地 + */ + 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, "本地登录信息已清除") + } + // 私有辅助方法 private fun ensureMusicManager(activity: Activity) { if (musicManager == null) { 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 7a0b9fe4c..e339988ed 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 @@ -177,6 +177,21 @@ class QqMusicPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { ) } } + "syncCurrentPlayInfo" -> { + qqMusicSingleton.syncCurrentPlayInfo { kotlinResult -> + kotlinResult.fold( + onSuccess = { data -> + result.success(mapOf( + "success" to true, + "data" to data + )) + }, + onFailure = { error -> + result.error("SYNC_FAILED", error.message, null) + } + ) + } + } "getPlatformVersion" -> { result.success("Android ${android.os.Build.VERSION.RELEASE}") } diff --git a/local_plugins/qq_music/lib/qq_music.dart b/local_plugins/qq_music/lib/qq_music.dart index 51af20f64..3e3bda767 100644 --- a/local_plugins/qq_music/lib/qq_music.dart +++ b/local_plugins/qq_music/lib/qq_music.dart @@ -170,6 +170,16 @@ class QqMusic { }; } } + + /// 同步当前播放信息 + Future> syncCurrentPlayInfo() async { + try { + final result = await _methodChannel.invokeMethod('syncCurrentPlayInfo'); + return Map.from(result); + } catch (e) { + throw Exception('同步播放信息失败: $e'); + } + } } /// 登录结果类