Browse Source

上传代码

newdev_shunjiawei
liwei1dao 1 year ago
parent
commit
667af20420
  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:get/get.dart';
import 'package:qq_music/qq_music.dart'; import 'package:qq_music/qq_music.dart';
import '../models/song_model.dart';
/// QQ音乐服务适配器 /// QQ音乐服务适配器 - 全局状态管理
class QqMusocService extends GetxService { class QqMusocService extends GetxService {
static QqMusocService get to => Get.find<QqMusocService>(); static QqMusocService get to => Get.find<QqMusocService>();
@ -9,19 +11,173 @@ class QqMusocService extends GetxService {
// 音乐服务实例 // 音乐服务实例
final QqMusic _qqMusic = QqMusic(); 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 @override
void onInit() { void onInit() {
super.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音乐 /// 登录QQ音乐
Future<Map<String, dynamic>> login() async { Future<Map<String, dynamic>> login() async {
try { try {
// 直接调用QQ音乐插件的登录方法
final result = await _qqMusic.login(); final result = await _qqMusic.login();
// 检查登录结果
if (result['success'] == true) { if (result['success'] == true) {
isInitialized = true; isInitialized = true;
return result; return result;
@ -36,7 +192,7 @@ class QqMusocService extends GetxService {
/// 搜索歌曲 /// 搜索歌曲
Future<Map<String, dynamic>> searchSongs({ Future<Map<String, dynamic>> searchSongs({
required String keyword, required String keyword,
int searchType = 0, // SEARCH_TYPE_SONG int searchType = 0,
}) async { }) async {
try { try {
final result = await _qqMusic.searchSongs( final result = await _qqMusic.searchSongs(
@ -49,16 +205,41 @@ class QqMusocService extends GetxService {
} }
} }
/// 播放歌曲列表 /// 播放歌曲列表 - 更新全局状态
Future<Map<String, dynamic>> playSongs({ Future<Map<String, dynamic>> playSongs({
required List<String> songMids, required List<String> songMids,
int index = 0, int index = 0,
List<SongModel>? songList,
SongModel? currentSongModel,
}) async { }) async {
try { try {
final result = await _qqMusic.playSongs( final result = await _qqMusic.playSongs(
songMids: songMids, songMids: songMids,
index: index, 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; return result;
} catch (e) { } catch (e) {
return {'success': false, 'error': '播放失败: ${e.toString()}'}; return {'success': false, 'error': '播放失败: ${e.toString()}'};
@ -69,6 +250,11 @@ class QqMusocService extends GetxService {
Future<Map<String, dynamic>> playMusic() async { Future<Map<String, dynamic>> playMusic() async {
try { try {
final result = await _qqMusic.playMusic(); final result = await _qqMusic.playMusic();
if (result['success'] == true) {
isPlaying.value = true;
startPlaybackStateMonitor();
_notifyListeners();
}
return result; return result;
} catch (e) { } catch (e) {
return {'success': false, 'error': '播放失败: ${e.toString()}'}; return {'success': false, 'error': '播放失败: ${e.toString()}'};
@ -79,6 +265,10 @@ class QqMusocService extends GetxService {
Future<Map<String, dynamic>> pauseMusic() async { Future<Map<String, dynamic>> pauseMusic() async {
try { try {
final result = await _qqMusic.pauseMusic(); final result = await _qqMusic.pauseMusic();
if (result['success'] == true) {
isPlaying.value = false;
_notifyListeners();
}
return result; return result;
} catch (e) { } catch (e) {
return {'success': false, 'error': '暂停失败: ${e.toString()}'}; return {'success': false, 'error': '暂停失败: ${e.toString()}'};
@ -89,6 +279,11 @@ class QqMusocService extends GetxService {
Future<Map<String, dynamic>> resumeMusic() async { Future<Map<String, dynamic>> resumeMusic() async {
try { try {
final result = await _qqMusic.resumeMusic(); final result = await _qqMusic.resumeMusic();
if (result['success'] == true) {
isPlaying.value = true;
startPlaybackStateMonitor();
_notifyListeners();
}
return result; return result;
} catch (e) { } catch (e) {
return {'success': false, 'error': '继续播放失败: ${e.toString()}'}; return {'success': false, 'error': '继续播放失败: ${e.toString()}'};
@ -99,6 +294,12 @@ class QqMusocService extends GetxService {
Future<Map<String, dynamic>> stopMusic() async { Future<Map<String, dynamic>> stopMusic() async {
try { try {
final result = await _qqMusic.stopMusic(); final result = await _qqMusic.stopMusic();
if (result['success'] == true) {
isPlaying.value = false;
currentPosition.value = 0;
stopPlaybackStateMonitor();
_notifyListeners();
}
return result; return result;
} catch (e) { } catch (e) {
return {'success': false, 'error': '停止失败: ${e.toString()}'}; return {'success': false, 'error': '停止失败: ${e.toString()}'};
@ -109,6 +310,14 @@ class QqMusocService extends GetxService {
Future<Map<String, dynamic>> skipToNext() async { Future<Map<String, dynamic>> skipToNext() async {
try { try {
final result = await _qqMusic.skipToNext(); 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; return result;
} catch (e) { } catch (e) {
return {'success': false, 'error': '下一首失败: ${e.toString()}'}; return {'success': false, 'error': '下一首失败: ${e.toString()}'};
@ -119,6 +328,14 @@ class QqMusocService extends GetxService {
Future<Map<String, dynamic>> skipToPrevious() async { Future<Map<String, dynamic>> skipToPrevious() async {
try { try {
final result = await _qqMusic.skipToPrevious(); final result = await _qqMusic.skipToPrevious();
if (result['success'] == true) {
// 更新当前索引
if (currentIndex.value > 0) {
currentIndex.value--;
currentSong.value = playlist[currentIndex.value];
}
_notifyListeners();
}
return result; return result;
} catch (e) { } catch (e) {
return {'success': false, 'error': '上一首失败: ${e.toString()}'}; return {'success': false, 'error': '上一首失败: ${e.toString()}'};
@ -154,4 +371,14 @@ class QqMusocService extends GetxService {
return {'success': false, 'error': '获取播放时间失败: ${e.toString()}'}; 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:get/get.dart';
import 'package:qq_music/qq_music.dart'; import '../../../data/models/song_model.dart';
import 'package:voitrans/data/services/qq_musoc.dart'; import '../../../data/services/qq_musoc.dart';
import 'package:voitrans/modules/qqmusic/controllers/song_model.dart';
class QQMusicPlaylistController extends GetxController { class QQMusicPlaylistController extends GetxController {
final QqMusocService _qqMusic = Get.find<QqMusocService>(); final QqMusocService _qqMusicService = QqMusocService.to;
// 登录状态 // 登录相关
final isLoggedIn = false.obs; final isLoggedIn = false.obs;
final userInfo = Rxn<QQMusicLoginResult>(); final userInfo = Rxn<Map<String, dynamic>>();
// 歌单数据 // 直接使用全局服务的状态 - 修复返回类型
final playlist = <SongModel>[].obs; RxBool get isPlaying => _qqMusicService.isPlaying;
RxInt get currentPosition => _qqMusicService.currentPosition;
final currentSong = Rxn<SongModel>(); RxInt get totalDuration => _qqMusicService.totalDuration;
final isPlaying = false.obs; Rxn<SongModel> get currentSong => _qqMusicService.currentSong;
final currentPosition = 0.obs; RxList<SongModel> get playlist => _qqMusicService.playlist;
final totalDuration = 0.obs; RxInt get currentIndex => _qqMusicService.currentIndex;
final currentIndex = 0.obs;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
// 从路由参数获取歌单数据
final List<Map<String, dynamic>> playlistData = Get.arguments ?? []; // 从路由参数获取播放列表
final songs = playlistData.map((songData) { final arguments = Get.arguments;
return SongModel( if (arguments != null && arguments['playlist'] != null) {
mid: songData['mid'] ?? '', final List<dynamic> playlistData = arguments['playlist'];
name: songData['name'] ?? '', final songList =
artist: songData['singer'] ?? '', playlistData.map((item) => SongModel.fromJson(item)).toList();
album: songData['album'] ?? '', _qqMusicService.playlist.assignAll(songList);
albumPic: songData['pic'],
duration: songData['duration'] ?? 0, if (songList.isNotEmpty) {
); _qqMusicService.currentSong.value = songList.first;
}).toList(); _qqMusicService.currentIndex.value = 0;
currentSong.value = songs[0]; }
playlist.assignAll(songs); }
// 添加状态监听器
_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 { Future<void> playSong(SongModel song, {List<SongModel>? songList}) async {
try { try {
currentSong.value = song; final List<SongModel> finalSongList = songList ?? [song];
final songMids = finalSongList.map((s) => s.mid).toList();
if (songList != null) { final playIndex = finalSongList.indexOf(song);
playlist.assignAll(songList);
currentIndex.value = songList.indexOf(song);
} else {
playlist.assignAll([song]);
currentIndex.value = 0;
}
// 获取歌曲MID列表
final songMids = playlist.map((s) => s.mid).toList();
final playIndex = currentIndex.value;
// 调用QQ音乐播放接口 final result = await _qqMusicService.playSongs(
final result = await _qqMusic.playSongs(
songMids: songMids, songMids: songMids,
index: playIndex, index: playIndex,
songList: finalSongList,
currentSongModel: song,
); );
if (result['success'] == true) { if (result['success'] == true) {
isPlaying.value = true;
totalDuration.value = song.duration;
currentPosition.value = 0;
Get.snackbar('开始播放', song.name); Get.snackbar('开始播放', song.name);
} else { } else {
Get.snackbar('播放失败', result['error'] ?? '未知错误'); Get.snackbar('播放失败', result['error'] ?? '未知错误');
@ -76,141 +80,30 @@ class QQMusicPlaylistController extends GetxController {
/// 暂停/继续播放 /// 暂停/继续播放
Future<void> togglePlayPause() async { Future<void> togglePlayPause() async {
try { if (isPlaying.value) {
if (isPlaying.value) { await _qqMusicService.pauseMusic();
final result = await _qqMusic.pauseMusic(); } else {
if (result['success'] == true) { await _qqMusicService.resumeMusic();
isPlaying.value = false;
}
} else {
final result = await _qqMusic.resumeMusic();
if (result['success'] == true) {
isPlaying.value = true;
}
}
} catch (e) {
Get.snackbar('操作失败', e.toString());
}
}
/// 上一首
Future<void> previousSong() async {
try {
final result = await _qqMusic.skipToPrevious();
if (result['success'] == true) {
if (playlist.isNotEmpty) {
int newIndex = currentIndex.value - 1;
if (newIndex < 0) newIndex = playlist.length - 1;
currentIndex.value = newIndex;
currentSong.value = playlist[newIndex];
}
await _updateCurrentSong();
}
} catch (e) {
Get.snackbar('切换失败', e.toString());
} }
} }
/// 下一首 /// 下一首
Future<void> nextSong() async { Future<void> nextSong() async {
try { await _qqMusicService.skipToNext();
final result = await _qqMusic.skipToNext();
if (result['success'] == true) {
if (playlist.isNotEmpty) {
int newIndex = currentIndex.value + 1;
if (newIndex >= playlist.length) newIndex = 0;
currentIndex.value = newIndex;
currentSong.value = playlist[newIndex];
}
await _updateCurrentSong();
}
} catch (e) {
Get.snackbar('切换失败', e.toString());
}
} }
/// 停止播放 /// 上一首
Future<void> stopMusic() async { Future<void> previousSong() async {
try { await _qqMusicService.skipToPrevious();
final result = await _qqMusic.stopMusic();
if (result['success'] == true) {
isPlaying.value = false;
currentPosition.value = 0;
}
} catch (e) {
Get.snackbar('停止失败', e.toString());
}
}
/// 更新当前播放歌曲信息
Future<void> _updateCurrentSong() async {
try {
final result = await _qqMusic.getCurrentSong();
if (result['success'] == true && result['song'] != null) {
final songData = result['song'];
final song = SongModel(
mid: songData['mid'] ?? '',
name: songData['name'] ?? '',
artist: songData['singer'] ?? '',
album: songData['album'] ?? '',
albumPic: songData['pic'],
duration: songData['duration'] ?? 0,
);
currentSong.value = song;
}
} catch (e) {
print('更新当前歌曲信息失败: $e');
}
}
/// 启动播放状态监控
void _startPlaybackStateMonitor() {
// 每秒检查一次播放状态和时间
Stream.periodic(const Duration(seconds: 1)).listen((_) async {
if (isLoggedIn.value) {
await _updatePlaybackState();
await _updatePlayTime();
}
});
}
/// 更新播放状态
Future<void> _updatePlaybackState() async {
try {
final result = await _qqMusic.getPlaybackState();
if (result['success'] == true && result['state'] != null) {
final state = result['state'];
// QQ音乐播放状态:0-停止,1-播放,2-暂停
isPlaying.value = state == 1;
}
} catch (e) {
// 静默处理错误,避免频繁弹窗
}
}
/// 更新播放时间
Future<void> _updatePlayTime() async {
try {
final result = await _qqMusic.getPlayTime();
if (result['success'] == true) {
currentPosition.value = result['currentTime'] ?? 0;
totalDuration.value = result['totalTime'] ?? 0;
}
} catch (e) {
// 静默处理错误,避免频繁弹窗
}
} }
/// 跳转到指定位置(暂时保留,QQ音乐可能不支持) /// 停止播放
void seekTo(int position) { Future<void> stopMusic() async {
currentPosition.value = position; await _qqMusicService.stopMusic();
// QQ音乐SDK可能不支持跳转功能
Get.snackbar('提示', '当前版本不支持跳转功能');
} }
@override /// 格式化时间
void onClose() { String formatDuration(int milliseconds) {
// 清理资源 return _qqMusicService.formatDuration(milliseconds);
super.onClose();
} }
} }

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/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:voitrans/modules/qqmusic/controllers/qqmusic_playlist_controller.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 '../../../core/utils/logger.dart';
import '../../agent/controllers/agent_controller.dart'; import '../../agent/controllers/agent_controller.dart';
import '../../agent/views/circular_progress_button.dart'; import '../../agent/views/circular_progress_button.dart';
@ -12,14 +12,13 @@ class QqmusicPlaylistView extends GetView<QQMusicPlaylistController> {
@override @override
Widget build(BuildContext context) { 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; final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: IconButton( 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), onPressed: () => Navigator.pop(context),
), ),
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white,
@ -39,7 +38,7 @@ class QqmusicPlaylistView extends GetView<QQMusicPlaylistController> {
), ),
SizedBox(height: 16.h), SizedBox(height: 16.h),
Text( Text(
'noMusic'.tr, // 暂无音乐 'noMusic'.tr,
style: TextStyle( style: TextStyle(
fontSize: 16.sp, fontSize: 16.sp,
color: Colors.grey[600], color: Colors.grey[600],
@ -50,19 +49,21 @@ class QqmusicPlaylistView extends GetView<QQMusicPlaylistController> {
) )
: Column( : Column(
children: [ children: [
// 上部:当前播放音乐详情 - 响应式高度 // 上部:当前播放音乐详情
Container( Container(
height: MediaQuery.of(context).size.height * 0.38, // 增大高度比例 height: MediaQuery.of(context).size.height * 0.38,
child: _buildCurrentPlayingSection(isDarkMode), child: _buildCurrentPlayingSection(isDarkMode),
), ),
// 中部:播放全部按钮 // 中部:播放全部按钮
_buildPlayAllSection(isDarkMode), _buildPlayAllSection(isDarkMode),
// 下部:歌单列表 - 使用剩余空间 // 下部:歌单列表
Expanded( Expanded(
child: _buildPlaylistSection(isDarkMode), child: _buildPlaylistSection(isDarkMode),
), ),
], ],
), ),
// 添加迷你播放器
bottomNavigationBar: _buildMiniPlayer(isDarkMode),
); );
} }
@ -77,8 +78,8 @@ class QqmusicPlaylistView extends GetView<QQMusicPlaylistController> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
// 封面图片 - 响应式尺寸 // 封面图片 - 响应式尺寸
_buildCoverImage( _buildCoverImage(controller.currentSong.value!.albumPic ?? '',
controller.currentSong.value!.albumPic, isDarkMode, isDarkMode, // Handle nullable albumPic
size: 110.w), size: 110.w),
SizedBox(height: 12.h), SizedBox(height: 12.h),
// 歌曲信息 // 歌曲信息
@ -356,64 +357,155 @@ class QqmusicPlaylistView extends GetView<QQMusicPlaylistController> {
); );
} }
// 构建进度条 // 构建进度条 - 参考qqmusic_test的简洁设计
Widget _buildProgressBar(bool isDarkMode) { Widget _buildProgressBar(bool isDarkMode) {
return Obx(() { 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: [ children: [
SizedBox( // 时间显示
width: 40.w, Row(
child: Text( mainAxisAlignment: MainAxisAlignment.spaceBetween,
_formatDuration(controller.currentPosition.value), children: [
style: TextStyle( Text(
fontSize: 12.sp, _formatDuration(controller.currentPosition.value),
color: isDarkMode ? Colors.grey[400] : Colors.grey[600], style: TextStyle(
fontFeatures: const [FontFeature.tabularFigures()], fontSize: 12.sp,
), color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
textAlign: TextAlign.left, ),
),
),
SizedBox(width: 8.w),
Expanded(
child: SliderTheme(
data: SliderThemeData(
activeTrackColor: Theme.of(Get.context!).primaryColor,
inactiveTrackColor:
isDarkMode ? Colors.grey[700] : Colors.grey[300],
thumbColor: Theme.of(Get.context!).primaryColor,
overlayColor:
Theme.of(Get.context!).primaryColor.withOpacity(0.2),
trackHeight: 4.h,
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8),
overlayShape: const RoundSliderOverlayShape(overlayRadius: 14),
), ),
child: Slider( Text(
value: controller.totalDuration.value > 0 _formatDuration(controller.totalDuration.value),
? controller.currentPosition.value / style: TextStyle(
controller.totalDuration.value fontSize: 12.sp,
: 0, color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
onChanged: (value) {}, ),
), ),
), ],
), ),
SizedBox(width: 8.w), SizedBox(height: 8.h),
SizedBox( // 进度条 - 使用LinearProgressIndicator,更简洁
width: 40.w, LinearProgressIndicator(
child: Text( value: progressValue,
_formatDuration(controller.currentSong.value!.duration), backgroundColor: isDarkMode ? Colors.grey[700] : Colors.grey[300],
style: TextStyle( valueColor: AlwaysStoppedAnimation<Color>(
fontSize: 12.sp, Theme.of(Get.context!).primaryColor),
color: isDarkMode ? Colors.grey[400] : Colors.grey[600], minHeight: 4.h,
fontFeatures: const [FontFeature.tabularFigures()],
),
textAlign: TextAlign.right,
),
), ),
], ],
); );
}); });
} }
// 构建迷你播放器 - 参考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) { Widget _buildPlayControls(bool isDarkMode) {
return Obx(() { 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.*
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody 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 userMetadata = JSONObject()
var reply = true; //是否回复 var reply = true; //是否回复
var broadcast = true //是否播发 var broadcast = true //是否播发
var nocard = false;
// 如果有图片,添加图片信息到元数据 // 如果有图片,添加图片信息到元数据
if (hasImage) { if (hasImage) {
@ -988,13 +991,16 @@ object AgentService : CoroutineScope {
aiMetadata = resultmap["meta"] as String; aiMetadata = resultmap["meta"] as String;
broadcast = resultmap["broadcast"] as Boolean; broadcast = resultmap["broadcast"] as Boolean;
reply = !(resultmap["notreply"] as Boolean); reply = !(resultmap["notreply"] as Boolean);
sendEvent( nocard = resultmap["nocard"] as Boolean;
"function_call_result", mapOf( if (!nocard){
"function_call" to functionCall.toString(), sendEvent(
"result" to functionCallResult.toString(), "function_call_result", mapOf(
"meta" to aiMetadata, "function_call" to functionCall.toString(),
"result" to functionCallResult.toString(),
"meta" to aiMetadata,
)
) )
) }
} catch (e: Exception) { } catch (e: Exception) {
Log.e( Log.e(
TAG, TAG,
@ -1589,6 +1595,7 @@ object AgentService : CoroutineScope {
val metadata: MutableMap<String, Any> = mutableMapOf() val metadata: MutableMap<String, Any> = mutableMapOf()
var broadcast = true; var broadcast = true;
var notreply = false; var notreply = false;
var nocard = false;
if (textStr.isNotEmpty()) { if (textStr.isNotEmpty()) {
val meta = JSONObject(textStr) val meta = JSONObject(textStr)
if (meta.has("broadcast")) { if (meta.has("broadcast")) {
@ -1597,6 +1604,10 @@ object AgentService : CoroutineScope {
if (meta.has("notreply")) { if (meta.has("notreply")) {
notreply = meta["notreply"] as Boolean notreply = meta["notreply"] as Boolean
} }
if (meta.has("nocard")) {
nocard = meta["nocard"] as Boolean
}
metadata[toolname] = meta // 现在可以赋值 metadata[toolname] = meta // 现在可以赋值
metadata["notreply"] = notreply // 现在可以赋值 metadata["notreply"] = notreply // 现在可以赋值
val metaStr = JSONObject(metadata as Map<*, *>).toString() val metaStr = JSONObject(metadata as Map<*, *>).toString()
@ -1657,13 +1668,13 @@ object AgentService : CoroutineScope {
val end = cardNavigation.optString("end", "") val end = cardNavigation.optString("end", "")
processNavigation(start, 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) { } catch (e: Exception) {
// 捕获其他类型的异常(可选) // 捕获其他类型的异常(可选)
println("为解析到卡片数据!") 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>) { fun sendStatisticsEvent(actions: Map<String, Int>) {
val client = OkHttpClient() // 参数验证
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() launch(Dispatchers.IO) {
val actionsObject = JSONObject() 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) { jsonObject.put("actions", actionsObject)
actionsObject.put(key, value)
} 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) override fun onResponse(call: Call, response: Response) {
response.use {
val json = jsonObject.toString() if (response.isSuccessful) {
val mediaType = "application/json".toMediaType() Log.d(TAG, "统计事件发送成功")
val body = json.toRequestBody(mediaType) } else {
Log.w(TAG, "统计事件发送失败,HTTP状态码: ${response.code}")
val request = Request.Builder() }
.url("$serverurl/api/home/user_pushaction") }
.addHeader("Authorization", usertoken) // 添加 Token }
.post(body) })
.build()
Thread { } catch (e: Exception) {
val response = client.newCall(request).execute() // 捕获所有异常,防止崩溃
// println("sendStatisticsEvent Response: ${response.body?.string()}") Log.e(TAG, "发送统计事件时发生异常: ${e.message}", e)
}.start() }
}
} }
} }

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 // Only register QQ Music search function
client.registerLocalFunction( client.registerLocalFunction(
"qqmusic_search_songs", "qqmusic_search_songs",
"在QQ音乐中搜索内容,支持歌曲、专辑、歌手等搜索类型", "在QQ音乐中搜索歌曲,支持歌曲、专辑、歌手等搜索类型",
mapOf( mapOf(
"type" to "object", "type" to "object",
"properties" to mapOf( "properties" to mapOf(
@ -31,7 +31,7 @@ class QQMusicSystemFunctionHandler(private val context: Context? = null) {
), ),
"searchType" to mapOf( "searchType" to mapOf(
"type" to "integer", "type" to "integer",
"description" to "搜索类型:0-歌曲,1-专辑,2-歌手", "description" to "搜索类型:0-歌名搜索,1-歌手搜索,2-歌曲类型搜索",
"default" to 0 "default" to 0
) )
), ),
@ -63,16 +63,16 @@ class QQMusicSystemFunctionHandler(private val context: Context? = null) {
) )
// QQ音乐播放控制 // QQ音乐播放控制
// client.registerLocalFunction( client.registerLocalFunction(
// "qqmusic_play", "qqmusic_play",
// "播放QQ音乐", "播放QQ音乐",
// mapOf( mapOf(
// "type" to "object", "type" to "object",
// "properties" to emptyMap<String, Any>(), "properties" to emptyMap<String, Any>(),
// "required" to emptyList<String>() "required" to emptyList<String>()
// ), ),
// QQMusicPlayHandler(context) QQMusicPlayHandler(context)
// ) )
client.registerLocalFunction( client.registerLocalFunction(
"qqmusic_pause", "qqmusic_pause",
@ -162,7 +162,6 @@ class QQMusicSystemFunctionHandler(private val context: Context? = null) {
), ),
QQMusicGetPlayTimeHandler(context) QQMusicGetPlayTimeHandler(context)
) )
Log.d(TAG, "QQ音乐所有功能注册完成") Log.d(TAG, "QQ音乐所有功能注册完成")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "注册QQ音乐功能失败: ${e.message}", e) 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 { private class QQMusicLoginHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String { override suspend fun handle(arguments: Map<String, Any>): String {
return try { return try {
@ -207,10 +205,10 @@ private class QQMusicSearchHandler(private val context: Context?) : FunctionHand
var result = "" var result = ""
// 直接调用搜索,内部会自动处理登录检查 // 直接调用搜索,内部会自动处理登录检查
qqMusicSingleton.searchSongs(keyword, searchType) { searchResult -> qqMusicSingleton.searchSongs(keyword) { searchResult ->
result = if (searchResult.isSuccess) { result = if (searchResult.isSuccess) {
val songs = searchResult.getOrNull() ?: emptyList() val songs = searchResult.getOrNull() ?: emptyList()
// 根据搜索类型处理结果 // 根据搜索类型处理结果
val processedSongs = when (searchType) { val processedSongs = when (searchType) {
0 -> { // 歌曲搜索,只返回第一个结果 0 -> { // 歌曲搜索,只返回第一个结果
@ -220,7 +218,7 @@ private class QQMusicSearchHandler(private val context: Context?) : FunctionHand
songs songs
} }
} }
val songsJson = processedSongs.joinToString(",") { song -> val songsJson = processedSongs.joinToString(",") { song ->
val songMap = song as Map<String, Any?> val songMap = song as Map<String, Any?>
// 添加调试日志 // 添加调试日志
@ -234,15 +232,7 @@ private class QQMusicSearchHandler(private val context: Context?) : FunctionHand
val duration = songMap["duration"] ?: 0 val duration = songMap["duration"] ?: 0
"{\"mid\": \"$mid\", \"name\": \"$name\", \"artist\": \"$artist\", \"album\": \"$album\", \"duration\":$duration,\"pic\": \"$pic\"}" "{\"mid\": \"$mid\", \"name\": \"$name\", \"artist\": \"$artist\", \"album\": \"$album\", \"duration\":$duration,\"pic\": \"$pic\"}"
} }
"{\"success\": true, \"message\": \"搜索成功\", \"data\": [$songsJson]}"
val searchTypeText = when (searchType) {
0 -> "歌曲"
1 -> "专辑"
2 -> "歌手"
else -> "内容"
}
"{\"success\": true, \"message\": \"${searchTypeText}搜索成功\", \"searchType\": $searchType, \"data\": [$songsJson]}"
} else { } else {
val error = searchResult.exceptionOrNull() val error = searchResult.exceptionOrNull()
"{\"success\": false, \"message\": \"搜索失败: ${error?.message}\"}" "{\"success\": false, \"message\": \"搜索失败: ${error?.message}\"}"
@ -270,19 +260,19 @@ private class QQMusicPlaySongsHandler(private val context: Context?) : FunctionH
qqMusicSingleton.playSongs(songMids, index) { playResult -> qqMusicSingleton.playSongs(songMids, index) { playResult ->
result = if (playResult.isSuccess) { result = if (playResult.isSuccess) {
"{\"success\": true, \"message\": \"开始播放歌曲列表\"}" "{\"nocard\": true, \"success\": true, \"message\": \"开始播放歌曲列表\"}"
} else { } else {
val error = playResult.exceptionOrNull() val error = playResult.exceptionOrNull()
"{\"success\": false, \"message\": \"播放失败: ${error?.message}\"}" "{\"nocard\": true, \"success\": false, \"message\": \"播放失败: ${error?.message}\"}"
} }
} }
// 等待回调完成 // 等待回调完成
delay(3000) delay(3000)
result.ifEmpty { "{\"success\": false, \"message\": \"播放超时\"}" } result.ifEmpty { "{\"nocard\": true, \"success\": false, \"message\": \"播放超时\"}" }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("QQMusicPlaySongsHandler", "QQ音乐播放歌曲失败", e) 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_ID = "open_id"
private const val KEY_OPEN_TOKEN = "open_token" private const val KEY_OPEN_TOKEN = "open_token"
private const val KEY_EXPIRE_TIME = "expire_time" 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 @Volatile
private var INSTANCE: QQMusicSingleton? = null private var INSTANCE: QQMusicSingleton? = null
@ -39,6 +44,8 @@ class QQMusicSingleton private constructor() {
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
private var sharedPreferences: SharedPreferences? = null private var sharedPreferences: SharedPreferences? = null
private var currentLoginResult: LoginResult? = null private var currentLoginResult: LoginResult? = null
// 添加搜索超时跟踪
private val searchTimeouts = mutableMapOf<String, Runnable>()
/** /**
* 初始化单例,设置Activity上下文 * 初始化单例,设置Activity上下文
@ -126,12 +133,24 @@ class QQMusicSingleton private constructor() {
} }
/** /**
* 搜索歌曲(带自动登录检查) * 搜索歌曲(带自动登录检查和重试机制)
*/ */
fun searchSongs( fun searchSongs(
keyword: String, keyword: String,
searchType: Int = Data.SearchType.SEARCH_TYPE_SONG, searchType: Int = Data.SearchType.SEARCH_TYPE_SONG,
callback: (Result<List<Map<String, Any?>>>) -> Unit 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 val currentActivity = activity
if (currentActivity == null) { if (currentActivity == null) {
@ -144,101 +163,99 @@ class QQMusicSingleton private constructor() {
Log.d(TAG, "搜索前检测到未登录,开始自动登录") Log.d(TAG, "搜索前检测到未登录,开始自动登录")
login { loginResult -> login { loginResult ->
if (loginResult.isSuccess) { if (loginResult.isSuccess) {
Log.d(TAG, "自动登录成功,继续搜索") Log.d(TAG, "自动登录成功,等待QQ音乐服务连接...")
performSearch(keyword, searchType, callback) handler.postDelayed({
Log.d(TAG, "等待完成,开始搜索")
performSearchWithTimeout(keyword, searchType, retryCount, callback)
}, LOGIN_WAIT_DELAY_MS)
} else { } else {
callback(Result.failure(Exception("自动登录失败: ${loginResult.exceptionOrNull()?.message}"))) callback(Result.failure(Exception("自动登录失败: ${loginResult.exceptionOrNull()?.message}")))
} }
} }
} else { } else {
performSearch(keyword, searchType, callback) performSearchWithTimeout(keyword, searchType, retryCount, callback)
} }
} }
/** /**
* 执行搜索操作 * 执行带超时的搜索操作
*/ */
private fun performSearch( private fun performSearchWithTimeout(
keyword: String, keyword: String,
searchType: Int, searchType: Int,
retryCount: Int,
callback: (Result<List<Map<String, Any?>>>) -> Unit callback: (Result<List<Map<String, Any?>>>) -> Unit
) { ) {
val currentActivity = activity ?: return 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) ensureMusicManager(currentActivity)
musicManager?.searchSongs(keyword, searchType, object : QQMusicManager.SearchCallback { musicManager?.searchSongs(keyword, searchType, object : QQMusicManager.SearchCallback {
override fun onSuccess(songs: List<Map<String, Any?>>) { override fun onSuccess(songs: List<Map<String, Any?>>) {
handler.post { handler.post {
Log.d(TAG, "搜索成功: 找到 ${songs.size} 首歌曲") if (!completed) {
callback(Result.success(songs)) 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) { override fun onError(errorCode: Int, errorMessage: String) {
handler.post { handler.post {
Log.e(TAG, "搜索失败: code=$errorCode, message=$errorMessage") if (!completed) {
// 如果是登录相关错误,清除本地登录信息 completed = true
if (errorMessage.contains("登录") || errorMessage.contains("授权")) { searchTimeouts.remove(searchKey)?.let { handler.removeCallbacks(it) }
Log.d(TAG, "检测到登录相关错误,清除本地登录信息") Log.e(TAG, "搜索失败: code=$errorCode, message=$errorMessage")
logout()
// 如果是登录相关错误,清除本地登录信息
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() { fun cleanup() {
// 清理搜索超时任务
searchTimeouts.values.forEach { handler.removeCallbacks(it) }
searchTimeouts.clear()
// 清理管理器
loginManager?.cleanup() loginManager?.cleanup()
loginManager = null loginManager = null
musicManager?.cleanup() musicManager?.cleanup()
@ -386,6 +436,57 @@ class QQMusicSingleton private constructor() {
Log.d(TAG, "QQ音乐单例资源清理完成") 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) { private fun ensureMusicManager(activity: Activity) {
if (musicManager == null) { 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" -> { "getPlatformVersion" -> {
result.success("Android ${android.os.Build.VERSION.RELEASE}") 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