diff --git a/assets/images/music.png b/assets/images/music.png new file mode 100644 index 000000000..cc29d6979 Binary files /dev/null and b/assets/images/music.png differ diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index eec0aa32c..b9f48be6e 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -537,17 +537,6 @@ class BleManager extends GetxService { } var jsonString = _storage.read("logintoken"); - Logger.info('启动BLE服务,serverurl: ${serverurl}'); - // final azureSpeechRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? 'eastasia'; - // final openaiApiKey = dotenv.env['OPENAI_API_KEY'] ?? ''; - // final openaiBaseUrl = - // dotenv.env['OPENAI_BASE_URL'] ?? 'https://api.openai.com/v1'; - // final openaiModel = dotenv.env['OPENAI_MODEL'] ?? 'gpt-3.5-turbo'; - // 从Flutter资源包中加载.mcp.json - // final mcpConfig = await rootBundle.loadString('.mcp.json'); - - // 验证JSON格式 - // final jsonData = jsonDecode(mcpConfig); final azureSpeechKey = AppConfig.env('AZURE_SPEECH_KEY') ?? ''; final azureSpeechRegion = @@ -561,10 +550,10 @@ class BleManager extends GetxService { final openaiagentid = AppConfig.env('OPENAI_AGENT_ID') ?? 'personal_assistant'; final systemPrompt = AppConfig.agentsystemPrompt(openaiagentid); - + final musicServiceType = AppConfig.env('APP_MUSICSERVICE_TYPE') ?? 'AUTO'; // 从Flutter资源包中加载.mcp.json final jsonData = AppConfig.mcpConfig(); - + Logger.info('启动BLE服务,musicServiceType: ${musicServiceType}'); // 根据APP语言动态配置语音识别语言 List supportedLanguages = []; try { @@ -611,7 +600,7 @@ class BleManager extends GetxService { supportedLanguages.add('en-US'); } - Logger.info('动态配置语音识别语言: ${supportedLanguages.join(', ')}'); + // Logger.info('动态配置语音识别语言: ${supportedLanguages.join(', ')}'); } catch (e) { Logger.error('获取APP语言配置失败,使用默认语言: $e'); supportedLanguages = ['zh-CN', 'en-US']; @@ -632,6 +621,7 @@ class BleManager extends GetxService { 'serverurl': serverurl, 'usertoken': jsonString, 'supportedLanguages': supportedLanguages, + 'musicServiceType': musicServiceType, }; // 记录日志(不记录敏感信息) diff --git a/lib/modules/agent/views/message_bubble.dart b/lib/modules/agent/views/message_bubble.dart index 2cec99326..c26503de6 100644 --- a/lib/modules/agent/views/message_bubble.dart +++ b/lib/modules/agent/views/message_bubble.dart @@ -1,4 +1,5 @@ import 'package:voitrans/core/utils/logger.dart'; +import 'package:voitrans/modules/agent/views/music_card.dart'; import 'package:voitrans/modules/agent/views/qqmusic_card.dart'; import '../../../data/models/song_model.dart'; @@ -233,18 +234,18 @@ class MessageBubble extends GetView { // return buildMusicItem(context, musicId, musicName, musicUrl, // musicSinger, musicImage, ts); // } - // } else if (meta!.keys.contains('music_playlist')) { - // final card_musiclist = - // meta!["music_playlist"]['card_musiclist'] as Map?; - // if (card_musiclist != null) { - // final rawList = card_musiclist['musics'] as List? ?? []; - // final playlist = - // rawList.map((e) => Map.from(e)).toList(); - // //音乐播放列表 - // return buildMusicContent(context, playlist); - // } - // } else - if (meta!.keys.contains('qqmusic_searchandplay_songs')) { + if (meta!.keys.contains('music_searchandplay_songs')) { + final music_search_songs = + meta!['music_searchandplay_songs'] as Map; + final data = music_search_songs['data']; + if (data != null && data is List) { + final rawList = data as List? ?? []; + final playlist = + rawList.map((e) => Map.from(e)).toList(); + //音乐播放列表 + return buildMusicContent(context, playlist); + } + } else if (meta!.keys.contains('qqmusic_searchandplay_songs')) { final qqmusic_search_songs = meta!['qqmusic_searchandplay_songs'] as Map; final data = qqmusic_search_songs['data']; diff --git a/lib/modules/agent/views/music_card.dart b/lib/modules/agent/views/music_card.dart index d2543674f..0eabaf3ef 100644 --- a/lib/modules/agent/views/music_card.dart +++ b/lib/modules/agent/views/music_card.dart @@ -72,23 +72,23 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url, right: 2.w, bottom: 2.w, child: Container( - width: 12.w, - height: 12.w, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(2.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - blurRadius: 1, - offset: const Offset(0, 0.5), - ), - ], - ), + width: 16.w, + height: 16.w, + // decoration: BoxDecoration( + // color: Colors.white, + // borderRadius: BorderRadius.circular(2.r), + // boxShadow: [ + // BoxShadow( + // color: Colors.black.withOpacity(0.2), + // blurRadius: 1, + // offset: const Offset(0, 0.5), + // ), + // ], + // ), child: ClipRRect( borderRadius: BorderRadius.circular(2.r), child: Image.asset( - 'assets/images/qqmusic.png', + 'assets/images/music.png', fit: BoxFit.contain, ), ), @@ -203,11 +203,13 @@ Widget buildMusicContent( children: [ Row( children: [ - Icon( - Icons.music_note, - size: 16, - // ignore: deprecated_member_use - color: Theme.of(context).primaryColor.withOpacity(0.7), + Container( + width: 18.w, + height: 18.w, + child: Image.asset( + 'assets/images/music.png', + fit: BoxFit.contain, + ), ), const SizedBox(width: 6), Text( @@ -273,47 +275,6 @@ Widget buildMusicContent( ), ], ), - //), - //const SizedBox(height: 10), - // Row( - // mainAxisSize: MainAxisSize.min, - // children: [ - // Expanded( - // child: OutlinedButton.icon( - // onPressed: () {}, - // icon: Icon(Icons.play_arrow, size: 16), - // label: Text('播放全部'), - // style: OutlinedButton.styleFrom( - // foregroundColor: Theme.of(context).primaryColor, - // side: BorderSide( - // color: Theme.of(context).primaryColor.withOpacity(0.5)), - // padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4), - // shape: RoundedRectangleBorder( - // borderRadius: BorderRadius.circular(16), - // ), - // textStyle: TextStyle(fontSize: 12), - // ), - // ), - // ), - // SizedBox(width: 8), - // Expanded( - // child: OutlinedButton.icon( - // onPressed: () {}, - // icon: Icon(Icons.shuffle, size: 16), - // label: Text('随机播放'), - // style: OutlinedButton.styleFrom( - // foregroundColor: Colors.grey[700], - // side: BorderSide(color: Colors.grey[400]!), - // padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4), - // shape: RoundedRectangleBorder( - // borderRadius: BorderRadius.circular(16), - // ), - // textStyle: TextStyle(fontSize: 12), - // ), - // ), - // ), - // ], - // ), ], ); } diff --git a/lib/modules/music/controllers/music_playlist_controller.dart b/lib/modules/music/controllers/music_playlist_controller.dart index 367d2b560..5fe2c8d81 100644 --- a/lib/modules/music/controllers/music_playlist_controller.dart +++ b/lib/modules/music/controllers/music_playlist_controller.dart @@ -3,7 +3,7 @@ import 'package:get/get.dart'; class MusicPlaylistController extends GetxController { // 歌单数据 final playlist = >[].obs; - + @override void onInit() { super.onInit(); @@ -11,6 +11,4 @@ class MusicPlaylistController extends GetxController { final List> playlistData = Get.arguments ?? []; playlist.assignAll(playlistData); } - - } diff --git a/lib/modules/music/views/music_playlist_view.dart b/lib/modules/music/views/music_playlist_view.dart index 3803f328c..e0e720eb2 100644 --- a/lib/modules/music/views/music_playlist_view.dart +++ b/lib/modules/music/views/music_playlist_view.dart @@ -314,7 +314,7 @@ class MusicPlaylistView extends StatelessWidget { child: ClipRRect( borderRadius: BorderRadius.circular(2.r), child: Image.asset( - 'assets/images/qqmusic.png', + 'assets/images/music.png', fit: BoxFit.contain, ), ), @@ -463,7 +463,7 @@ class MusicPlaylistView extends StatelessWidget { child: ClipRRect( borderRadius: BorderRadius.circular(4.r), child: Image.asset( - 'assets/images/qqmusic.png', + 'assets/images/music.png', fit: BoxFit.contain, ), ), diff --git a/lib/modules/qqmusic/views/qqmusic_playlist_view.dart b/lib/modules/qqmusic/views/qqmusic_playlist_view.dart index 69b5f263a..3fc92b6a3 100644 --- a/lib/modules/qqmusic/views/qqmusic_playlist_view.dart +++ b/lib/modules/qqmusic/views/qqmusic_playlist_view.dart @@ -113,7 +113,7 @@ class QqmusicPlaylistView extends GetView { _buildProgressBar(isDarkMode), SizedBox(height: 15.h), // 播放控制按钮 - _buildPlayControls(isDarkMode), + // _buildPlayControls(isDarkMode), ], ), ); diff --git a/lib/modules/qqmusic_test/bindings/qqmusic_binding.dart b/lib/modules/qqmusic_test/bindings/qqmusic_binding.dart deleted file mode 100644 index 1050ea386..000000000 --- a/lib/modules/qqmusic_test/bindings/qqmusic_binding.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:get/get.dart'; -import '../controllers/qqmusic_controller.dart'; - -class QQMusicBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut(() => QQMusicController()); - } -} \ No newline at end of file diff --git a/lib/modules/qqmusic_test/controllers/qqmusic_controller.dart b/lib/modules/qqmusic_test/controllers/qqmusic_controller.dart deleted file mode 100644 index 92e02560b..000000000 --- a/lib/modules/qqmusic_test/controllers/qqmusic_controller.dart +++ /dev/null @@ -1,326 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:qq_music/qq_music.dart'; -import 'package:voitrans/data/services/qq_music.dart'; -import '../models/song_model.dart'; - -class QQMusicController extends GetxController { - final QqMusicService _qqMusic = Get.find(); - - // 登录状态 - final isLoggedIn = false.obs; - final userInfo = Rxn(); - - // 搜索相关 - final searchResults = [].obs; - final isSearching = false.obs; - final searchKeyword = ''.obs; - - // 播放相关 - final currentSong = Rxn(); - final isPlaying = false.obs; - final currentPosition = 0.obs; - final totalDuration = 0.obs; - final playlist = [].obs; - final currentIndex = 0.obs; - - @override - void onInit() { - super.onInit(); - _checkLoginStatus(); - _startPlaybackStateMonitor(); - } - - /// 检查登录状态 - void _checkLoginStatus() { - // 这里可以检查本地存储的登录信息 - // 暂时设为未登录状态 - isLoggedIn.value = false; - } - - /// QQ音乐登录 - Future login() async { - try { - print('开始QQ音乐登录...'); - - Get.dialog( - const Center( - child: CircularProgressIndicator(), - ), - barrierDismissible: false, - ); - - // 添加超时处理 - final result = await _qqMusic.login().timeout( - const Duration(seconds: 30), - onTimeout: () { - print('QQ音乐登录超时'); - return {'success': false, 'error': '登录超时,请重试'}; - }, - ); - - print('QQ音乐登录结果: $result'); - - Get.back(); // 关闭加载对话框 - - if (result['success'] == true) { - userInfo.value = QQMusicLoginResult.fromMap(result); - isLoggedIn.value = true; - print('QQ音乐登录成功: ${result['openId']}'); - Get.snackbar('登录成功', '欢迎使用QQ音乐'); - } else { - print('QQ音乐登录失败: ${result['error']}'); - Get.snackbar('登录失败', result['error'] ?? '未知错误'); - } - } catch (e) { - print('QQ音乐登录异常: $e'); - Get.back(); - Get.snackbar('登录失败', e.toString()); - } - } - - /// 退出登录 - void logout() { - userInfo.value = null; - isLoggedIn.value = false; - currentSong.value = null; - isPlaying.value = false; - playlist.clear(); - Get.snackbar('已退出', '已退出QQ音乐登录'); - } - - /// 搜索歌曲 - Future searchSongs(String keyword) async { - if (keyword.trim().isEmpty) return; - - print('开始搜索歌曲: $keyword'); - searchKeyword.value = keyword; - isSearching.value = true; - - try { - // 检查登录状态 - if (!isLoggedIn.value) { - print('用户未登录,无法搜索'); - Get.snackbar('搜索失败', '请先登录QQ音乐'); - return; - } - - print('调用搜索接口...'); - final result = await _qqMusic.searchSongs(keyword: keyword).timeout( - const Duration(seconds: 15), - onTimeout: () { - print('搜索超时'); - return {'success': false, 'error': '搜索超时,请重试'}; - }, - ); - - print('搜索结果: $result'); - - if (result['success'] == true && result['songs'] != null) { - final List songsData = result['songs']; - print('找到 ${songsData.length} 首歌曲'); - - final songs = songsData.map((songData) { - return SongModel( - mid: songData['mid'] ?? '', - name: songData['name'] ?? '', - artist: songData['singer'] ?? '', - album: songData['album'] ?? '', - albumPic: songData['pic'], - duration: songData['duration'] ?? 0, - ); - }).toList(); - - searchResults.assignAll(songs); - print('搜索成功,更新UI'); - } else { - print('搜索失败: ${result['error']}'); - Get.snackbar('搜索失败', result['error'] ?? '搜索结果为空'); - searchResults.clear(); - } - } catch (e) { - print('搜索异常: $e'); - Get.snackbar('搜索失败', e.toString()); - searchResults.clear(); - } finally { - print('搜索完成,重置状态'); - isSearching.value = false; - } - } - - /// 播放歌曲 - Future playSong(SongModel song, {List? songList}) async { - try { - currentSong.value = song; - - if (songList != null) { - playlist.assignAll(songList); - currentIndex.value = songList.indexOf(song); - } else { - playlist.assignAll([song]); - currentIndex.value = 0; - } - - // 获取歌曲MID列表 - final songMids = playlist.map((s) => s.mid).toList(); - final playIndex = currentIndex.value; - - // 调用QQ音乐播放接口 - final result = await _qqMusic.playSongs( - songMids: songMids, - index: playIndex, - ); - - if (result['success'] == true) { - isPlaying.value = true; - totalDuration.value = song.duration; - currentPosition.value = 0; - Get.snackbar('开始播放', song.name); - } else { - Get.snackbar('播放失败', result['error'] ?? '未知错误'); - } - } catch (e) { - Get.snackbar('播放失败', e.toString()); - } - } - - /// 暂停/继续播放 - Future togglePlayPause() async { - try { - if (isPlaying.value) { - final result = await _qqMusic.pauseMusic(); - if (result['success'] == true) { - isPlaying.value = false; - } - } else { - final result = await _qqMusic.resumeMusic(); - if (result['success'] == true) { - isPlaying.value = true; - } - } - } catch (e) { - Get.snackbar('操作失败', e.toString()); - } - } - - /// 上一首 - Future previousSong() async { - try { - final result = await _qqMusic.skipToPrevious(); - if (result['success'] == true) { - if (playlist.isNotEmpty) { - int newIndex = currentIndex.value - 1; - if (newIndex < 0) newIndex = playlist.length - 1; - currentIndex.value = newIndex; - currentSong.value = playlist[newIndex]; - } - await _updateCurrentSong(); - } - } catch (e) { - Get.snackbar('切换失败', e.toString()); - } - } - - /// 下一首 - Future nextSong() async { - try { - final result = await _qqMusic.skipToNext(); - if (result['success'] == true) { - if (playlist.isNotEmpty) { - int newIndex = currentIndex.value + 1; - if (newIndex >= playlist.length) newIndex = 0; - currentIndex.value = newIndex; - currentSong.value = playlist[newIndex]; - } - await _updateCurrentSong(); - } - } catch (e) { - Get.snackbar('切换失败', e.toString()); - } - } - - /// 停止播放 - Future stopMusic() async { - try { - final result = await _qqMusic.stopMusic(); - if (result['success'] == true) { - isPlaying.value = false; - currentPosition.value = 0; - } - } catch (e) { - Get.snackbar('停止失败', e.toString()); - } - } - - /// 更新当前播放歌曲信息 - Future _updateCurrentSong() async { - try { - final result = await _qqMusic.getCurrentSong(); - if (result['success'] == true && result['song'] != null) { - final songData = result['song']; - final song = SongModel( - mid: songData['mid'] ?? '', - name: songData['name'] ?? '', - artist: songData['singer'] ?? '', - album: songData['album'] ?? '', - albumPic: songData['pic'], - duration: songData['duration'] ?? 0, - ); - currentSong.value = song; - } - } catch (e) { - print('更新当前歌曲信息失败: $e'); - } - } - - /// 启动播放状态监控 - void _startPlaybackStateMonitor() { - // 每秒检查一次播放状态和时间 - Stream.periodic(const Duration(seconds: 1)).listen((_) async { - if (isLoggedIn.value) { - await _updatePlaybackState(); - await _updatePlayTime(); - } - }); - } - - /// 更新播放状态 - Future _updatePlaybackState() async { - try { - final result = await _qqMusic.getPlaybackState(); - if (result['success'] == true && result['state'] != null) { - final state = result['state']; - // QQ音乐播放状态:0-停止,1-播放,2-暂停 - isPlaying.value = state == 1; - } - } catch (e) { - // 静默处理错误,避免频繁弹窗 - } - } - - /// 更新播放时间 - Future _updatePlayTime() async { - try { - final result = await _qqMusic.getPlayTime(); - if (result['success'] == true) { - currentPosition.value = result['currentTime'] ?? 0; - totalDuration.value = result['totalTime'] ?? 0; - } - } catch (e) { - // 静默处理错误,避免频繁弹窗 - } - } - - /// 跳转到指定位置(暂时保留,QQ音乐可能不支持) - void seekTo(int position) { - currentPosition.value = position; - // QQ音乐SDK可能不支持跳转功能 - Get.snackbar('提示', '当前版本不支持跳转功能'); - } - - @override - void onClose() { - // 清理资源 - super.onClose(); - } -} diff --git a/lib/modules/qqmusic_test/models/song_model.dart b/lib/modules/qqmusic_test/models/song_model.dart deleted file mode 100644 index e93470927..000000000 --- a/lib/modules/qqmusic_test/models/song_model.dart +++ /dev/null @@ -1,49 +0,0 @@ -class SongModel { - final String mid; - final String name; - final String artist; - final String album; - final String? albumPic; - final int duration; - final bool isFavorite; - - SongModel({ - required this.mid, - required this.name, - required this.artist, - required this.album, - this.albumPic, - required this.duration, - this.isFavorite = false, - }); - - factory SongModel.fromJson(Map json) { - return SongModel( - mid: json['mid'] ?? '', - name: json['name'] ?? '', - artist: json['artist'] ?? '', - album: json['album'] ?? '', - albumPic: json['albumPic'], - duration: json['duration'] ?? 0, - isFavorite: json['isFavorite'] ?? false, - ); - } - - Map toJson() { - return { - 'mid': mid, - 'name': name, - 'artist': artist, - 'album': album, - 'albumPic': albumPic, - 'duration': duration, - 'isFavorite': isFavorite, - }; - } - - String get durationText { - final minutes = duration ~/ 60; - final seconds = duration % 60; - return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; - } -} \ No newline at end of file diff --git a/lib/modules/qqmusic_test/views/qqmusic_view.dart b/lib/modules/qqmusic_test/views/qqmusic_view.dart deleted file mode 100644 index 5cf842e76..000000000 --- a/lib/modules/qqmusic_test/views/qqmusic_view.dart +++ /dev/null @@ -1,242 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import '../controllers/qqmusic_controller.dart'; -import '../models/song_model.dart'; - -class QQMusicView extends GetView { - const QQMusicView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('QQ音乐'), - actions: [ - Obx(() => controller.isLoggedIn.value - ? IconButton( - icon: const Icon(Icons.logout), - onPressed: controller.logout, - ) - : IconButton( - icon: const Icon(Icons.login), - onPressed: controller.login, - )), - ], - ), - body: Obx(() { - if (!controller.isLoggedIn.value) { - return _buildLoginView(); - } - return _buildMainView(); - }), - bottomNavigationBar: Obx(() { - if (controller.currentSong.value != null) { - return _buildMiniPlayer(); - } - return const SizedBox.shrink(); - }), - ); - } - - Widget _buildLoginView() { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.music_note, - size: 100, - color: Colors.blue, - ), - const SizedBox(height: 20), - const Text( - '请先登录QQ音乐', - style: TextStyle(fontSize: 18), - ), - const SizedBox(height: 20), - ElevatedButton( - onPressed: controller.login, - child: const Text('登录QQ音乐'), - ), - ], - ), - ); - } - - Widget _buildMainView() { - return Column( - children: [ - _buildSearchBar(), - Expanded( - child: Obx(() { - if (controller.isSearching.value) { - return const Center(child: CircularProgressIndicator()); - } - if (controller.searchResults.isEmpty) { - return const Center( - child: Text('搜索歌曲开始您的音乐之旅'), - ); - } - return _buildSearchResults(); - }), - ), - ], - ); - } - - Widget _buildSearchBar() { - return Container( - padding: const EdgeInsets.all(16), - child: TextField( - decoration: const InputDecoration( - hintText: '搜索歌曲、歌手、专辑', - prefixIcon: Icon(Icons.search), - border: OutlineInputBorder(), - ), - onSubmitted: controller.searchSongs, - ), - ); - } - - Widget _buildSearchResults() { - return ListView.builder( - itemCount: controller.searchResults.length, - itemBuilder: (context, index) { - final song = controller.searchResults[index]; - return _buildSongItem(song); - }, - ); - } - - Widget _buildSongItem(SongModel song) { - return ListTile( - leading: Container( - width: 50, - height: 50, - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(8), - ), - child: song.albumPic != null - ? ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Image.network( - song.albumPic!, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return const Icon(Icons.music_note); - }, - ), - ) - : const Icon(Icons.music_note), - ), - title: Text( - song.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - subtitle: Text( - '${song.artist} • ${song.album}', - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(song.durationText), - const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.play_arrow), - onPressed: () => controller.playSong( - song, - songList: controller.searchResults, - ), - ), - ], - ), - ); - } - - Widget _buildMiniPlayer() { - return Obx(() { - final song = controller.currentSong.value!; - return Container( - height: 80, - decoration: BoxDecoration( - color: Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 4, - offset: const Offset(0, -2), - ), - ], - ), - child: Column( - children: [ - // 进度条 - Obx(() => LinearProgressIndicator( - value: controller.totalDuration.value > 0 - ? controller.currentPosition.value / - controller.totalDuration.value - : 0, - backgroundColor: Colors.grey[300], - valueColor: const AlwaysStoppedAnimation(Colors.blue), - )), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - // 歌曲信息 - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - song.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - Text( - song.artist, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle(color: Colors.grey[600]), - ), - ], - ), - ), - // 控制按钮 - Row( - children: [ - IconButton( - icon: const Icon(Icons.skip_previous), - onPressed: controller.previousSong, - ), - Obx(() => IconButton( - icon: Icon( - controller.isPlaying.value - ? Icons.pause - : Icons.play_arrow, - ), - onPressed: controller.togglePlayPause, - )), - IconButton( - icon: const Icon(Icons.skip_next), - onPressed: controller.nextSong, - ), - ], - ), - ], - ), - ), - ), - ], - ), - ); - }); - } -} \ No newline at end of file diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index 34f1fcc40..864c9b320 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -2,8 +2,6 @@ import 'package:get/get.dart'; import 'package:qq_music/qq_music.dart'; import 'package:voitrans/modules/qqmusic/bindings/qqmusic_playlist_binding.dart'; import 'package:voitrans/modules/qqmusic/views/qqmusic_playlist_view.dart'; -import 'package:voitrans/modules/qqmusic_test/bindings/qqmusic_binding.dart'; -import 'package:voitrans/modules/qqmusic_test/views/qqmusic_view.dart'; import '/modules/ble/bindings/ble_binding.dart'; import '../modules/ble/views/ble_view.dart'; diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 7de405f95..e74de5958 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -11,7 +11,7 @@ import com.yunqiinnovation.azure_speech.AzureTtsHelper import com.yunqiinnovation.chat_api.ChatApiService import com.yunqiinnovation.chat_api.StreamCallback import com.yunqiinnovation.chat_storage.ChatStorageHelper -import com.yunqiinnovation.music_service.MusicServiceStarter +import com.yunqiinnovation.music_service.MusicSingleton import com.yunqiinnovation.amap_service.NavigationServiceHelper import com.yunqiinnovation.user_portrait.UserPortrait import com.yunqiinnovation.location_service.LocationService @@ -24,13 +24,10 @@ import com.yunqiinnovation.ble_service.BleService import android.media.MediaPlayer import java.util.concurrent.atomic.AtomicBoolean import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import com.deep_voice.speech.tts.TtsEvent import com.deep_voice.speech.tts.TtsEventListener import com.deep_voice.speech.tts.TtsEventType -import com.deep_voice.bytedance_speech.BytedanceTTS import java.util.UUID -import com.yunqiinnovation.agent_service.Utils import okhttp3.* import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.toRequestBody @@ -187,6 +184,8 @@ object AgentService : CoroutineScope { if (isInitialized) return true try { + serverurl = config["serverurl"]?.toString() ?: ""; + usertoken = config["usertoken"]?.toString() ?: ""; // 初始化上下文和服务 this.context = appContext.applicationContext this.chatApiService = ChatApiService(context) @@ -196,6 +195,7 @@ object AgentService : CoroutineScope { UserPortrait.initialize(appContext); //初始化定位服务 LocationService.initialize(appContext) + MusicSingleton.getInstance().initialize(appContext,serverurl,usertoken) QQMusicSingleton.getInstance().initialize(appContext) // 设置系统提示词 val customSystemPrompt = config["systemPrompt"] as? String @@ -243,10 +243,10 @@ object AgentService : CoroutineScope { config["openaiBaseUrl"]?.toString() ?: "", config["openaiModel"]?.toString() ?: "", config["openaivisionModel"]?.toString() ?: "", - config["mcpServer"]?.toString() ?: "" + config["mcpServer"]?.toString() ?: "", + config["musicServiceType"]?.toString() ?: "" ) - serverurl = config["serverurl"]?.toString() ?: ""; - usertoken = config["usertoken"]?.toString() ?: ""; + Log.e(TAG, "liwei -----------读取服务地址和用户token: $serverurl $usertoken") // 加载最近的聊天记录 @@ -1892,9 +1892,8 @@ object AgentService : CoroutineScope { "ts" to ts ) ) - } else if (meta.has("card_musiclist")) { //音乐列表 - val cardMusiclist = meta.getJSONObject("card_musiclist") - val musics = cardMusiclist.optJSONArray("musics") + } else if (toolname == "music_searchandplay_songs" && meta.has("data")) { //qq音乐 + val musics = meta.optJSONArray("data") if (musics != null) { val playlist = mutableListOf>() for (i in 0 until musics.length()) { @@ -1985,7 +1984,7 @@ object AgentService : CoroutineScope { fun processMusicPlay(song: Map) { // 在其他 Service、BroadcastReceiver 或 Application 中调用 Log.i(TAG, "播放音乐: ${song}") - MusicServiceStarter.startServiceWithCommand(context, command = "play", song = song) + MusicSingleton.startServiceWithCommand(context, command = "play", song = song) } /** @@ -2000,7 +1999,7 @@ object AgentService : CoroutineScope { fun processMusicPlayList(songs: List>) { // 如果 playlist 中有有效的歌曲,开始播放 if (songs.isNotEmpty()) { - MusicServiceStarter.startServiceWithPlaylist( + MusicSingleton.startServiceWithPlaylist( context, command = "playlist", songs = songs diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt index 2c22546f3..54122ce11 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt @@ -73,6 +73,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream config["volcanoToken"]?.toString()?.let { intent.putExtra("volcanoToken", it) } config["serverurl"]?.toString()?.let { intent.putExtra("serverurl", it) } config["usertoken"]?.toString()?.let { intent.putExtra("usertoken", it) } + config["musicServiceType"]?.toString()?.let { intent.putExtra("musicServiceType", it) } // 添加 supportedLanguages 参数处理 config["supportedLanguages"]?.let { languages -> diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt index 9eb03b1f3..24cdd92d6 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt @@ -52,6 +52,8 @@ class BleMediaService : Service(), CoroutineScope { private const val KEY_SERVER_URL = "serverurl" private const val KEY_USER_TOKEN = "usertoken" private const val KEY_SUPPORTED_LANGUAGES = "supportedLanguages" + private const val KEY_MUSICSERVICE_TYPE = "musicServiceType" + // 用于跟踪服务是否正在运行 private val isRunning = AtomicBoolean(false) @@ -179,6 +181,7 @@ class BleMediaService : Service(), CoroutineScope { it.getStringExtra(KEY_VOLCANO_TOKEN)?.let { value -> config[KEY_VOLCANO_TOKEN] = value } it.getStringExtra(KEY_SERVER_URL)?.let { value -> config[KEY_SERVER_URL] = value } it.getStringExtra(KEY_USER_TOKEN)?.let { value -> config[KEY_USER_TOKEN] = value } + it.getStringExtra(KEY_MUSICSERVICE_TYPE)?.let { value -> config[KEY_MUSICSERVICE_TYPE] = value } // 提取 supportedLanguages 参数 it.getStringArrayListExtra(KEY_SUPPORTED_LANGUAGES)?.let { languages -> @@ -238,6 +241,7 @@ class BleMediaService : Service(), CoroutineScope { config[KEY_VOLCANO_TOKEN] = getConfigString(prefs, KEY_VOLCANO_TOKEN, "") config[KEY_SERVER_URL] = getConfigString(prefs, KEY_SERVER_URL, "") config[KEY_USER_TOKEN] = getConfigString(prefs, KEY_USER_TOKEN, "") + config[KEY_MUSICSERVICE_TYPE] = getConfigString(prefs, KEY_MUSICSERVICE_TYPE, "") // 添加 supportedLanguages 参数处理 val supportedLanguagesStr = getConfigString(prefs, KEY_SUPPORTED_LANGUAGES, "") diff --git a/local_plugins/chat_api/android/build.gradle.kts b/local_plugins/chat_api/android/build.gradle.kts index 1d3abb7cf..0f564abce 100644 --- a/local_plugins/chat_api/android/build.gradle.kts +++ b/local_plugins/chat_api/android/build.gradle.kts @@ -60,5 +60,6 @@ dependencies { add("compileOnly", project(":location_service")) - add("compileOnly", project(":qq_music")) + add("compileOnly", project(":music_service")) + add("compileOnly", project(":qq_music")) } \ No newline at end of file diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt index 4823653ef..6e3300759 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt @@ -198,7 +198,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor /** * 初始化ChatAPI服务 */ - fun initialize(apiKey: String, baseUrl: String, model: String, visionModel: String, mcpServer: String): Boolean { + fun initialize(apiKey: String, baseUrl: String, model: String, visionModel: String, mcpServer: String,musicType: String = "AUTO"): Boolean { this.apiKey = apiKey if (baseUrl.isNotEmpty()) { this.baseUrl = baseUrl @@ -236,7 +236,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor mcpConfigJson = mcpServer // 异步初始化MCP客户端 launch { - initializeMcpClient(mcpServer) + initializeMcpClient(mcpServer,musicType) // initializeMcpClient("{}") } @@ -784,9 +784,9 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor /** * 初始化MCP客户端 */ - fun initializeMcpClient(serverUrl: String): Boolean { + fun initializeMcpClient(serverUrl: String,musicType: String = "AUTO"): Boolean { if (_mcpClient == null) { - _mcpClient = MCPClient(context) + _mcpClient = MCPClient(context,musicType) } // 直接使用类的CoroutineScope启动协程 diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt index f4b452bde..17d4c3f87 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt @@ -31,7 +31,7 @@ interface FunctionHandler { * MCP客户端 * 与 iOS 版本 MCPClient 功能对等 */ -class MCPClient(private val context: Context? = null) : AutoCloseable { +class MCPClient(private val context: Context? = null,musicType: String = "AUTO") : AutoCloseable { companion object { private const val TAG = "MCPClient" @@ -54,7 +54,17 @@ class MCPClient(private val context: Context? = null) : AutoCloseable { init { initializeSystemFunctions() - initializeQQMusiceFunctions() + when (musicType) { + "WY" -> initializeMusiceFunctions() //网易 + "QQ" -> initializeQQMusiceFunctions() //QQ + else -> { + if (QQMusicSystemFunctionHandler.CheckQQMusicInstall()) { //检查是否安装qq音乐 + initializeQQMusiceFunctions(); + }else{ + initializeMusiceFunctions() + } + } + } } /** @@ -69,6 +79,17 @@ class MCPClient(private val context: Context? = null) : AutoCloseable { } } + /** + * 初始化音乐函数 + */ + private fun initializeMusiceFunctions() { + try { + val handler = MusicSystemFunctionHandler(context) + handler.registerAllFunctions(this) + } catch (e: Exception) { + Log.w(TAG, "Failed to initialize system functions", e) + } + } /** * 初始化音乐函数 */ diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MusicSystemFunctionHandler.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MusicSystemFunctionHandler.kt new file mode 100644 index 000000000..918ebc967 --- /dev/null +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MusicSystemFunctionHandler.kt @@ -0,0 +1,307 @@ +package com.yunqiinnovation.chat_api + +import android.content.Context +import android.content.Intent +import android.util.Log +import com.yunqiinnovation.music_service.MusicSingleton +import org.json.JSONObject +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +/** + * QQ音乐系统功能处理器 + * 负责注册QQ音乐相关的MCP函数 + */ +class MusicSystemFunctionHandler(private val context: Context? = null) { + companion object { + const val TAG = "MusicSystem" + } + + /** + * 注册所有QQ音乐相关函数到MCPClient + */ + fun registerAllFunctions(client: MCPClient) { + try { + // Only register QQ Music search function + client.registerLocalFunction( + "music_searchandplay_songs", + "在音乐播放服务中搜索并播放歌曲,支持歌曲、专辑、歌手等搜索类型", + mapOf( + "type" to "object", + "properties" to mapOf( + "keyword" to mapOf( + "type" to "string", + "description" to "搜索关键词,歌曲名、专辑名或歌手名" + ), + "searchType" to mapOf( + "type" to "integer", + "description" to "搜索类型:0-歌名搜索,1-歌手搜索,2-歌曲类型搜索", + "default" to 0 + ) + ), + "required" to listOf("keyword") + ), + MusicSearchHandler(context) + ) + + + // 注册媒体播放功能 + client.registerLocalFunction( + name = "media_play", + description = "播放媒体", + parameters = mapOf( + "type" to "object", + "properties" to emptyMap(), + "required" to emptyList() + ), + handler = MediaPlayHandler(context) + ) + + // 注册媒体暂停功能 + client.registerLocalFunction( + name = "media_pause", + description = "暂停媒体播放", + parameters = mapOf( + "type" to "object", + "properties" to emptyMap(), + "required" to emptyList() + ), + handler = MediaPauseHandler(context) + ) + // 注册媒体暂停功能 + client.registerLocalFunction( + name = "media_close", + description = "关闭媒体播放", + parameters = mapOf( + "type" to "object", + "properties" to emptyMap(), + "required" to emptyList() + ), + handler = MediaCloseHandler(context) + ) + // 注册媒体上一首功能 + client.registerLocalFunction( + name = "media_previous", + description = "播放上一首", + parameters = mapOf( + "type" to "object", + "properties" to emptyMap(), + "required" to emptyList() + ), + handler = MediaPreviousHandler(context) + ) + + // 注册媒体下一首功能 + client.registerLocalFunction( + name = "media_next", + description = "播放下一首", + parameters = mapOf( + "type" to "object", + "properties" to emptyMap(), + "required" to emptyList() + ), + handler = MediaNextHandler(context) + ) + Log.d(TAG, "音乐所有功能注册完成") + } catch (e: Exception) { + Log.e(TAG, "注册音乐功能失败: ${e.message}", e) + } + } +} + +private class MusicSearchHandler(private val context: Context?) : FunctionHandler { + override suspend fun handle(arguments: Map): String { + return try { + val keyword = arguments["keyword"] as? String + ?: return "{\"success\": false, \"message\": \"缺少搜索关键词\"}" + val searchType = arguments["searchType"] as? Int ?: 0 // 默认搜索歌曲 + val musicSingleton = MusicSingleton.getInstance() + + // 使用 suspendCoroutine 将回调转换为协程 + suspendCoroutine { continuation -> + musicSingleton.searchSongs(keyword) { searchResult -> + val result = if (searchResult.isSuccess) { + val songs = searchResult.getOrNull() ?: emptyList() + + // 根据搜索类型处理结果 + val processedSongs = when (searchType) { + 0 -> { // 歌曲搜索,返回所有结果 + songs + } + + else -> { // 专辑、歌手等其他类型,返回所有结果 + songs + } + } + + val songsJson = processedSongs.joinToString(",") { song -> + val songMap = song as Map + // 添加调试日志 + Log.d("QQMusicSearchHandler", "原始数据: $songMap") + // 修正字段名映射 + val id = songMap["id"] ?: 0 + val name = songMap["name"] ?: "unknown" + val singer = songMap["singer"] ?: "unknown" + val url = songMap["url"] ?: "" + val image = songMap["image"] ?: "" + val ts = songMap["ts"] ?: 0 + "{\"id\": $id, \"name\": \"$name\", \"singer\": \"$singer\", \"url\": \"$url\", \"ts\":$ts,\"image\": \"$image\"}" + } + "{\"success\": true, \"broadcast\": false, \"message\": \"搜索成功,正在播放\", \"data\": [$songsJson]}" + } else { + val error = searchResult.exceptionOrNull() + "{\"success\": false, \"message\": \"搜索失败: ${error?.message}\"}" + } + // 恢复协程执行 + continuation.resume(result) + } + } + } catch (e: Exception) { + Log.e("QQMusicSearchHandler", "QQ音乐搜索失败", e) + "{\"success\": false, \"message\": \"搜索异常: ${e.message}\"}" + } + } +} + + + /** + * 媒体播放处理器 + */ + private class MediaPlayHandler(private val context: Context?) : FunctionHandler { + override suspend fun handle(arguments: Map): String { + if (context == null) { + return JSONObject().apply { + put("error", JSONObject().apply { + put("code", "context_error") + put("message", "上下文未初始化") + }) + }.toString() + } + + // 发送媒体播放广播 + val intent = Intent("com.yunqiinnovation.music_service.MEDIA_PLAY").apply { + setPackage(context.packageName) + } + context.sendBroadcast(intent) + + return JSONObject().apply { + put("result", "已开始播放媒体") + put("notreply", true) + put("broadcast", false) + }.toString() + } + } + + /** + * 媒体暂停处理器 + */ + private class MediaPauseHandler(private val context: Context?) : FunctionHandler { + override suspend fun handle(arguments: Map): String { + if (context == null) { + return JSONObject().apply { + put("error", JSONObject().apply { + put("code", "context_error") + put("message", "上下文未初始化") + }) + }.toString() + } + + Log.d("MediaPauseHandler", "📤 正在发送广播 context = $context") + // 发送媒体暂停广播 + val intent = Intent("com.yunqiinnovation.music_service.MEDIA_PAUSE").apply { + setPackage(context.packageName) + } + context.sendBroadcast(intent) + + return JSONObject().apply { + put("result", "已暂停媒体播放") + put("notreply", true) + put("broadcast", false) + }.toString() + } + } + + /** + * 媒体关闭处理器 + */ + private class MediaCloseHandler(private val context: Context?) : FunctionHandler { + override suspend fun handle(arguments: Map): String { + if (context == null) { + return JSONObject().apply { + put("error", JSONObject().apply { + put("code", "context_error") + put("message", "上下文未初始化") + }) + }.toString() + } + + Log.d("MediaCloseHandler", "📤 正在发送广播 context = $context") + // 发送媒体关闭广播 + val intent = Intent("com.yunqiinnovation.music_service.MEDIA_CLOSE").apply { + setPackage(context.packageName) + } + context.sendBroadcast(intent) + + return JSONObject().apply { + put("result", "已关闭媒体播放") + put("notreply", true) + put("broadcast", false) + }.toString() + } + } + + /** + * 媒体上一首处理器 + */ + private class MediaPreviousHandler(private val context: Context?) : FunctionHandler { + override suspend fun handle(arguments: Map): String { + if (context == null) { + return JSONObject().apply { + put("error", JSONObject().apply { + put("code", "context_error") + put("message", "上下文未初始化") + }) + }.toString() + } + + // 发送切换上一首广播 + val intent = Intent("com.yunqiinnovation.music_service.MEDIA_PREVIOUS").apply { + setPackage(context.packageName) + } + context.sendBroadcast(intent) + + return JSONObject().apply { + put("result", "已切换到上一首") + put("notreply", true) + put("broadcast", false) + }.toString() + } + } + + /** + * 媒体下一首处理器 + */ + private class MediaNextHandler(private val context: Context?) : FunctionHandler { + override suspend fun handle(arguments: Map): String { + if (context == null) { + return JSONObject().apply { + put("error", JSONObject().apply { + put("code", "context_error") + put("message", "上下文未初始化") + }) + }.toString() + } + + // 发送切换下一首广播 + val intent = Intent("com.yunqiinnovation.music_service.MEDIA_NEXT").apply { + setPackage(context.packageName) + } + context.sendBroadcast(intent) + + return JSONObject().apply { + put("result", "已切换到下一首") + put("notreply", true) + put("broadcast", false) + }.toString() + } + } diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt index 08a0578c5..1b9e9fe80 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt @@ -13,8 +13,15 @@ import kotlin.coroutines.resume class QQMusicSystemFunctionHandler(private val context: Context? = null) { companion object { const val TAG = "QQMusicSystemFunctionHandler" + + fun CheckQQMusicInstall(): Boolean { + val qqMusicSingleton = QQMusicSingleton.getInstance() + return qqMusicSingleton.isQQMusicInstalled() + } } - + + + /** * 注册所有QQ音乐相关函数到MCPClient */ diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt index 00c95ad79..adacdafd9 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt @@ -151,65 +151,7 @@ class SystemFunctionHandler(private val context: Context? = null) { ), handler = GetCurrentLocationHandler(context) ) - - // // 注册媒体播放功能 - // client.registerLocalFunction( - // name = "media_play", - // description = "播放媒体", - // parameters = mapOf( - // "type" to "object", - // "properties" to emptyMap(), - // "required" to emptyList() - // ), - // handler = MediaPlayHandler(context) - // ) - - // // 注册媒体暂停功能 - // client.registerLocalFunction( - // name = "media_pause", - // description = "暂停媒体播放", - // parameters = mapOf( - // "type" to "object", - // "properties" to emptyMap(), - // "required" to emptyList() - // ), - // handler = MediaPauseHandler(context) - // ) - // // 注册媒体暂停功能 - // client.registerLocalFunction( - // name = "media_close", - // description = "关闭媒体播放", - // parameters = mapOf( - // "type" to "object", - // "properties" to emptyMap(), - // "required" to emptyList() - // ), - // handler = MediaCloseHandler(context) - // ) - // // 注册媒体上一首功能 - // client.registerLocalFunction( - // name = "media_previous", - // description = "播放上一首", - // parameters = mapOf( - // "type" to "object", - // "properties" to emptyMap(), - // "required" to emptyList() - // ), - // handler = MediaPreviousHandler(context) - // ) - - // // 注册媒体下一首功能 - // client.registerLocalFunction( - // name = "media_next", - // description = "播放下一首", - // parameters = mapOf( - // "type" to "object", - // "properties" to emptyMap(), - // "required" to emptyList() - // ), - // handler = MediaNextHandler(context) - // ) - + // 注册打开录音机功能 client.registerLocalFunction( name = "open_recorder", @@ -1189,145 +1131,6 @@ private class GetCurrentLocationHandler(private val context: Context?) : Functio } } -/** - * 媒体播放处理器 - */ -private class MediaPlayHandler(private val context: Context?) : FunctionHandler { - override suspend fun handle(arguments: Map): String { - if (context == null) { - return JSONObject().apply { - put("error", JSONObject().apply { - put("code", "context_error") - put("message", "上下文未初始化") - }) - }.toString() - } - - // 发送媒体播放广播 - val intent = Intent("com.yunqiinnovation.music_service.MEDIA_PLAY").apply { - setPackage(context.packageName) - } - context.sendBroadcast(intent) - - return JSONObject().apply { - put("result", "已开始播放媒体") - put("notreply", true) - put("broadcast", false) - }.toString() - } -} - -/** - * 媒体暂停处理器 - */ -private class MediaPauseHandler(private val context: Context?) : FunctionHandler { - override suspend fun handle(arguments: Map): String { - if (context == null) { - return JSONObject().apply { - put("error", JSONObject().apply { - put("code", "context_error") - put("message", "上下文未初始化") - }) - }.toString() - } - - Log.d("MediaPauseHandler", "📤 正在发送广播 context = $context") - // 发送媒体暂停广播 - val intent = Intent("com.yunqiinnovation.music_service.MEDIA_PAUSE").apply { - setPackage(context.packageName) - } - context.sendBroadcast(intent) - - return JSONObject().apply { - put("result", "已暂停媒体播放") - put("notreply", true) - put("broadcast", false) - }.toString() - } -} -/** - * 媒体关闭处理器 - */ -private class MediaCloseHandler(private val context: Context?) : FunctionHandler { - override suspend fun handle(arguments: Map): String { - if (context == null) { - return JSONObject().apply { - put("error", JSONObject().apply { - put("code", "context_error") - put("message", "上下文未初始化") - }) - }.toString() - } - - Log.d("MediaCloseHandler", "📤 正在发送广播 context = $context") - // 发送媒体关闭广播 - val intent = Intent("com.yunqiinnovation.music_service.MEDIA_CLOSE").apply { - setPackage(context.packageName) - } - context.sendBroadcast(intent) - - return JSONObject().apply { - put("result", "已关闭媒体播放") - put("notreply", true) - put("broadcast", false) - }.toString() - } -} -/** - * 媒体上一首处理器 - */ -private class MediaPreviousHandler(private val context: Context?) : FunctionHandler { - override suspend fun handle(arguments: Map): String { - if (context == null) { - return JSONObject().apply { - put("error", JSONObject().apply { - put("code", "context_error") - put("message", "上下文未初始化") - }) - }.toString() - } - - // 发送切换上一首广播 - val intent = Intent("com.yunqiinnovation.music_service.MEDIA_PREVIOUS").apply { - setPackage(context.packageName) - } - context.sendBroadcast(intent) - - return JSONObject().apply { - put("result", "已切换到上一首") - put("notreply", true) - put("broadcast", false) - }.toString() - } -} - -/** - * 媒体下一首处理器 - */ -private class MediaNextHandler(private val context: Context?) : FunctionHandler { - override suspend fun handle(arguments: Map): String { - if (context == null) { - return JSONObject().apply { - put("error", JSONObject().apply { - put("code", "context_error") - put("message", "上下文未初始化") - }) - }.toString() - } - - // 发送切换下一首广播 - val intent = Intent("com.yunqiinnovation.music_service.MEDIA_NEXT").apply { - setPackage(context.packageName) - } - context.sendBroadcast(intent) - - return JSONObject().apply { - put("result", "已切换到下一首") - put("notreply", true) - put("broadcast", false) - }.toString() - } -} /** * 打开录音机处理器 diff --git a/local_plugins/music_service/android/build.gradle b/local_plugins/music_service/android/build.gradle index 1f0ad0cf4..6e3624901 100644 --- a/local_plugins/music_service/android/build.gradle +++ b/local_plugins/music_service/android/build.gradle @@ -67,4 +67,5 @@ android { dependencies { implementation("androidx.media:media:1.6.0") + implementation("com.squareup.okhttp3:okhttp:4.12.0") } diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt index 3bfaa1e1b..c24a250c9 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt @@ -1,10 +1,6 @@ package com.yunqiinnovation.music_service import android.content.Context -import android.content.Intent -import androidx.core.content.ContextCompat -import android.util.Log -import com.yunqiinnovation.music_service.MusicServiceStarter import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel @@ -29,7 +25,7 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler { "play" -> { val song = call.argument>("song") if (song != null) { - MusicServiceStarter.startServiceWithCommand(context, "play", song) + MusicSingleton.startServiceWithCommand(context, "play", song) result.success(null) } else { result.error("INVALID_ARGUMENT", "Missing 'song' map", null) @@ -38,27 +34,27 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler { "playlist" -> { val songs = call.argument>>("songs") if (songs != null && songs.isNotEmpty()) { - MusicServiceStarter.startServiceWithPlaylist(context, "playlist", songs) + MusicSingleton.startServiceWithPlaylist(context, "playlist", songs) result.success(null) } else { result.error("INVALID_ARGUMENT", "Missing or empty 'songs' list", null) } } "pause" -> { - MusicServiceStarter.startServiceWithCommand(context, "pause") + MusicSingleton.startServiceWithCommand(context, "pause") result.success(null) } "resume" -> { - MusicServiceStarter.startServiceWithCommand(context, "resume") + MusicSingleton.startServiceWithCommand(context, "resume") result.success(null) } "stop" -> { - MusicServiceStarter.startServiceWithCommand(context, "stop") + MusicSingleton.startServiceWithCommand(context, "stop") result.success(null) } "setVolume" -> { val volume = call.argument("volume")?.toFloat() - MusicServiceStarter.startServiceWithCommand(context, "setVolume", volume = volume) + MusicSingleton.startServiceWithCommand(context, "setVolume", volume = volume) result.success(null) } "getStatus" -> { @@ -77,7 +73,7 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler { "seekTo" -> { val position = call.argument("position") if (position != null) { - MusicServiceStarter.startServiceWithCommand(context, "seekTo", position = position) + MusicSingleton.startServiceWithCommand(context, "seekTo", position = position) result.success(null) } else { result.error("INVALID_ARGUMENT", "Missing 'position' parameter", null) diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServiceStarter.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServiceStarter.kt deleted file mode 100644 index 08821753a..000000000 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServiceStarter.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.yunqiinnovation.music_service - -import android.content.Context -import android.content.Intent -import android.os.Build -import android.util.Log -import androidx.core.content.ContextCompat -import java.util.jar.Manifest - -//@Parcelize -//data class Song( -// val url: String, -// val title: String, -// val artist: String, -// val coverUrl: String? -//) - -object MusicServiceStarter { - private val TAG = "MusicServiceStarter" - //播放单曲 - fun startServiceWithCommand(context: Context, command: String, song: Map? = null, volume: Float? = null, position: Int? = null) { - Log.i(TAG, "liwei------------------- startServiceWithCommand: $command $song") - val intent = Intent(context, MusicService::class.java).apply { - putExtra("command", command) - song?.let { - putExtra("song", HashMap(it)) // 只有非 null 时才 putExtra - } - volume?.let { putExtra("volume", it) } - position?.let { putExtra("position", it) } - } - val success = try { - ContextCompat.startForegroundService(context, intent) - true - } catch (e: Exception) { - Log.e("MusicServiceStarter", "liwei------------------- Failed to start service", e) - false - } - Log.d(" MusicServiceStarter", "Service start success: $success") - } - - //启动播放列表 - fun startServiceWithPlaylist(context: Context,command: String, songs: List>,volume: Float? = null) { - Log.i(TAG, "liwei------------------- startServiceWithPlaylist: $command $songs") - val serializableList = ArrayList>().apply { - songs.forEach { add(HashMap(it)) } - } - val intent = Intent(context, MusicService::class.java).apply { - putExtra("command", command) - putExtra("songs", serializableList) // ✅ Serializable - volume?.let { putExtra("volume", it) } - } - ContextCompat.startForegroundService(context, intent) - - } -} \ No newline at end of file diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicSingleton.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicSingleton.kt new file mode 100644 index 000000000..773790a8e --- /dev/null +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicSingleton.kt @@ -0,0 +1,202 @@ +package com.yunqiinnovation.music_service + +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.core.content.ContextCompat +import kotlinx.coroutines.launch +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import okhttp3.Call +import okhttp3.Callback +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import org.json.JSONArray +import org.json.JSONObject +import java.io.IOException +import java.util.concurrent.TimeUnit + + +class MusicSingleton private constructor() { + companion object { + private val TAG = "MusicServiceStarter" + @Volatile + private var INSTANCE: MusicSingleton? = null + + fun getInstance(): MusicSingleton { + return INSTANCE ?: synchronized(this) { + INSTANCE ?: MusicSingleton().also { INSTANCE = it } + } + } + //播放单曲 + fun startServiceWithCommand( + context: Context, + command: String, + song: Map? = null, + volume: Float? = null, + position: Int? = null + ) { + Log.i(TAG, "liwei------------------- startServiceWithCommand: $command $song") + val intent = Intent(context, MusicService::class.java).apply { + putExtra("command", command) + song?.let { + putExtra("song", HashMap(it)) // 只有非 null 时才 putExtra + } + volume?.let { putExtra("volume", it) } + position?.let { putExtra("position", it) } + } + val success = try { + ContextCompat.startForegroundService(context, intent) + true + } catch (e: Exception) { + Log.e("MusicServiceStarter", "liwei------------------- Failed to start service", e) + false + } + Log.d(" MusicServiceStarter", "Service start success: $success") + } + + //启动播放列表 + fun startServiceWithPlaylist( + context: Context, + command: String, + songs: List>, + volume: Float? = null + ) { + Log.i(TAG, "liwei------------------- startServiceWithPlaylist: $command $songs") + val serializableList = ArrayList>().apply { + songs.forEach { add(HashMap(it)) } + } + val intent = Intent(context, MusicService::class.java).apply { + putExtra("command", command) + putExtra("songs", serializableList) // ✅ Serializable + volume?.let { putExtra("volume", it) } + } + ContextCompat.startForegroundService(context, intent) + + } + } + private var url = "" + private var token = "" + private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + /** + * 初始化单例,设置Activity上下文 + */ + fun initialize(context: Context,serviceUrl:String,usertoken:String) { + url = serviceUrl; + token = usertoken; + } + + /** + * 搜索歌曲(带自动登录检查和重试机制) + */ + fun searchSongs( + keyword: String, + searchType: Int = 0, + callback: (Result>>) -> Unit + ) { + // 使用协程异步处理,避免阻塞主线程 + coroutineScope.launch { + try { + // 配置带超时的 OkHttpClient + val client = OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build() + + // 构建搜索请求的 JSON 数据 + val jsonObject = JSONObject().apply { + put("keywords", keyword) + put("searchtype", searchType) + put("limit", 20) // 默认返回20首歌曲 + put("offset", 0) // 默认返回20首歌曲 + } + + val json = jsonObject.toString() + val mediaType = "application/json".toMediaType() + val body = json.toRequestBody(mediaType) + + // 构建请求 + val request = Request.Builder() + .url("$url/api/home/music_search") + .addHeader("Authorization", token) + .addHeader("Content-Type", "application/json") + .post(body) + .build() + + Log.d(TAG, "发送搜索请求: $json") + + // 使用异步调用 + client.newCall(request).enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + Log.e(TAG, "搜索歌曲请求失败: ${e.message}", e) + callback(Result.failure(e)) + } + + override fun onResponse(call: Call, response: Response) { + response.use { + try { + if (response.isSuccessful) { + val responseBody = response.body?.string() + Log.d(TAG, "搜索响应: $responseBody") + + if (responseBody != null) { + val jsonResponse = JSONObject(responseBody) + val songs = parseSongsFromResponse(jsonResponse) + callback(Result.success(songs)) + } else { + callback(Result.failure(Exception("响应体为空"))) + } + } else { + val errorMsg = "搜索失败,HTTP状态码: ${response.code}" + Log.w(TAG, errorMsg) + callback(Result.failure(Exception(errorMsg))) + } + } catch (e: Exception) { + Log.e(TAG, "解析搜索响应时发生异常: ${e.message}", e) + callback(Result.failure(e)) + } + } + } + }) + + } catch (e: Exception) { + Log.e(TAG, "搜索歌曲时发生异常: ${e.message}", e) + callback(Result.failure(e)) + } + } + } + + /** + * 解析服务器响应中的歌曲列表 + */ + private fun parseSongsFromResponse(jsonResponse: JSONObject): List> { + val songs = mutableListOf>() + try { + val data = jsonResponse.optJSONObject("data") + val songArray = data?.optJSONArray("musics") + + songArray?.let { array -> + for (i in 0 until array.length()) { + val songObj = array.getJSONObject(i) + songs.add(mapOf( + "id" to songObj.optLong("id"), + "name" to songObj.optString("name"), + "singer" to songObj.optString("singer"), + "url" to songObj.optString("url"), + "image" to songObj.optString("image"), + "ts" to songObj.optLong("ts"), + )) + } + } + } catch (e: Exception) { + Log.e(TAG, "Failed to parse songs from response", e) + } + return songs + } + +} \ No newline at end of file diff --git a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicManager.kt b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicManager.kt index 4fdc139ef..822a6cc06 100644 --- a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicManager.kt +++ b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicManager.kt @@ -59,6 +59,8 @@ class QQMusicManager(private val context: Context) { Log.d(TAG, "QQ音乐服务连接断开") qqmusicApi = null isBindQQMusicService = false + //重连绑定 + bindQQMusicApiService(); } } diff --git a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt index 9aad96445..f34f9d547 100644 --- a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt +++ b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt @@ -60,7 +60,7 @@ class QQMusicSingleton private constructor() { /** * 检查QQ音乐App是否已安装 */ - private fun isQQMusicInstalled(): Boolean { + fun isQQMusicInstalled(): Boolean { val packageName = when (Config.BIND_PLATFORM) { CommonCmd.AIDL_PLATFORM_TYPE_PHONE -> "com.tencent.qqmusic" CommonCmd.AIDL_PLATFORM_TYPE_CAR -> "com.tencent.qqmusiccar" diff --git a/pubspec.yaml b/pubspec.yaml index b5b804f1f..d63c9b133 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -139,5 +139,6 @@ flutter: - assets/images/headphone_new4.png - assets/images/headphone_new3.png - assets/images/qqmusic.png + - assets/images/music.png - .env - .mcp.json