Browse Source

上传代码

weicu
liwei1dao 1 year ago
parent
commit
98586a6ab8
  1. 60
      lib/data/models/song_model.dart
  2. 239
      lib/data/services/qq_musoc.dart
  3. 235
      lib/modules/qqmusic/controllers/qqmusic_playlist_controller.dart
  4. 49
      lib/modules/qqmusic/controllers/song_model.dart
  5. 204
      lib/modules/qqmusic/views/qqmusic_playlist_view.dart
  6. 122
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  7. 50
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt
  8. 107
      local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicManager.kt
  9. 231
      local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt
  10. 15
      local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QqMusicPlugin.kt
  11. 10
      local_plugins/qq_music/lib/qq_music.dart

60
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<String, dynamic> 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<String, dynamic> 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;
}

239
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<QqMusocService>();
@ -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<SongModel>();
final playlist = <SongModel>[].obs;
final currentIndex = 0.obs;
// 状态监听定时器
Timer? _stateMonitorTimer;
// 监听器列表 - 用于通知页面状态变化
final List<Function()> _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<void> _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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> playSongs({
required List<String> songMids,
int index = 0,
List<SongModel>? 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> syncCurrentPlayInfo() async {
try {
final result = await _qqMusic.syncCurrentPlayInfo();
return result;
} catch (e) {
return {'success': false, 'error': '同步播放信息失败: ${e.toString()}'};
}
}
}

235
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<QqMusocService>();
final QqMusocService _qqMusicService = QqMusocService.to;
// 登录状态
// 登录相关
final isLoggedIn = false.obs;
final userInfo = Rxn<QQMusicLoginResult>();
final userInfo = Rxn<Map<String, dynamic>>();
// 歌单数据
final playlist = <SongModel>[].obs;
final currentSong = Rxn<SongModel>();
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<SongModel> get currentSong => _qqMusicService.currentSong;
RxList<SongModel> get playlist => _qqMusicService.playlist;
RxInt get currentIndex => _qqMusicService.currentIndex;
@override
void onInit() {
super.onInit();
// 从路由参数获取歌单数据
final List<Map<String, dynamic>> playlistData = Get.arguments ?? [];
final songs = playlistData.map((songData) {
return SongModel(
mid: songData['mid'] ?? '',
name: songData['name'] ?? '',
artist: songData['singer'] ?? '',
album: songData['album'] ?? '',
albumPic: songData['pic'],
duration: songData['duration'] ?? 0,
);
}).toList();
currentSong.value = songs[0];
playlist.assignAll(songs);
// 从路由参数获取播放列表
final arguments = Get.arguments;
if (arguments != null && arguments['playlist'] != null) {
final List<dynamic> 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<void> playSong(SongModel song, {List<SongModel>? songList}) async {
try {
currentSong.value = song;
if (songList != null) {
playlist.assignAll(songList);
currentIndex.value = songList.indexOf(song);
} else {
playlist.assignAll([song]);
currentIndex.value = 0;
}
// 获取歌曲MID列表
final songMids = playlist.map((s) => s.mid).toList();
final playIndex = currentIndex.value;
final List<SongModel> 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<void> togglePlayPause() async {
try {
if (isPlaying.value) {
final result = await _qqMusic.pauseMusic();
if (result['success'] == true) {
isPlaying.value = false;
}
} else {
final result = await _qqMusic.resumeMusic();
if (result['success'] == true) {
isPlaying.value = true;
}
}
} catch (e) {
Get.snackbar('操作失败', e.toString());
}
}
/// 上一首
Future<void> previousSong() async {
try {
final result = await _qqMusic.skipToPrevious();
if (result['success'] == true) {
if (playlist.isNotEmpty) {
int newIndex = currentIndex.value - 1;
if (newIndex < 0) newIndex = playlist.length - 1;
currentIndex.value = newIndex;
currentSong.value = playlist[newIndex];
}
await _updateCurrentSong();
}
} catch (e) {
Get.snackbar('切换失败', e.toString());
if (isPlaying.value) {
await _qqMusicService.pauseMusic();
} else {
await _qqMusicService.resumeMusic();
}
}
/// 下一首
Future<void> nextSong() async {
try {
final result = await _qqMusic.skipToNext();
if (result['success'] == true) {
if (playlist.isNotEmpty) {
int newIndex = currentIndex.value + 1;
if (newIndex >= playlist.length) newIndex = 0;
currentIndex.value = newIndex;
currentSong.value = playlist[newIndex];
}
await _updateCurrentSong();
}
} catch (e) {
Get.snackbar('切换失败', e.toString());
}
await _qqMusicService.skipToNext();
}
/// 停止播放
Future<void> stopMusic() async {
try {
final result = await _qqMusic.stopMusic();
if (result['success'] == true) {
isPlaying.value = false;
currentPosition.value = 0;
}
} catch (e) {
Get.snackbar('停止失败', e.toString());
}
}
/// 更新当前播放歌曲信息
Future<void> _updateCurrentSong() async {
try {
final result = await _qqMusic.getCurrentSong();
if (result['success'] == true && result['song'] != null) {
final songData = result['song'];
final song = SongModel(
mid: songData['mid'] ?? '',
name: songData['name'] ?? '',
artist: songData['singer'] ?? '',
album: songData['album'] ?? '',
albumPic: songData['pic'],
duration: songData['duration'] ?? 0,
);
currentSong.value = song;
}
} catch (e) {
print('更新当前歌曲信息失败: $e');
}
}
/// 启动播放状态监控
void _startPlaybackStateMonitor() {
// 每秒检查一次播放状态和时间
Stream.periodic(const Duration(seconds: 1)).listen((_) async {
if (isLoggedIn.value) {
await _updatePlaybackState();
await _updatePlayTime();
}
});
}
/// 更新播放状态
Future<void> _updatePlaybackState() async {
try {
final result = await _qqMusic.getPlaybackState();
if (result['success'] == true && result['state'] != null) {
final state = result['state'];
// QQ音乐播放状态:0-停止,1-播放,2-暂停
isPlaying.value = state == 1;
}
} catch (e) {
// 静默处理错误,避免频繁弹窗
}
}
/// 更新播放时间
Future<void> _updatePlayTime() async {
try {
final result = await _qqMusic.getPlayTime();
if (result['success'] == true) {
currentPosition.value = result['currentTime'] ?? 0;
totalDuration.value = result['totalTime'] ?? 0;
}
} catch (e) {
// 静默处理错误,避免频繁弹窗
}
/// 上一首
Future<void> previousSong() async {
await _qqMusicService.skipToPrevious();
}
/// 跳转到指定位置(暂时保留,QQ音乐可能不支持)
void seekTo(int position) {
currentPosition.value = position;
// QQ音乐SDK可能不支持跳转功能
Get.snackbar('提示', '当前版本不支持跳转功能');
/// 停止播放
Future<void> stopMusic() async {
await _qqMusicService.stopMusic();
}
@override
void onClose() {
// 清理资源
super.onClose();
/// 格式化时间
String formatDuration(int milliseconds) {
return _qqMusicService.formatDuration(milliseconds);
}
}

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

@ -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<String, dynamic> json) {
return SongModel(
mid: json['mid'] ?? '',
name: json['name'] ?? '',
artist: json['artist'] ?? '',
album: json['album'] ?? '',
albumPic: json['albumPic'] ?? '',
duration: json['duration'] ?? 0,
isFavorite: json['isFavorite'] ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'mid': mid,
'name': name,
'artist': artist,
'album': album,
'albumPic': albumPic,
'duration': duration,
'isFavorite': isFavorite,
};
}
String get durationText {
final minutes = duration ~/ 60;
final seconds = duration % 60;
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
}

204
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<QQMusicPlaylistController> {
@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<QQMusicPlaylistController> {
),
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<QQMusicPlaylistController> {
)
: 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<QQMusicPlaylistController> {
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<QQMusicPlaylistController> {
);
}
// 构建进度条
// 构建进度条 - 参考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<Color>(
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<Color>(
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(() {

122
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<String, Any> = 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<String,Int>) {
val client = OkHttpClient()
fun sendStatisticsEvent(actions: Map<String, Int>) {
// 参数验证
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)
}
}
}
}

50
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<String, Any>(),
// "required" to emptyList<String>()
// ),
// QQMusicPlayHandler(context)
// )
client.registerLocalFunction(
"qqmusic_play",
"播放QQ音乐",
mapOf(
"type" to "object",
"properties" to emptyMap<String, Any>(),
"required" to emptyList<String>()
),
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, Any>): 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<String, Any?>
// 添加调试日志
@ -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}\"}"
}
}
}

107
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<String, Any?>()
// 处理歌曲信息
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}")
}
}
/**
* 获取播放状态
*/

231
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<String, Runnable>()
/**
* 初始化单例,设置Activity上下文
@ -126,12 +133,24 @@ class QQMusicSingleton private constructor() {
}
/**
* 搜索歌曲(带自动登录检查)
* 搜索歌曲(带自动登录检查和重试机制)
*/
fun searchSongs(
keyword: String,
searchType: Int = Data.SearchType.SEARCH_TYPE_SONG,
callback: (Result<List<Map<String, Any?>>>) -> Unit
) {
searchWithRetry(keyword, searchType, 0, callback)
}
/**
* 带重试的搜索实现
*/
private fun searchWithRetry(
keyword: String,
searchType: Int,
retryCount: Int,
callback: (Result<List<Map<String, Any?>>>) -> 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<List<Map<String, Any?>>>) -> 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<Map<String, Any?>>) {
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<Map<String, Any?>>) -> 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<String, Any?>) {
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) {

15
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}")
}

10
local_plugins/qq_music/lib/qq_music.dart

@ -170,6 +170,16 @@ class QqMusic {
};
}
}
/// 同步当前播放信息
Future<Map<String, dynamic>> syncCurrentPlayInfo() async {
try {
final result = await _methodChannel.invokeMethod('syncCurrentPlayInfo');
return Map<String, dynamic>.from(result);
} catch (e) {
throw Exception('同步播放信息失败: $e');
}
}
}
/// 登录结果类

Loading…
Cancel
Save