From 261262f24be4d771264eb0cca076a50449f67226 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Tue, 24 Jun 2025 21:20:18 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat(=E9=9F=B3=E4=B9=90=E6=9C=8D=E5=8A=A1):?= =?UTF-8?q?=20=E5=AE=9E=E7=8E=B0=E5=AA=92=E4=BD=93=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96=E6=92=AD=E6=94=BE?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 在音乐服务插件中新增媒体会话功能,支持锁屏控制和通知栏显示 2. 为AgentController添加播放列表管理和媒体会话回调处理 3. 重构音乐播放列表界面,优化布局和交互体验 4. 修复播放进度同步和ID类型转换问题 5. 简化圆形进度按钮的点击交互逻辑 --- lib/data/services/music_manager.dart | 52 ++ .../agent/controllers/agent_controller.dart | 221 +++++- .../agent/views/circular_progress_button.dart | 32 +- .../login/controllers/login_controller.dart | 12 +- .../music_playlist_controller.dart | 5 +- .../music/views/music_playlist_view.dart | 672 ++++++++++++++---- .../music_service/MusicService.kt | 122 +++- .../music_service/MusicServicePlugin.kt | 25 + .../Sources/music_service/MusicService.swift | 156 ++++ .../music_service/lib/music_service.dart | 62 ++ 10 files changed, 1179 insertions(+), 180 deletions(-) diff --git a/lib/data/services/music_manager.dart b/lib/data/services/music_manager.dart index 26e7aef91..9bfae0e7a 100644 --- a/lib/data/services/music_manager.dart +++ b/lib/data/services/music_manager.dart @@ -85,4 +85,56 @@ class MusicManager extends GetxService { Future playPlaylist(List> playlist) async { await _musicService.playMusicPlaylist(playlist); } + + /// 更新媒体会话进度信息 + Future updateMediaSessionProgress( + int currentPosition, int duration, double progress) async { + try { + await _musicService.updateMediaSessionProgress( + currentPosition, duration, progress); + } catch (e) { + Logger.d('MusicManager', '更新媒体会话进度失败: $e'); + } + } + + /// 设置媒体会话回调 + Future setMediaSessionCallbacks({ + required Function() onPlay, + required Function() onPause, + required Function() onNext, + required Function() onPrevious, + required Function(int) onSeekTo, + required Function() onStop, + }) async { + try { + await _musicService.setMediaSessionCallbacks( + onPlay: onPlay, + onPause: onPause, + onNext: onNext, + onPrevious: onPrevious, + onSeekTo: onSeekTo, + onStop: onStop, + ); + } catch (e) { + Logger.d('MusicManager', '设置媒体会话回调失败: $e'); + } + } + + /// 更新媒体会话元数据 + Future updateMediaSessionMetadata(Map song) async { + try { + await _musicService.updateMediaSessionMetadata(song); + } catch (e) { + Logger.d('MusicManager', '更新媒体会话元数据失败: $e'); + } + } + + /// 更新媒体会话播放状态 + Future updateMediaSessionPlaybackState(bool isPlaying) async { + try { + await _musicService.updateMediaSessionPlaybackState(isPlaying); + } catch (e) { + Logger.d('MusicManager', '更新媒体会话播放状态失败: $e'); + } + } } diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 5d394178c..79d3e64c3 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -43,6 +43,10 @@ class AgentController extends GetxController { var duration = 0.obs; // 总时长(毫秒) var progress = 0.0.obs; // 播放进度(0.0-1.0) + // 当前播放列表管理 + final currentPlaylist = >[].obs; // 当前播放列表 + final currentPlaylistIndex = 0.obs; // 当前播放歌曲在列表中的索引 + // 各种状态标志 final isInitialized = false.obs; final isListening = false.obs; //是否正在语音识别 @@ -102,6 +106,7 @@ class AgentController extends GetxController { _loadChatHistory(); //加载聊天历史记录 _subscribeToEvents(); //订阅代理服务事件 _initMusicStatus(); //初始化音乐状态 + _setupMediaSessionCallbacks(); //设置媒体会话回调 } // 初始化音乐播放状态 @@ -114,6 +119,23 @@ class AgentController extends GetxController { } } + // 设置媒体会话回调 + void _setupMediaSessionCallbacks() async { + try { + await _musiceManager.setMediaSessionCallbacks( + onPlay: onMediaSessionPlay, + onPause: onMediaSessionPause, + onNext: onMediaSessionNext, + onPrevious: onMediaSessionPrevious, + onSeekTo: onMediaSessionSeekTo, + onStop: onMediaSessionStop, + ); + Logger.d('AgentController', '媒体会话回调设置完成'); + } catch (e) { + Logger.e('AgentController', '媒体会话回调设置失败: $e'); + } + } + @override void onClose() { AgentService.stopConversation(); // 停止当前会话 @@ -871,7 +893,11 @@ class AgentController extends GetxController { // 立即更新UI状态,提供即时反馈 isPlaying.value = true; - currentId.value = song['id'] ?? 0; + // 修复:确保currentId接收int类型 + final songId = song['id']; + currentId.value = songId is int + ? songId + : (int.tryParse(songId?.toString() ?? '0') ?? 0); // 重置进度状态 currentPosition.value = 0; @@ -898,7 +924,11 @@ class AgentController extends GetxController { print('getMusicUrl 响应数据: ${response.toString()}'); // 调试输出原始数据 final urldata = response['urls'][0] as Map; song["url"] = urldata['url'] ?? ''; - currentId.value = song['id'] ?? 0; + // 修复:确保currentId接收int类型 + final newSongId = song['id']; + currentId.value = newSongId is int + ? newSongId + : (int.tryParse(newSongId?.toString() ?? '0') ?? 0); Logger.d('AgentController', '获取新URL成功'); } catch (e) { Logger.e('AgentController', '获取新URL失败: $e'); @@ -921,6 +951,10 @@ class AgentController extends GetxController { // 调用音乐管理器播放 await _musiceManager.play(musicData); + // 更新媒体会话元数据和播放状态 + await _updateMediaSessionMetadata(song); + await _updateMediaSessionPlaybackState(true); + // 启动进度定时器 _startProgressTimer(); @@ -940,6 +974,9 @@ class AgentController extends GetxController { await _musiceManager.pause(); isPlaying.value = false; _stopProgressTimer(); + + // 更新媒体会话播放状态 + await _updateMediaSessionPlaybackState(false); } catch (e) { Logger.e('AgentController', '暂停音乐失败: $e'); } @@ -952,6 +989,10 @@ class AgentController extends GetxController { await _musiceManager.resume(); isPlaying.value = true; _startProgressTimer(); + + // 更新媒体会话播放状态 + await _updateMediaSessionPlaybackState(true); + Logger.d('AgentController', '音乐恢复播放成功'); } catch (e) { Logger.e('AgentController', '恢复播放音乐失败: $e'); @@ -997,22 +1038,66 @@ class AgentController extends GetxController { final newDuration = progressInfo['duration'] ?? 0; final newProgress = progressInfo['progress'] ?? 0.0; + bool hasUpdate = false; + // 只有当值发生变化时才更新,避免不必要的UI刷新 if (currentPosition.value != newPosition) { currentPosition.value = newPosition; + hasUpdate = true; } if (duration.value != newDuration) { duration.value = newDuration; + hasUpdate = true; } if ((progress.value - newProgress).abs() > 0.001) { // 避免浮点数精度问题 progress.value = newProgress; + hasUpdate = true; + } + + // 如果有进度更新,通知系统媒体会话更新进度信息 + if (hasUpdate) { + await _updateMediaSessionProgress(); } } catch (e) { Logger.e('AgentController', '获取播放进度失败: $e'); } } + /// 更新系统媒体会话的进度信息 + Future _updateMediaSessionProgress() async { + try { + // 这里将调用music_service插件的方法来更新系统媒体会话进度 + // 具体实现将在music_service插件中添加 + await _musiceManager.updateMediaSessionProgress( + currentPosition.value, + duration.value, + progress.value, + ); + } catch (e) { + // 忽略错误,避免影响正常播放 + Logger.d('AgentController', '更新媒体会话进度失败: $e'); + } + } + + /// 更新系统媒体会话的元数据 + Future _updateMediaSessionMetadata(Map song) async { + try { + await _musiceManager.updateMediaSessionMetadata(song); + } catch (e) { + Logger.d('AgentController', '更新媒体会话元数据失败: $e'); + } + } + + /// 更新系统媒体会话的播放状态 + Future _updateMediaSessionPlaybackState(bool isPlaying) async { + try { + await _musiceManager.updateMediaSessionPlaybackState(isPlaying); + } catch (e) { + Logger.d('AgentController', '更新媒体会话播放状态失败: $e'); + } + } + // 跳转到指定位置 Future seekToPosition(int positionMs) async { try { @@ -1071,45 +1156,51 @@ class AgentController extends GetxController { } Logger.d(TAG, '开始播放音乐列表,共${playlist.length}首歌曲'); - + + // 保存当前播放列表 + currentPlaylist.assignAll(playlist); + currentPlaylistIndex.value = 0; + // 设置加载状态 isLoadingMusic.value = true; loadingMusicId.value = playlist[0]['id']?.toString() ?? ''; - + // 重置播放状态 isPlaying.value = true; currentPosition.value = 0; duration.value = 0; progress.value = 0.0; - + // 格式化歌曲列表数据 final formattedPlaylist = playlist.map((song) { return { 'id': song['id']?.toString() ?? '', 'url': song['url']?.toString() ?? '', 'title': song['title']?.toString() ?? song['name']?.toString() ?? '', - 'artist': song['artist']?.toString() ?? song['sgener']?.toString() ?? '', - 'coverUrl': song['coverUrl']?.toString() ?? song['image']?.toString() ?? '', + 'artist': + song['artist']?.toString() ?? song['sgener']?.toString() ?? '', + 'coverUrl': + song['coverUrl']?.toString() ?? song['image']?.toString() ?? '', }; }).toList(); - + // 调用音乐管理器播放列表 await _musiceManager.playPlaylist(formattedPlaylist); - + // 更新当前播放歌曲ID if (formattedPlaylist.isNotEmpty) { currentId.value = int.tryParse(formattedPlaylist[0]['id'] ?? '0') ?? 0; } - + // 启动进度定时器 _startProgressTimer(); - + Logger.d(TAG, '音乐列表播放启动成功'); } catch (e) { Logger.e(TAG, '播放音乐列表失败: $e'); isPlaying.value = false; currentId.value = 0; - + // 显示错误提示 Get.snackbar( '播放失败', @@ -1125,4 +1216,110 @@ class AgentController extends GetxController { loadingMusicId.value = ''; } } + + /// 播放上一曲 + Future playPrevious() async { + if (currentPlaylist.isEmpty) { + Logger.w(TAG, '当前播放列表为空,无法播放上一曲'); + return; + } + + // 计算上一曲的索引 + int previousIndex = currentPlaylistIndex.value - 1; + if (previousIndex < 0) { + previousIndex = currentPlaylist.length - 1; // 循环到最后一首 + } + + currentPlaylistIndex.value = previousIndex; + final previousSong = currentPlaylist[previousIndex]; + + Logger.d(TAG, '播放上一曲: ${previousSong['name'] ?? previousSong['title']}'); + await musicPlay(previousSong); + } + + /// 播放下一曲 + Future playNext() async { + if (currentPlaylist.isEmpty) { + Logger.w(TAG, '当前播放列表为空,无法播放下一曲'); + return; + } + + // 计算下一曲的索引 + int nextIndex = currentPlaylistIndex.value + 1; + if (nextIndex >= currentPlaylist.length) { + nextIndex = 0; // 循环到第一首 + } + + currentPlaylistIndex.value = nextIndex; + final nextSong = currentPlaylist[nextIndex]; + + Logger.d(TAG, '播放下一曲: ${nextSong['name'] ?? nextSong['title']}'); + await musicPlay(nextSong); + } + + /// 更新当前播放列表和索引(当单独播放某首歌曲时调用) + void updateCurrentPlaylistContext( + List> playlist, int songId) { + currentPlaylist.assignAll(playlist); + + // 查找当前歌曲在列表中的索引 + int index = -1; + for (int i = 0; i < playlist.length; i++) { + if (int.tryParse(playlist[i]['id']?.toString() ?? '0') == songId) { + index = i; + break; + } + } + + if (index != -1) { + currentPlaylistIndex.value = index; + Logger.d(TAG, '更新播放列表上下文,当前歌曲索引: $index'); + } + } + + // ========== 媒体会话回调处理方法 ========== + + /// 处理来自系统通知栏的播放操作 + Future onMediaSessionPlay() async { + Logger.d(TAG, '收到媒体会话播放回调'); + if (!isPlaying.value) { + await musicResume(); + } + } + + /// 处理来自系统通知栏的暂停操作 + Future onMediaSessionPause() async { + Logger.d(TAG, '收到媒体会话暂停回调'); + if (isPlaying.value) { + await musicPause(); + } + } + + /// 处理来自系统通知栏的下一曲操作 + Future onMediaSessionNext() async { + Logger.d(TAG, '收到媒体会话下一曲回调'); + await playNext(); + } + + /// 处理来自系统通知栏的上一曲操作 + Future onMediaSessionPrevious() async { + Logger.d(TAG, '收到媒体会话上一曲回调'); + await playPrevious(); + } + + /// 处理来自系统通知栏的跳转操作 + Future onMediaSessionSeekTo(int positionMs) async { + Logger.d(TAG, '收到媒体会话跳转回调: ${positionMs}ms'); + await seekToPosition(positionMs); + } + + /// 处理来自系统通知栏的停止操作 + Future onMediaSessionStop() async { + Logger.d(TAG, '收到媒体会话停止回调'); + await musicPause(); + // 重置播放状态 + currentPosition.value = 0; + progress.value = 0.0; + _stopProgressTimer(); + } } diff --git a/lib/modules/agent/views/circular_progress_button.dart b/lib/modules/agent/views/circular_progress_button.dart index 510bf95a4..28729d231 100644 --- a/lib/modules/agent/views/circular_progress_button.dart +++ b/lib/modules/agent/views/circular_progress_button.dart @@ -27,35 +27,9 @@ class CircularProgressButton extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - onTapDown: (details) { - if (onSeek != null) { - // 计算点击位置对应的进度 - final center = Offset(size / 2, size / 2); - final tapPosition = details.localPosition; - final dx = tapPosition.dx - center.dx; - final dy = tapPosition.dy - center.dy; - - // 计算角度 (从顶部开始,顺时针) - double angle = math.atan2(dy, dx) + math.pi / 2; - if (angle < 0) angle += 2 * math.pi; - - // 转换为进度值 (0.0 - 1.0) - final seekProgress = angle / (2 * math.pi); - - // 检查点击是否在圆环区域内 - final distance = math.sqrt(dx * dx + dy * dy); - final innerRadius = (size / 2) - strokeWidth - 4; - final outerRadius = (size / 2) + 4; - - if (distance >= innerRadius && distance <= outerRadius) { - onSeek!(seekProgress); - } else if (distance < innerRadius) { - // 点击中心区域,播放/暂停 - onPlayPause(); - } - } else { - onPlayPause(); - } + onTap: () { + // 只保留播放/暂停功能,移除进度条点击交互 + onPlayPause(); }, child: SizedBox( width: size, diff --git a/lib/modules/login/controllers/login_controller.dart b/lib/modules/login/controllers/login_controller.dart index d6cc76ca1..a06a2cd1f 100644 --- a/lib/modules/login/controllers/login_controller.dart +++ b/lib/modules/login/controllers/login_controller.dart @@ -656,7 +656,7 @@ class LoginController extends GetxController { // }); try { switch (loginType) { - case 0: + case 0: //邮箱登录 { if (!isVerificationCodeComplete()) { Get.snackbar( @@ -670,7 +670,7 @@ class LoginController extends GetxController { } requestLogin({ "mail": contactController.text, - "phone": selectedCountryCode + contactController.text, + "phone": "", "stype": loginType, "vcode": verificationCodeController.text, "ttoken": "", @@ -681,7 +681,7 @@ class LoginController extends GetxController { }); break; } - case 1: + case 1: //手机号登录 { if (!isVerificationCodeComplete()) { Get.snackbar( @@ -706,7 +706,7 @@ class LoginController extends GetxController { }); break; } - case 4: + case 4: //Facebook登录 { print("数据获取Facebook"); final facebook = Facebook(); @@ -736,7 +736,7 @@ class LoginController extends GetxController { } break; } - case 3: + case 3: //Google登录 { print("数据获取_Google()"); final google = Google(); @@ -766,7 +766,7 @@ class LoginController extends GetxController { break; } - case 2: + case 2: //微信登录 { print("数据获取_FlWX()"); diff --git a/lib/modules/music/controllers/music_playlist_controller.dart b/lib/modules/music/controllers/music_playlist_controller.dart index ac0c8d2ee..367d2b560 100644 --- a/lib/modules/music/controllers/music_playlist_controller.dart +++ b/lib/modules/music/controllers/music_playlist_controller.dart @@ -12,8 +12,5 @@ class MusicPlaylistController extends GetxController { playlist.assignAll(playlistData); } - @override - void onClose() { - super.onClose(); - } + } diff --git a/lib/modules/music/views/music_playlist_view.dart b/lib/modules/music/views/music_playlist_view.dart index a66d228a3..eaffa21c2 100644 --- a/lib/modules/music/views/music_playlist_view.dart +++ b/lib/modules/music/views/music_playlist_view.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import '../../agent/controllers/agent_controller.dart'; import '../../agent/views/circular_progress_button.dart'; +import '../../../core/utils/logger.dart'; class MusicPlaylistView extends StatelessWidget { const MusicPlaylistView({Key? key}) : super(key: key); @@ -18,7 +20,6 @@ class MusicPlaylistView extends StatelessWidget { icon: const Icon(Icons.arrow_back_ios, size: 20), // iOS风格箭头 onPressed: () => Navigator.pop(context), ), - //title: const Text('音乐播放列表'), backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, foregroundColor: isDarkMode ? Colors.white : Colors.black, elevation: 0, @@ -45,32 +46,177 @@ class MusicPlaylistView extends StatelessWidget { ], ), ) - : ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: playlist.length, - itemBuilder: (context, index) { - final item = playlist[index]; - final id = item["id"]?.toString() ?? ""; - final name = item["name"] ?? "未知歌曲"; - final url = item["url"] ?? ""; - final singer = item["singer"] ?? item["sgener"] ?? "未知歌手"; - final image = item["image"] ?? ""; - - return _buildMusicItem( - context, - controller, - id, - name, - url, - singer, - image, - isDarkMode, - ); - }, + : Column( + children: [ + // 上部:当前播放音乐详情 - 进一步优化高度占比 + SizedBox( + height: MediaQuery.of(context).size.height * + 0.35, // 从35%减少到32%,为进度条优化腾出空间 + child: _buildCurrentPlayingSection( + controller, playlist, isDarkMode), + ), + // 中部:播放全部按钮 + _buildPlayAllSection(controller, playlist, isDarkMode), + // 下部:歌单列表 - 使用剩余空间 + Expanded( + child: + _buildPlaylistSection(controller, playlist, isDarkMode), + ), + ], ), ); } + // 构建当前播放音乐详情部分 + Widget _buildCurrentPlayingSection(AgentController controller, + List> playlist, bool isDarkMode) { + return Obx(() { + // 查找当前播放的歌曲 - 修复自动切换时不更新的问题 + Map? currentSong; + if (controller.currentId.value != 0) { + try { + currentSong = playlist.firstWhere( + (song) => + int.tryParse(song['id']?.toString() ?? '0') == + controller.currentId.value, + ); + } catch (e) { + // 如果没找到,使用第一首歌曲 + currentSong = playlist.isNotEmpty ? playlist.first : null; + } + } else { + currentSong = playlist.isNotEmpty ? playlist.first : null; + } + + if (currentSong == null) { + return Container( + padding: const EdgeInsets.all(16), // 减少padding + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.music_note, + size: 60, // 减小图标尺寸 + color: Colors.grey[400], + ), + const SizedBox(height: 12), // 减少间距 + Text( + '暂无播放音乐', + style: TextStyle( + fontSize: 16, // 减小字体 + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + ), + ], + ), + ), + ); + } + + final title = currentSong['name'] ?? currentSong['title'] ?? '未知歌曲'; + final artist = currentSong['singer'] ?? + currentSong['sgener'] ?? + currentSong['artist'] ?? + '未知歌手'; + final coverUrl = currentSong['image'] ?? currentSong['coverUrl'] ?? ''; + + return SingleChildScrollView( + // 添加滚动支持 + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 12), // 优化padding:水平增加,垂直减少 + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 封面图片 - 进一步优化尺寸 + _buildCoverImage(coverUrl, isDarkMode, + size: 110), // 从120减小到110,为进度条优化腾出空间 + const SizedBox(height: 12), // 减少间距从16到14 + // 歌曲信息 + _buildSongInfo(title, artist, isDarkMode), + const SizedBox(height: 16), // 增加歌曲信息和进度条之间的间距,提升视觉层次 + // 进度条 - 现在是水平布局,需要更多关注 + _buildProgressBar(controller, isDarkMode), + const SizedBox(height: 15), // 增加进度条和控制按钮之间的间距 + // 播放控制按钮 + _buildPlayControls(controller, playlist, isDarkMode), + ], + ), + ), + ); + }); + } + + // 构建播放全部按钮部分 + Widget _buildPlayAllSection(AgentController controller, + List> playlist, bool isDarkMode) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 12), // 优化padding,与音乐详情区域保持一致 + child: SizedBox( + width: double.infinity, + height: 48, // 从50减小到48,更紧凑 + child: ElevatedButton.icon( + onPressed: () async { + if (playlist.isNotEmpty) { + await controller.playPlaylist(playlist); + } + }, + icon: const Icon(Icons.play_arrow, + color: Colors.white, size: 20), // 调整图标大小 + label: const Text( + '播放全部', + style: TextStyle( + fontSize: 15, // 从16减小到15 + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(Get.context!).primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), // 从25减小到14,更协调 + ), + elevation: 3, // 从2增加到3,增强视觉层次 + shadowColor: Theme.of(Get.context!) + .primaryColor + .withValues(alpha: 0.4), // 添加阴影颜色 + ), + ), + ), + ); + } + + // 构建歌单列表部分 + Widget _buildPlaylistSection(AgentController controller, + List> playlist, bool isDarkMode) { + return ListView.builder( + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 8), // 优化padding,与其他区域保持一致 + itemCount: playlist.length, + itemBuilder: (context, index) { + final item = playlist[index]; + final id = item["id"]?.toString() ?? ""; + final name = item["name"] ?? "未知歌曲"; + final url = item["url"] ?? ""; + final singer = item["singer"] ?? item["sgener"] ?? "未知歌手"; + final image = item["image"] ?? ""; + + return _buildMusicItem( + context, + controller, + id, + name, + url, + singer, + image, + isDarkMode, + ); + }, + ); + } + Widget _buildMusicItem( BuildContext context, AgentController controller, @@ -89,123 +235,399 @@ class MusicPlaylistView extends StatelessWidget { 'coverUrl': image, }; + return GestureDetector( + onTap: () { + // 点击音乐卡片直接播放对应歌曲 + _playSelectedSong(controller, song, Get.arguments ?? []); + }, + child: Container( + margin: const EdgeInsets.only(bottom: 10), // 从12减小到10,更紧凑 + padding: const EdgeInsets.all(14), // 从12增加到14,增加内部空间 + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[800] : Colors.white, + borderRadius: BorderRadius.circular(14), // 从12增加到14,更现代的圆角 + boxShadow: [ + BoxShadow( + color: Colors.black + .withValues(alpha: isDarkMode ? 0.25 : 0.05), // 增强阴影效果 + blurRadius: 4, // 从3增加到4 + spreadRadius: 0, // 从1减小到0,更自然的阴影 + offset: const Offset(0, 2), // 从(0,1)调整到(0,2),更明显的深度感 + ), + ], + ), + child: Row( + children: [ + // 封面图片 - 优化设计 + Container( + width: 52, // 从50增加到52,稍微大一点 + height: 52, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), // 从8增加到10,更现代的圆角 + color: isDarkMode + ? Colors.grey[700] + : Colors.grey[200], // 根据主题调整背景色 + boxShadow: [ + BoxShadow( + color: + Colors.black.withValues(alpha: isDarkMode ? 0.2 : 0.08), + blurRadius: 2, + offset: const Offset(0, 1), + ), + ], + ), + child: image.isNotEmpty + ? ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Image.network( + image, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Icon( + Icons.music_note, + color: isDarkMode + ? Colors.grey[500] + : Colors.grey[600], + size: 24, // 指定图标大小 + ); + }, + ), + ) + : Icon( + Icons.music_note, + color: isDarkMode ? Colors.grey[500] : Colors.grey[600], + size: 24, // 指定图标大小 + ), + ), + const SizedBox(width: 14), // 从12增加到14,增加间距 + // 歌曲信息 - 优化文本样式 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, // 垂直居中对齐 + children: [ + Text( + name, + style: TextStyle( + fontSize: 15.5, // 从16减小到15.5,更协调 + fontWeight: FontWeight.w600, // 从w500增加到w600,更突出 + color: isDarkMode ? Colors.white : Colors.black87, + height: 1.2, // 添加行高控制 + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 3), // 从4减小到3,更紧凑 + Text( + singer, + style: TextStyle( + fontSize: 13, // 从14减小到13 + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + height: 1.1, // 添加行高控制 + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const SizedBox(width: 14), // 从12增加到14,与封面图片间距保持一致 + // 播放按钮 + Obx(() { + // 修复类型比较问题:将id转换为int进行比较 + final currentIdInt = int.tryParse(id) ?? 0; + final isThisPlaying = controller.isPlaying.value && + controller.currentId.value == currentIdInt; + + // 只有当前播放的歌曲才显示进度 + final currentProgress = + isThisPlaying ? controller.progress.value : 0.0; + + return CircularProgressButton( + isPlaying: isThisPlaying, + progress: currentProgress, + primaryColor: Theme.of(context).primaryColor, + backgroundColor: Colors.grey, + size: 40.0, + strokeWidth: 2.5, + onPlayPause: () async { + try { + Logger.d('MusicPlaylistView', + '播放按钮被点击,当前播放状态: ${controller.isPlaying.value}'); + Logger.d('MusicPlaylistView', + '当前歌曲ID: ${controller.currentId.value}, 点击歌曲ID: $currentIdInt'); + + if (controller.isPlaying.value) { + if (controller.currentId.value == currentIdInt) { + Logger.d('MusicPlaylistView', '暂停当前播放的歌曲'); + await controller.musicPause(); + } else { + Logger.d('MusicPlaylistView', '播放新歌曲'); + await controller.musicPlay(song); + } + } else { + Logger.d('MusicPlaylistView', '开始播放歌曲'); + await controller.musicPlay(song); + } + } catch (e) { + Logger.e('MusicPlaylistView', '播放音乐时发生错误: $e'); + } + }, + onSeek: isThisPlaying + ? (seekProgress) { + // 只有当前播放的歌曲才允许跳转 + final targetPosition = + (controller.duration.value * seekProgress).round(); + controller.seekToPosition(targetPosition); + } + : null, + ); + }), + ], + ), + ), + ); + } + + // 构建封面图片 + Widget _buildCoverImage(String coverUrl, bool isDarkMode, + {double size = 200}) { return Container( - margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.all(12), + width: size, + height: size, decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[800] : Colors.white, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: isDarkMode ? 0.2 : 0.03), - blurRadius: 3, - spreadRadius: 1, - offset: const Offset(0, 1), + color: Colors.black.withValues(alpha: isDarkMode ? 0.3 : 0.1), + blurRadius: 15, + spreadRadius: 2, + offset: const Offset(0, 6), ), ], ), - child: Row( + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: coverUrl.isNotEmpty + ? Image.network( + coverUrl, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return _buildDefaultCover(isDarkMode); + }, + ) + : _buildDefaultCover(isDarkMode), + ), + ); + } + + // 构建默认封面 + Widget _buildDefaultCover(bool isDarkMode) { + return Container( + color: isDarkMode ? Colors.grey[800] : Colors.grey[300], + child: Icon( + Icons.music_note, + size: 60, // 从20改为60,更合适的大小 + color: isDarkMode ? Colors.grey[600] : Colors.grey[500], + ), + ); + } + + // 构建歌曲信息 + Widget _buildSongInfo(String title, String artist, bool isDarkMode) { + return Column( + children: [ + Text( + title, + style: TextStyle( + fontSize: 18, // 从20减小到18 + fontWeight: FontWeight.bold, + color: isDarkMode ? Colors.white : Colors.black87, + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 6), // 从8减小到6 + Text( + artist, + style: TextStyle( + fontSize: 14, // 从16减小到14 + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ); + } + + // 构建进度条 - 优化布局:将进度条和时间显示合并到同一水平线 + Widget _buildProgressBar(AgentController controller, bool isDarkMode) { + return Obx(() { + return Row( children: [ - // 封面图片 - Container( - width: 50, - height: 50, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: Colors.grey[300], + // 左侧:当前播放时间 + SizedBox( + width: 35.w, // 固定宽度确保对齐,足够显示 "99:59" 格式 + + child: Text( + _formatDuration(controller.currentPosition.value), + style: TextStyle( + fontSize: 12, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + fontFeatures: const [FontFeature.tabularFigures()], // 使用等宽数字 + ), + textAlign: TextAlign.left, ), - child: image.isNotEmpty - ? ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Image.network( - image, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Icon( - Icons.music_note, - color: Colors.grey[600], - ); - }, - ), - ) - : Icon( - Icons.music_note, - color: Colors.grey[600], - ), ), - const SizedBox(width: 12), - // 歌曲信息 + SizedBox(width: 6.w), // 左侧时间和进度条之间的间距 + // 中间:可交互的进度条 Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - name, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: isDarkMode ? Colors.white : Colors.black87, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 4), - Text( - singer, - style: TextStyle( - fontSize: 14, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, + child: SliderTheme( + data: SliderTheme.of(Get.context!).copyWith( + 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.withValues(alpha: 0.2), + trackHeight: 4.h, + thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 0), + overlayShape: const RoundSliderOverlayShape(overlayRadius: 0), + ), + child: Slider( + value: controller.progress.value.clamp(0.0, 1.0), + onChanged: (value) { + final targetPosition = + (controller.duration.value * value).round(); + controller.seekToPosition(targetPosition); + }, + ), + ), + ), + SizedBox(width: 6.w), // 进度条和右侧时间之间的间距 + // 右侧:音乐总时长 + SizedBox( + width: 35.w, // 固定宽度确保对齐,足够显示 "99:59" 格式 + child: Text( + _formatDuration(controller.duration.value), + style: TextStyle( + fontSize: 12, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + fontFeatures: const [FontFeature.tabularFigures()], // 使用等宽数字 + ), + textAlign: TextAlign.right, + ), + ), + ], + ); + }); + } + + // 格式化时间显示 + 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')}'; + } + + // 构建播放控制按钮 + Widget _buildPlayControls(AgentController controller, + List> playlist, bool isDarkMode) { + return Obx(() { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 上一曲按钮 + IconButton( + onPressed: () => _playPrevious(controller, playlist), + icon: Icon( + Icons.skip_previous, + size: 32, // 从36减小到32 + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + const SizedBox(width: 16), // 从20减小到16 + // 播放/暂停按钮 + Container( + width: 56, // 从64减小到56 + height: 56, + decoration: BoxDecoration( + color: Theme.of(Get.context!).primaryColor, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Theme.of(Get.context!) + .primaryColor + .withValues(alpha: 0.3), + blurRadius: 10, // 从12减小到10 + spreadRadius: 1, // 从2减小到1 + offset: const Offset(0, 3), // 从4减小到3 ), ], ), + child: IconButton( + onPressed: () => _togglePlayPause(controller), + icon: Icon( + controller.isPlaying.value ? Icons.pause : Icons.play_arrow, + size: 28, // 从32减小到28 + color: Colors.white, + ), + ), + ), + const SizedBox(width: 16), // 从20减小到16 + // 下一曲按钮 + IconButton( + onPressed: () => _playNext(controller, playlist), + icon: Icon( + Icons.skip_next, + size: 32, // 从36减小到32 + color: isDarkMode ? Colors.white70 : Colors.black54, + ), ), - const SizedBox(width: 12), - // 播放按钮 - Obx(() { - final isThisPlaying = - controller.isPlaying.value && controller.currentId.value == id; - - // 只有当前播放的歌曲才显示进度 - final currentProgress = - isThisPlaying ? controller.progress.value : 0.0; - - return CircularProgressButton( - isPlaying: isThisPlaying, - progress: currentProgress, - primaryColor: Theme.of(context).primaryColor, - backgroundColor: Colors.grey, - size: 40.0, - strokeWidth: 2.5, - onPlayPause: () async { - try { - if (controller.isPlaying.value) { - if (controller.currentId.value == id) { - await controller.musicPause(); - } else { - await controller.musicPlay(song); - } - } else { - controller.musicPlay(song); - } - } catch (e) { - // 静默处理错误 - } - }, - onSeek: isThisPlaying - ? (seekProgress) { - // 只有当前播放的歌曲才允许跳转 - final targetPosition = - (controller.duration.value * seekProgress).round(); - controller.seekToPosition(targetPosition); - } - : null, - ); - }), ], - ), - ); + ); + }); + } + + // 播放/暂停切换 + Future _togglePlayPause(AgentController controller) async { + if (controller.isPlaying.value) { + await controller.musicPause(); + } else { + await controller.musicResume(); + } + } + + // 播放上一曲 + void _playPrevious( + AgentController controller, List> playlist) { + // 确保播放列表上下文是最新的 + controller.updateCurrentPlaylistContext( + playlist, controller.currentId.value); + controller.playPrevious(); + } + + // 播放下一曲 + void _playNext( + AgentController controller, List> playlist) { + // 确保播放列表上下文是最新的 + controller.updateCurrentPlaylistContext( + playlist, controller.currentId.value); + controller.playNext(); + } + + // 播放选中的歌曲 + void _playSelectedSong(AgentController controller, Map song, + List> playlist) { + // 更新播放列表上下文 + final songId = int.tryParse(song['id']?.toString() ?? '0') ?? 0; + controller.updateCurrentPlaylistContext(playlist, songId); + + // 播放选中的歌曲 + controller.musicPlay(song); } } diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt index 682098255..b807dbc03 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt @@ -76,6 +76,88 @@ class MusicService : MediaBrowserServiceCompat() { } private var instance: MusicService? = null + private var flutterChannel: io.flutter.plugin.common.MethodChannel? = null + + // 更新媒体会话进度信息 + fun updateMediaSessionProgress(currentPosition: Int, duration: Int, progress: Float) { + instance?.let { service -> + try { + val state = if (currentStatus) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED + val playbackState = PlaybackStateCompat.Builder() + .setActions( + PlaybackStateCompat.ACTION_PLAY or + PlaybackStateCompat.ACTION_PAUSE or + PlaybackStateCompat.ACTION_PLAY_PAUSE or + PlaybackStateCompat.ACTION_SKIP_TO_NEXT or + PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or + PlaybackStateCompat.ACTION_STOP or + PlaybackStateCompat.ACTION_SEEK_TO + ) + .setState(state, currentPosition.toLong(), 1.0f, System.currentTimeMillis()) + .build() + service.mediaSession.setPlaybackState(playbackState) + } catch (e: Exception) { + Log.e(TAG, "更新媒体会话进度失败: ${e.message}") + } + } + } + + // 启用媒体会话回调 + fun enableMediaSessionCallbacks(channel: io.flutter.plugin.common.MethodChannel) { + flutterChannel = channel + Log.d(TAG, "媒体会话回调已启用") + } + + // 更新媒体会话元数据 + fun updateMediaSessionMetadata(song: Map) { + instance?.let { service -> + try { + val title = song["title"] as? String ?: song["name"] as? String ?: "未知标题" + val artist = song["artist"] as? String ?: song["sgener"] as? String ?: "未知歌手" + val coverUrl = song["coverUrl"] as? String ?: song["image"] as? String ?: "" + + val metadata = android.support.v4.media.MediaMetadataCompat.Builder() + .putString(android.support.v4.media.MediaMetadataCompat.METADATA_KEY_TITLE, title) + .putString(android.support.v4.media.MediaMetadataCompat.METADATA_KEY_ARTIST, artist) + .putString(android.support.v4.media.MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, coverUrl) + .build() + + service.mediaSession.setMetadata(metadata) + Log.d(TAG, "媒体会话元数据已更新: $title - $artist") + } catch (e: Exception) { + Log.e(TAG, "更新媒体会话元数据失败: ${e.message}") + } + } + } + + // 更新媒体会话播放状态 + fun updateMediaSessionPlaybackState(isPlaying: Boolean) { + instance?.let { service -> + try { + currentStatus = isPlaying + val state = if (isPlaying) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED + val position = service.mediaPlayer?.currentPosition?.toLong() ?: 0L + + val playbackState = PlaybackStateCompat.Builder() + .setActions( + PlaybackStateCompat.ACTION_PLAY or + PlaybackStateCompat.ACTION_PAUSE or + PlaybackStateCompat.ACTION_PLAY_PAUSE or + PlaybackStateCompat.ACTION_SKIP_TO_NEXT or + PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or + PlaybackStateCompat.ACTION_STOP or + PlaybackStateCompat.ACTION_SEEK_TO + ) + .setState(state, position, 1.0f, System.currentTimeMillis()) + .build() + + service.mediaSession.setPlaybackState(playbackState) + Log.d(TAG, "媒体会话播放状态已更新: $isPlaying") + } catch (e: Exception) { + Log.e(TAG, "更新媒体会话播放状态失败: ${e.message}") + } + } + } } private var isReceiverRegistered = false private lateinit var mediaSession: MediaSessionCompat @@ -198,10 +280,42 @@ class MusicService : MediaBrowserServiceCompat() { ) // 回调中处理具体操作 setCallback(object : MediaSessionCompat.Callback() { - override fun onPlay() { Log.i(TAG, "onPlay 通知!"); resume()} - override fun onPause() { Log.i(TAG, "onPause 通知!"); pause()} - override fun onSkipToNext(){ Log.i(TAG, "onSkipToNext 通知!"); playNext()} - override fun onSkipToPrevious() { Log.i(TAG, "onSkipToNext 通知!"); playPrevious()} + override fun onPlay() { + Log.i(TAG, "onPlay 通知!") + resume() + // 通知Flutter层 + flutterChannel?.invokeMethod("onMediaSessionPlay", null) + } + override fun onPause() { + Log.i(TAG, "onPause 通知!") + pause() + // 通知Flutter层 + flutterChannel?.invokeMethod("onMediaSessionPause", null) + } + override fun onSkipToNext(){ + Log.i(TAG, "onSkipToNext 通知!") + playNext() + // 通知Flutter层 + flutterChannel?.invokeMethod("onMediaSessionNext", null) + } + override fun onSkipToPrevious() { + Log.i(TAG, "onSkipToPrevious 通知!") + playPrevious() + // 通知Flutter层 + flutterChannel?.invokeMethod("onMediaSessionPrevious", null) + } + override fun onSeekTo(pos: Long) { + Log.i(TAG, "onSeekTo 通知: ${pos}ms") + seekTo(pos.toInt()) + // 通知Flutter层 + flutterChannel?.invokeMethod("onMediaSessionSeekTo", mapOf("position" to pos.toInt())) + } + override fun onStop() { + Log.i(TAG, "onStop 通知!") + stopSelf() + // 通知Flutter层 + flutterChannel?.invokeMethod("onMediaSessionStop", null) + } }) // 初始播放状态 setPlaybackState( 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 74ee11e41..0c2fe1256 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 @@ -83,6 +83,31 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler { result.error("INVALID_ARGUMENT", "Missing 'position' parameter", null) } } + "updateMediaSessionProgress" -> { + val currentPosition = call.argument("currentPosition") ?: 0 + val duration = call.argument("duration") ?: 0 + val progress = call.argument("progress") ?: 0.0 + MusicService.updateMediaSessionProgress(currentPosition, duration, progress.toFloat()) + result.success(null) + } + "enableMediaSessionCallbacks" -> { + MusicService.enableMediaSessionCallbacks(channel) + result.success(null) + } + "updateMediaSessionMetadata" -> { + val song = call.argument>("song") + if (song != null) { + MusicService.updateMediaSessionMetadata(song) + result.success(null) + } else { + result.error("INVALID_ARGUMENT", "Song data is required", null) + } + } + "updateMediaSessionPlaybackState" -> { + val isPlaying = call.argument("isPlaying") ?: false + MusicService.updateMediaSessionPlaybackState(isPlaying) + result.success(null) + } else -> result.notImplemented() } } diff --git a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift index 81aa3d2c3..45b830e4a 100644 --- a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift +++ b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift @@ -16,6 +16,8 @@ public class MusicService: NSObject { private var playlist: [[String: String]] = [] private var currentIndex = 0 public var isPlaying = false + private var flutterChannel: FlutterMethodChannel? + private var progressTimer: Timer? private override init() { super.init() @@ -147,4 +149,158 @@ public class MusicService: NSObject { private func updatePlaybackState() { // iOS manages playback state automatically, but you can update NowPlayingInfo if needed } + + // MARK: - 新增媒体会话功能 + + /// 更新媒体会话进度信息 + public func updateMediaSessionProgress(currentPosition: Int, duration: Int, progress: Float) { + var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:] + + // 更新播放时长和当前位置 + nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = Double(duration) / 1000.0 + nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = Double(currentPosition) / 1000.0 + nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0 + + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + } + + /// 启用媒体会话回调 + public func enableMediaSessionCallbacks(channel: FlutterMethodChannel) { + flutterChannel = channel + setupEnhancedRemoteCommandCenter() + startProgressTimer() + } + + /// 更新媒体会话元数据 + public func updateMediaSessionMetadata(song: [String: Any]) { + let title = song["title"] as? String ?? song["name"] as? String ?? "Unknown Title" + let artist = song["artist"] as? String ?? song["sgener"] as? String ?? "Unknown Artist" + let coverUrl = song["coverUrl"] as? String ?? song["image"] as? String ?? "" + + var nowPlayingInfo: [String: Any] = [ + MPMediaItemPropertyTitle: title, + MPMediaItemPropertyArtist: artist + ] + + // 异步加载封面图片 + if !coverUrl.isEmpty, let url = URL(string: coverUrl) { + DispatchQueue.global(qos: .background).async { + if let data = try? Data(contentsOf: url), + let image = UIImage(data: data) { + DispatchQueue.main.async { + nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in image } + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + } + } else { + DispatchQueue.main.async { + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + } + } + } + } else { + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + } + } + + /// 更新媒体会话播放状态 + public func updateMediaSessionPlaybackState(isPlaying: Bool) { + self.isPlaying = isPlaying + var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:] + nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0 + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + + if isPlaying { + startProgressTimer() + } else { + stopProgressTimer() + } + } + + /// 设置增强的远程控制中心 + private func setupEnhancedRemoteCommandCenter() { + let commandCenter = MPRemoteCommandCenter.shared() + + // 清除之前的目标 + commandCenter.playCommand.removeTarget(nil) + commandCenter.pauseCommand.removeTarget(nil) + commandCenter.nextTrackCommand.removeTarget(nil) + commandCenter.previousTrackCommand.removeTarget(nil) + + // 播放命令 + commandCenter.playCommand.addTarget { [weak self] _ in + self?.resume() + self?.flutterChannel?.invokeMethod("onMediaSessionPlay", arguments: nil) + return .success + } + + // 暂停命令 + commandCenter.pauseCommand.addTarget { [weak self] _ in + self?.pause() + self?.flutterChannel?.invokeMethod("onMediaSessionPause", arguments: nil) + return .success + } + + // 下一曲命令 + commandCenter.nextTrackCommand.addTarget { [weak self] _ in + self?.next() + self?.flutterChannel?.invokeMethod("onMediaSessionNext", arguments: nil) + return .success + } + + // 上一曲命令 + commandCenter.previousTrackCommand.addTarget { [weak self] _ in + self?.previous() + self?.flutterChannel?.invokeMethod("onMediaSessionPrevious", arguments: nil) + return .success + } + + // 跳转命令 + commandCenter.changePlaybackPositionCommand.isEnabled = true + commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in + if let event = event as? MPChangePlaybackPositionCommandEvent { + let positionMs = Int(event.positionTime * 1000) + self?.seekTo(positionMs: positionMs) + self?.flutterChannel?.invokeMethod("onMediaSessionSeekTo", arguments: ["position": positionMs]) + return .success + } + return .commandFailed + } + + // 停止命令 + commandCenter.stopCommand.addTarget { [weak self] _ in + self?.stop() + self?.flutterChannel?.invokeMethod("onMediaSessionStop", arguments: nil) + return .success + } + } + + /// 启动进度定时器 + private func startProgressTimer() { + stopProgressTimer() + progressTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in + self?.updateProgressInfo() + } + } + + /// 停止进度定时器 + private func stopProgressTimer() { + progressTimer?.invalidate() + progressTimer = nil + } + + /// 更新进度信息 + private func updateProgressInfo() { + guard let player = player, let currentItem = player.currentItem, isPlaying else { return } + + let currentTime = CMTimeGetSeconds(player.currentTime()) + let duration = CMTimeGetSeconds(currentItem.duration) + + if !currentTime.isNaN && !currentTime.isInfinite && !duration.isNaN && !duration.isInfinite { + var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:] + nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime + nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration + nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 1.0 + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + } + } } diff --git a/local_plugins/music_service/lib/music_service.dart b/local_plugins/music_service/lib/music_service.dart index ac091afb0..b9d43552e 100644 --- a/local_plugins/music_service/lib/music_service.dart +++ b/local_plugins/music_service/lib/music_service.dart @@ -79,4 +79,66 @@ class MusicService { Future playMusicPlaylist(List> songs) async { await _methodChannel.invokeMethod('playlist', {'songs': songs}); } + + /// 更新媒体会话进度信息 + Future updateMediaSessionProgress( + int currentPosition, int duration, double progress) async { + await _methodChannel.invokeMethod('updateMediaSessionProgress', { + 'currentPosition': currentPosition, + 'duration': duration, + 'progress': progress, + }); + } + + /// 设置媒体会话回调 + Future setMediaSessionCallbacks({ + required Function() onPlay, + required Function() onPause, + required Function() onNext, + required Function() onPrevious, + required Function(int) onSeekTo, + required Function() onStop, + }) async { + // 设置方法调用处理器来处理来自原生平台的回调 + _methodChannel.setMethodCallHandler((call) async { + switch (call.method) { + case 'onMediaSessionPlay': + onPlay(); + break; + case 'onMediaSessionPause': + onPause(); + break; + case 'onMediaSessionNext': + onNext(); + break; + case 'onMediaSessionPrevious': + onPrevious(); + break; + case 'onMediaSessionSeekTo': + final position = call.arguments['position'] as int; + onSeekTo(position); + break; + case 'onMediaSessionStop': + onStop(); + break; + } + }); + + // 通知原生平台启用媒体会话回调 + await _methodChannel.invokeMethod('enableMediaSessionCallbacks'); + } + + /// 更新媒体会话元数据 + Future updateMediaSessionMetadata(Map song) async { + await _methodChannel.invokeMethod('updateMediaSessionMetadata', { + 'song': song, + }); + } + + /// 更新媒体会话播放状态 + Future updateMediaSessionPlaybackState(bool isPlaying) async { + await _methodChannel.invokeMethod('updateMediaSessionPlaybackState', { + 'isPlaying': isPlaying, + }); + } } From 560b8dc3f0db65442ca16535094d9a398128010a Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Tue, 24 Jun 2025 23:53:18 +0800 Subject: [PATCH 2/5] =?UTF-8?q?refactor(chat):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E5=BA=9F=E5=BC=83=E7=9A=84chat=E6=A8=A1=E5=9D=97=E5=8F=8A?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E7=BB=84=E4=BB=B6=E5=92=8C=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/modules/chat/bindings/chat_binding.dart | 10 - .../chat/controllers/chat_controller.dart | 992 ------------------ lib/modules/chat/models/message_model.dart | 64 -- .../views/animated_gradient_background.dart | 69 -- lib/modules/chat/views/chat_music_card.dart | 100 -- .../chat/views/chat_navigation_card.dart | 208 ---- lib/modules/chat/views/chat_view.dart | 820 --------------- lib/modules/chat/views/cloud_background.dart | 144 --- lib/modules/chat/views/weather_day_card.dart | 92 -- 9 files changed, 2499 deletions(-) delete mode 100644 lib/modules/chat/bindings/chat_binding.dart delete mode 100644 lib/modules/chat/controllers/chat_controller.dart delete mode 100644 lib/modules/chat/models/message_model.dart delete mode 100644 lib/modules/chat/views/animated_gradient_background.dart delete mode 100644 lib/modules/chat/views/chat_music_card.dart delete mode 100644 lib/modules/chat/views/chat_navigation_card.dart delete mode 100644 lib/modules/chat/views/chat_view.dart delete mode 100644 lib/modules/chat/views/cloud_background.dart delete mode 100644 lib/modules/chat/views/weather_day_card.dart diff --git a/lib/modules/chat/bindings/chat_binding.dart b/lib/modules/chat/bindings/chat_binding.dart deleted file mode 100644 index 6b6ae6683..000000000 --- a/lib/modules/chat/bindings/chat_binding.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:get/get.dart'; -import '../controllers/chat_controller.dart'; - -class ChatBinding extends Bindings { - @override - void dependencies() { - // 注册控制器 - Get.lazyPut(() => ChatController()); - } -} diff --git a/lib/modules/chat/controllers/chat_controller.dart b/lib/modules/chat/controllers/chat_controller.dart deleted file mode 100644 index 64668f38a..000000000 --- a/lib/modules/chat/controllers/chat_controller.dart +++ /dev/null @@ -1,992 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import '../../../data/services/spotify_service.dart'; -import 'package:get/get.dart'; -import 'package:flutter/widgets.dart'; -import 'package:get_storage/get_storage.dart'; -import 'package:haptic_feedback/haptic_feedback.dart'; -import 'package:just_audio/just_audio.dart'; -import 'package:chat_storage/chat_storage.dart'; -import '../../../data/services/ai_service.dart'; -import '../../../data/services/deapsound_ai_service.dart'; -import '../../../data/services/kouzi_ai_service.dart'; -import '../../../data/services/speech_impl/voice_clone_tts_service.dart'; -import '../models/message_model.dart'; -import '../../../core/utils/logger.dart'; -import '../../../data/services/tts_service.dart'; -import '../../../data/providers/agent_provider.dart'; -import '../../../data/models/agent.dart'; -import '../../../data/services/asr_service.dart'; -import '../../../data/services/open_ai_service_adapter.dart'; - -class ChatController extends GetxController { - // 服务 - late AiService _aiService; - late TtsService _ttsService; - final AsrService _asrService = Get.find(); - - late SpotifService _spotifService; - - // 添加ScrollController用于控制列表滚动 - final ScrollController scrollController = ScrollController(); - - StreamSubscription? _locationStream; - String _latLng = ''; - String _city = '深圳市'; - - // Agent 信息 - late final Agent agent; - - // 消息列表 - final RxList messages = [].obs; - - // 输入控制 - final RxString inputText = ''.obs; - - // 状态控制 - final RxBool isLoading = false.obs; - final RxBool isSending = false.obs; - final RxBool isTtsEnabled = true.obs; - final RxBool isVoiceRecognitionActive = false.obs; - - // 分页加载状态 - final RxInt _currentPage = 1.obs; - final RxInt _totalPages = 1.obs; - final RxBool _isLoadingMore = false.obs; - final RxBool _hasMoreHistory = true.obs; - static const int _pageSize = 20; // 每页加载的消息数量 - - // 语音交互模式 - bool _isVoiceInteractionMode = false; - - // 语音识别状态 - final RxString recognizedText = ''.obs; - final RxBool isListening = false.obs; - final RxBool isSpeechDetected = false.obs; - - // 防抖计时器 - Timer? _scrollDebounceTimer; - - // 流式响应控制 - StreamSubscription? _aiResponseSubscription; - StreamSubscription? _recognitionSubscription; - StreamSubscription? _voiceInteractionSubscription; - - // 取消标志 - bool _shouldCancelAiResponse = false; - - // 音频输入源 - bool _audioSourceType = false; - - // 自动停止语音识别计时器 - Timer? _autoStopTimer; - - // 记录最后一次活动时间 - DateTime _lastActivityTime = DateTime.now(); - - // 音乐播放 - final player = AudioPlayer(); - final RxString audiourl = ''.obs; - final RxBool isPlay = false.obs; - - // 按住说话、文字输入切换 - final RxBool isTextInput = false.obs; - final RxBool isHoldSpeak = false.obs; - String finalResult = ''; - - // 按住说话防抖 - bool isDebounce = false; - Timer? _talkDebounceTimer; - Timer? _talkStartDebounceTimer; - - @override - Future onInit() async { - super.onInit(); - Logger.info('ChatController onInit'); - - // 从路由参数获取Agent信息 - final Map args = Get.arguments ?? {}; - final String agentId = - args['agent'] ?? args['agentId'] ?? 'personal_assistant'; - - // 获取Agent - final Agent? foundAgent = AgentProvider.getAgentById(agentId); - if (foundAgent == null) { - Get.back(); - Logger.error('无法找到Agent: $agentId'); - - return; - } - - agent = foundAgent; - - // 根据Agent ID选择不同的AI服务 - switch (agent.id) { - case 'cyber_girlfriend': - // 亲子陪伴 - Get.put(KouziAIService()); - _aiService = Get.find(); - break; - case 'health_advisor': - // 多功能AI聊天 - Get.put(DeapsoundAIService()); - _aiService = Get.find(); - break; - default: - // 默认使用OpenAIServiceAdapter - _aiService = Get.find(); - Logger.info('使用OpenAIServiceAdapter'); - } - Get.put(SpotifService()); - _spotifService = Get.find(); - // 使用克隆音色语音合成 - final GetStorage storage = GetStorage(); - String timbre = storage.read('timbre') ?? '1'; - if (timbre == '1') { - _ttsService = Get.find(); - } else { - Get.put(VoiceCloneTtsService()); - _ttsService = Get.find(); - } - - _ttsService.initialize(); - _asrService.initialize(); - - // 设置语音交互模式 - _isVoiceInteractionMode = args['isVoiceInteraction'] == true; - - // 设置TTS服务 - await _ttsService.setVoice(agent.voice); - - // 添加滚动监听器,用于触发加载更多 - scrollController.addListener(_scrollListener); - - // 加载聊天历史 (加载第一页) - await _loadChatHistory(); - - // 如果没有历史记录,添加欢迎消息 - if (messages.isEmpty && agent.welcomeMessage.isNotEmpty) { - messages.add(Message( - role: 'assistant', - content: agent.welcomeMessage, - timestamp: DateTime.now(), - )); - // 如果启用了TTS,播放欢迎消息 - // if (isTtsEnabled.value) { - // _ttsService.speak(agent.welcomeMessage); - // } - } - - // 如果isVoiceInteraction为true,自动激活语音识别 - if (args['isVoiceInteraction'] == true) { - _ttsService.speakOnce(agent.voiceWelcomeMessage ?? agent.welcomeMessage); - startVoiceRecognition(); - // 自动停止计时器会在startVoiceRecognition中启动 - } - - - player.playerStateStream.listen((state) async { - if (state.processingState == ProcessingState.completed) { - await player.seek(Duration.zero); - player.pause(); - isPlay.value = false; - } - }); - - // 使用微任务在布局完成后自动滚动到底部 - WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollToBottom(); - }); - } - - - @override - void onClose() { - Logger.info('ChatController onClose'); - - // 清理资源 - scrollController.removeListener(_scrollListener); // 移除监听器 - scrollController.dispose(); - _aiResponseSubscription?.cancel(); - _recognitionSubscription?.cancel(); - _voiceInteractionSubscription?.cancel(); - _autoStopTimer?.cancel(); - _scrollDebounceTimer?.cancel(); // 清理防抖计时器 - _talkDebounceTimer?.cancel(); - _talkStartDebounceTimer?.cancel(); - - _locationStream?.cancel(); - - // 停止TTS - _ttsService.stop(); - - if (isVoiceRecognitionActive.value) { - _asrService.stopContinuousRecognition(); - } - - // 保存当前聊天记录 - if (agent.id != null) { - // _historyService.saveHistory(agent.id!, messages.toList()); - } - - player.dispose(); - - super.onClose(); - } - - /// 滚动到列表底部方法,添加防抖功能 - void _scrollToBottom() { - if (scrollController.hasClients && messages.isNotEmpty) { - try { - scrollController.animateTo( - scrollController.position.maxScrollExtent, - duration: Duration(milliseconds: 300), - curve: Curves.easeOut, - ); - } catch (e) { - Logger.error('滚动到底部失败', e); - } - } - } - - /// 中断当前响应 - void _interruptCurrentResponse(String reason) { - Logger.info('${reason}'); - - // 停止TTS - _ttsService.stop(); - - // 取消AI响应流订阅 - if (_aiResponseSubscription != null) { - _shouldCancelAiResponse = true; - _aiResponseSubscription!.cancel(); - _aiResponseSubscription = null; - - // 将AI中间消息转换为正常消息,保留已输出的内容 - final index = - messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant'); - if (index != -1) { - messages[index] = messages[index].copyWith( - isIntermediate: false, - ); - messages.refresh(); - } - - // 重置发送状态 - isSending.value = false; - } - } - - // 发送消息 - Future sendMessage(String text) async { - if (text.trim().isEmpty) return; - - // 中断当前响应 - _interruptCurrentResponse("发送新消息,中断当前响应"); - - // 添加用户消息 - final userMessage = Message( - role: 'user', - content: text, - timestamp: DateTime.now(), - ); - messages.add(userMessage); - _saveMessageToDb(userMessage); - - // 清空输入框 - inputText.value = ''; - - // 添加中间状态的AI消息 - final intermediateMessage = Message.intermediate('assistant'); - messages.add(intermediateMessage); - messages.refresh(); - WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollToBottom(); - }); - - isSending.value = true; - - try { - // 构建消息历史 - final List> messageHistory = _buildMessageHistory(); - - // 重置取消标志 - _shouldCancelAiResponse = false; - - String fullResponse = ''; - - // 获取AI响应流 - final responseStream = _aiService.sendMessageStream( - messages: messageHistory, - systemPrompt: agent.systemPrompt, - ); - Logger.info( - 'liwei----------- 发送用户提问 ${text}', - ); - // 订阅响应流 - _aiResponseSubscription = responseStream.listen( - (chunk) { - if (_shouldCancelAiResponse) return; - Logger.info('chat ${chunk}'); - if (chunk is Map) { - Logger.info( - 'liwei-----------收到卡片消息 ${chunk}', - ); - _updateIntermediateMessage(meta: chunk); - if (chunk['card_spotify'] != null) { - play_spotify(chunk['card_spotify']); - } - if (chunk['card_music'] != null) { - play_music(chunk['card_music']); - } - } else if (chunk is String) { - fullResponse += chunk; - - // 更新中间状态的消息内容 - _updateIntermediateMessage(content: fullResponse); - - // 如果TTS启用,使用流式处理发送到TTS - if (isTtsEnabled.value) { - String str = chunk.replaceAll('*', ''); - _ttsService.speakStream(str); - } - } - }, - onDone: () { - Logger.info( - 'liwei-----------结束当前问题 ${text}', - ); - if (_shouldCancelAiResponse) return; - - // 完成响应,将isIntermediate设为false - _finalizeResponse(fullResponse); - - // 如果TTS启用,处理缓冲区中的剩余文本 - if (isTtsEnabled.value) { - _ttsService.flushStream(); - } - }, - onError: (error) { - Logger.error('AI响应错误', error); - - // 更新中间状态的消息为错误消息 - _handleResponseError(); - }, - ); - } catch (e) { - Logger.error('发送消息失败', e); - - _handleResponseError(); - } - } - - // 构建消息历史 - List> _buildMessageHistory() { - final List> messageHistory = []; - - // 只取最近的10条消息作为上下文 - final recentMessages = messages.where((m) => !m.isIntermediate).toList(); - final startIndex = - recentMessages.length > 10 ? recentMessages.length - 10 : 0; - messageHistory.add({ - 'role': 'user', - 'content': '我当前所在城市是$_city,我的经纬度是$_latLng', - }); - - for (int i = startIndex; i < recentMessages.length; i++) { - final m = recentMessages[i]; - messageHistory.add({ - 'role': m.role, - 'content': m.content, - }); - } - - return messageHistory; - } - - // 更新中间状态的消息 - void _updateIntermediateMessage({Map? meta, String? content}) { - final index = - messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant'); - if (index != -1) { - if (meta != null) { - Map messagesData = messages[index].meta ?? {}; - messages[index] = messages[index].copyWith( - meta: {...messagesData, ...meta}, - ); - } - messages[index] = messages[index].copyWith( - content: content, - ); - messages.refresh(); - _scrollToBottom(); - } - } - -// 播放音乐 - void play_music(Map card) async { - try { - isPlay.value = false; - if (audiourl.value != card['url']) { - audiourl.value = ''; - await player.setUrl(card['url']); - audiourl.value = card['url']; - player.play(); - } else { - player.play(); - } - isPlay.value = true; - } catch (e) { - Logger.error('加载异常: ${e.toString()}'); - } - } - -// 播放音乐 - void pause_music() async { - await player.pause(); - isPlay.value = false; - } - -// 播放音乐 - void play_spotify(Map card) async { - try { - await _spotifService.initialize(); - _spotifService.play(card['url'] as String); - isPlay.value = true; - audiourl.value = card['url'] as String; - } catch (e) { - Logger.error('加载异常: ${e.toString()}'); - } - } - - // 暂停播放音乐 - void pause_spotify() async { - try { - _spotifService.pause(); - isPlay.value = false; - } catch (e) { - Logger.error('加载异常: ${e.toString()}'); - } - } - - // 更新或添加最终响应 - void _finalizeResponse(String content) { - // 查找AI的中间状态消息 - final index = - messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant'); - if (index != -1) { - // 更新中间状态消息为最终状态 - messages[index] = messages[index].copyWith( - content: content, - isIntermediate: false, - timestamp: DateTime.now(), // 更新时间戳为最终时间 - ); - // 保存最终的助手消息到数据库 - _saveMessageToDb(messages[index]); - } else { - // 如果没有中间状态消息,直接添加新消息 (理论上不应发生) - final assistantMsg = Message( - role: 'assistant', - content: content, - timestamp: DateTime.now(), - ); - messages.add(assistantMsg); - _saveMessageToDb(assistantMsg); // 保存到数据库 - } - - isLoading.value = false; - isSending.value = false; - _aiResponseSubscription = null; // 清理订阅 - _shouldCancelAiResponse = false; // 重置取消标志 - - // 响应完成后,重新启动语音识别的自动停止计时器 - if (_isVoiceInteractionMode) { - _startAutoStopTimer(); - } - } - - // 处理响应错误 - void _handleResponseError() { - // 查找AI的中间状态消息 - final index = - messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant'); - if (index != -1) { - messages[index] = messages[index].copyWith( - content: '抱歉,我遇到了一些问题,无法回应您的请求。', - isIntermediate: false, - ); - messages.refresh(); - } - - isSending.value = false; - _aiResponseSubscription = null; - } - - // 切换TTS状态 - void toggleTts() { - isTtsEnabled.toggle(); - - // 如果关闭TTS,停止当前播放 - if (!isTtsEnabled.value) { - _ttsService.stop(); - } - } - - // 按下开始说话 - void talkLongPress() async { - _talkStartDebounceTimer?.cancel(); - _talkDebounceTimer?.cancel(); - isHoldSpeak.value = true; - Haptics.vibrate(HapticsType.success); - - _talkStartDebounceTimer = - Timer(const Duration(milliseconds: 260), () async { - if (isDebounce) return; - - // 中断当前响应 - _interruptCurrentResponse("发送新消息,中断当前响应"); - try { - final recognitionStream = - await _asrService.startContinuousRecognition(_audioSourceType); - await Future.delayed(const Duration(milliseconds: 260)); - isDebounce = true; - recognizedText.value = ''; - finalResult = ''; - - // 监听识别事件 - _recognitionSubscription = recognitionStream.listen( - _handleRecognitionEvent, - onError: _handleRecognitionError, - ); - } catch (e) { - Logger.error('启动语音识别失败', e); - isHoldSpeak.value = false; - } - }); - } - - // 松开停止说话 - void talkLongPressUp() async { - _talkStartDebounceTimer?.cancel(); - _talkDebounceTimer?.cancel(); - isHoldSpeak.value = false; - - _talkDebounceTimer = Timer(const Duration(milliseconds: 300), () async { - if (!isDebounce) return; - try { - await _asrService.stopContinuousRecognition(); - _recognitionSubscription?.cancel(); - _recognitionSubscription = null; - - String sendText = ''; - - final intermediateIndex = - messages.indexWhere((m) => m.isIntermediate && m.role == 'user'); - if (intermediateIndex >= 0) { - sendText = messages[intermediateIndex].content; - messages.removeAt(intermediateIndex); - } - - sendMessage(sendText); - - recognizedText.value = ''; - finalResult = ''; - - isDebounce = false; - } catch (e) { - Logger.error('停止语音识别失败', e); - } - }); - } - - // 开始语音识别 - Future startVoiceRecognition() async { - if (isVoiceRecognitionActive.value) return; - - try { - // 启动连续语音识别 - // 注意:在UI更新后,使用模拟波形动画替代实际麦克风输入的波形数据 - final recognitionStream = - await _asrService.startContinuousRecognition(_audioSourceType); - - isVoiceRecognitionActive.value = true; - isListening.value = true; - recognizedText.value = ''; - - // 启动自动停止计时器 - _startAutoStopTimer(); - - // 监听识别事件 - _recognitionSubscription = recognitionStream.listen( - _handleRecognitionEvent, - onError: _handleRecognitionError, - ); - } catch (e) { - Logger.error('启动语音识别失败', e); - isVoiceRecognitionActive.value = false; - isListening.value = false; - } - } - - // 处理语音识别事件 - void _handleRecognitionEvent(RecognitionEvent event) { - // 更新活动时间 - _updateActivityTime(); - - switch (event.type) { - case RecognitionEventType.intermediateResult: - _handleRecognizing(event.text); - break; - case RecognitionEventType.finalResult: - _handleFinalResult(event.text); - break; - case RecognitionEventType.error: - _handleRecognitionEventError(event.error); - break; - default: - // 不做任何处理 - break; - } - } - - // 更新最后活动时间 - void _updateActivityTime() { - _lastActivityTime = DateTime.now(); - } - - // 启动自动停止计时器 - void _startAutoStopTimer() { - // 取消现有计时器 - _autoStopTimer?.cancel(); - - // 初始化最后活动时间 - _updateActivityTime(); - - // 根据模式设置超时时间 - final int timeoutSeconds = _isVoiceInteractionMode ? 8 : 15; - - // 创建新的计时器,每秒检查一次 - _autoStopTimer = Timer.periodic(Duration(seconds: 1), (timer) { - // 检查是否有活动 - bool isActive = _ttsService.isSpeaking || isSpeechDetected.value; - - // 如果有活动,更新时间并继续等待 - if (isActive) { - _updateActivityTime(); - return; - } - - // 检查空闲时间 - int idleSeconds = DateTime.now().difference(_lastActivityTime).inSeconds; - if (idleSeconds >= timeoutSeconds) { - timer.cancel(); - _stopVoiceRecognitionAutomatically(); - } - }); - } - - // 处理语音识别中的状态 - void _handleRecognizing(String text) { - if (text.isEmpty) return; - - recognizedText.value = text; - - // 设置语音检测状态为true,用于控制波形动画 - if (!isSpeechDetected.value) { - isSpeechDetected.value = true; - } - - // 更新活动时间 - _updateActivityTime(); - - // 用户开始讲话时,立即停止当前TTS和中断AI响应 - if (isSending.value || _aiResponseSubscription != null) { - _interruptCurrentResponse("检测到用户开始讲话,中断当前响应"); - } - - // 查找或创建用户的中间状态消息 - final intermediateIndex = - messages.indexWhere((m) => m.isIntermediate && m.role == 'user'); - - if (intermediateIndex >= 0) { - // 更新已有的中间状态消息 - messages[intermediateIndex] = messages[intermediateIndex].copyWith( - content: isVoiceRecognitionActive.value ? text : finalResult + text, - ); - messages.refresh(); - } else { - // 添加新的中间状态消息 - var intermediateMessage = Message.intermediate('user'); - intermediateMessage = intermediateMessage.copyWith(content: text); - messages.add(intermediateMessage); - messages.refresh(); - } - - WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollToBottom(); - }); - } - - // 处理语音识别最终结果 - void _handleFinalResult(String text) { - if (text.isEmpty) return; - - Logger.info('处理语音识别最终结果: $text'); - - // 更新活动时间 - _updateActivityTime(); - - if (isVoiceRecognitionActive.value) { - // 一直语音说话 - - recognizedText.value = text; - // 查找并移除用户的中间状态消息 - final intermediateIndex = - messages.indexWhere((m) => m.isIntermediate && m.role == 'user'); - if (intermediateIndex >= 0) { - messages.removeAt(intermediateIndex); - } - - // 发送识别到的文本 - sendMessage(text); - // 重置识别文本,但不关闭语音识别框 - recognizedText.value = ''; - // 重置语音检测状态 - isSpeechDetected.value = false; - } else { - // 按住说话 - - finalResult += text; - recognizedText.value = finalResult; - - final intermediateIndex = - messages.indexWhere((m) => m.isIntermediate && m.role == 'user'); - - if (intermediateIndex >= 0) { - // 更新已有的中间状态消息 - messages[intermediateIndex] = messages[intermediateIndex].copyWith( - content: finalResult, - ); - messages.refresh(); - } - } - } - - // 处理语音识别事件错误 - void _handleRecognitionEventError(String? error) { - Logger.error('语音识别错误: ${error ?? "未知错误"}'); - - _cleanupRecognition(); - } - - // 处理语音识别流错误 - void _handleRecognitionError(dynamic error) { - Logger.error('语音识别错误', error); - - _cleanupRecognition(); - } - - // 清理语音识别资源 - void _cleanupRecognition() { - // 查找并移除用户的中间状态消息 - final intermediateIndex = - messages.indexWhere((m) => m.isIntermediate && m.role == 'user'); - if (intermediateIndex >= 0) { - if (messages[intermediateIndex].content.trim().isEmpty) { - messages.removeAt(intermediateIndex); - } else { - // 如果中间状态消息有内容,将其转换为正式消息 - messages[intermediateIndex] = messages[intermediateIndex].copyWith( - isIntermediate: false, - ); - } - messages.refresh(); - } - - // 停止语音识别 - isVoiceRecognitionActive.value = false; - isListening.value = false; - isSpeechDetected.value = false; - _asrService.stopContinuousRecognition(); - } - - // 停止语音识别 - Future stopVoiceRecognition() async { - if (!isVoiceRecognitionActive.value) return; - - try { - await _asrService.stopContinuousRecognition(); - _recognitionSubscription?.cancel(); - _recognitionSubscription = null; - - isVoiceRecognitionActive.value = false; - isListening.value = false; - recognizedText.value = ''; - isSpeechDetected.value = false; - - // 查找并处理用户的中间状态消息 - final intermediateIndex = - messages.indexWhere((m) => m.isIntermediate && m.role == 'user'); - if (intermediateIndex >= 0) { - if (messages[intermediateIndex].content.trim().isEmpty) { - // 如果没有内容则移除 - messages.removeAt(intermediateIndex); - } else { - // 有内容则转为正式消息 - messages[intermediateIndex] = messages[intermediateIndex].copyWith( - isIntermediate: false, - ); - } - messages.refresh(); - } - } catch (e) { - Logger.error('停止语音识别失败', e); - } - } - - // 清空聊天记录 - void clearChat() { - messages.clear(); - _ttsService.stop(); - // 添加欢迎消息 - if (agent.welcomeMessage.isNotEmpty) { - messages.add(Message( - role: 'assistant', - content: agent.welcomeMessage, - timestamp: DateTime.now(), - )); - } - // 删除数据库中的历史记录 - ChatStorage.deleteMessages(sessionId: agent.id).catchError((e) { - Logger.error('清空数据库聊天记录失败: ${e.toString()}'); - }); - } - - // 滚动监听,用于触发加载更多历史记录 - void _scrollListener() { - // 当滚动到顶部并且有更多历史记录且不在加载中时,加载更多 - if (scrollController.position.pixels == - scrollController.position.minScrollExtent && - _hasMoreHistory.value && - !_isLoadingMore.value) { - _loadMoreHistory(); - } - } - - // 加载聊天历史 (首次加载或刷新) - Future _loadChatHistory() async { - isLoading.value = true; - _currentPage.value = 1; // 重置为第一页 - _hasMoreHistory.value = true; // 假设有更多 - - try { - final pagedResult = await ChatStorage.getMessages( - sessionId: agent.id, - page: _currentPage.value, - pageSize: _pageSize, - ); - - _totalPages.value = pagedResult.totalPages; - _hasMoreHistory.value = _currentPage.value < _totalPages.value; - - final historyMessages = pagedResult.messages.map((chatMsg) { - return Message( - role: chatMsg.sender, - content: chatMsg.message, - timestamp: DateTime.parse(chatMsg.timestamp), - meta: chatMsg.metadata != null ? jsonDecode(chatMsg.metadata!) : null, - ); - }).toList(); - - messages.assignAll(historyMessages.reversed); // 最新的在底部,所以要反转 - - // 滚动到底部 (如果是首次加载) - WidgetsBinding.instance.addPostFrameCallback((_) { - if (scrollController.hasClients) { - scrollController.jumpTo(scrollController.position.maxScrollExtent); - } - }); - } catch (e) { - Logger.error('加载聊天历史失败', e); - } finally { - isLoading.value = false; - } - } - - // 加载更多历史记录 - Future _loadMoreHistory() async { - if (_isLoadingMore.value || !_hasMoreHistory.value) return; - - _isLoadingMore.value = true; - _currentPage.value++; - - try { - final pagedResult = await ChatStorage.getMessages( - sessionId: agent.id, - page: _currentPage.value, - pageSize: _pageSize, - ); - - _totalPages.value = pagedResult.totalPages; - _hasMoreHistory.value = _currentPage.value < _totalPages.value; - - final olderMessages = pagedResult.messages.map((chatMsg) { - return Message( - role: chatMsg.sender, - content: chatMsg.message, - timestamp: DateTime.parse(chatMsg.timestamp), - meta: chatMsg.metadata != null ? jsonDecode(chatMsg.metadata!) : null, - ); - }).toList(); - - // 将旧消息插入到列表顶部 - messages.insertAll(0, olderMessages.reversed); - } catch (e) { - Logger.error('加载更多聊天历史失败', e); - _currentPage.value--; // 加载失败,恢复页码 - } finally { - _isLoadingMore.value = false; - } - } - - // 保存单条聊天消息到数据库 - Future _saveMessageToDb(Message message) async { - if (message.isIntermediate) return; // 不保存中间状态的消息 - try { - await ChatStorage.saveMessage( - sessionId: agent.id, - message: message.content, - sender: message.role, - metadata: jsonEncode(message.meta), - ); - } catch (e) { - Logger.error('保存消息到数据库失败', e); - } - } - - // 添加消息,供外部调用 (如用户发送消息、AI回复) - void addMessage(Message message) { - messages.add(message); - _saveMessageToDb(message); // 保存到数据库 - WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollToBottom(); - }); - } - - void _stopVoiceRecognitionAutomatically() { - final int timeoutSeconds = _isVoiceInteractionMode ? 8 : 15; - Logger.info('${timeoutSeconds}秒无活动,自动停止语音识别'); - stopVoiceRecognition(); - } - - /// 检查是否为重复消息 - bool _isDuplicateMessage(Message message) { - return messages.any((m) => - m.role == message.role && - m.content == message.content && - (m.timestamp.millisecondsSinceEpoch - - message.timestamp.millisecondsSinceEpoch) - .abs() < - 5000); - } -} \ No newline at end of file diff --git a/lib/modules/chat/models/message_model.dart b/lib/modules/chat/models/message_model.dart deleted file mode 100644 index bb1b0cce3..000000000 --- a/lib/modules/chat/models/message_model.dart +++ /dev/null @@ -1,64 +0,0 @@ -class Message { - final String role; // 'user' or 'assistant' - final String content; - final Map? meta; - final DateTime timestamp; - final bool isIntermediate; // 中间状态标记,替代isLoading - - Message({ - required this.role, - required this.content, - required this.timestamp, - this.meta, - this.isIntermediate = false, // 默认不是中间状态 - }); - - // 从JSON构造函数 - factory Message.fromJson(Map json) { - return Message( - role: json['role'] as String, - content: json['content'] as String, - meta: json['meta'], - timestamp: DateTime.parse(json['timestamp'] as String), - isIntermediate: json['isIntermediate'] as bool? ?? false, - ); - } - - // 转换为JSON - Map toJson() { - return { - 'role': role, - 'content': content, - 'meta': meta, - 'timestamp': timestamp.toIso8601String(), - 'isIntermediate': isIntermediate, - }; - } - - // 创建一个中间状态的消息 - factory Message.intermediate(String role) { - return Message( - role: role, - content: '', - timestamp: DateTime.now(), - isIntermediate: true, - ); - } - - // 复制并修改 - Message copyWith({ - String? role, - String? content, - Map? meta, - DateTime? timestamp, - bool? isIntermediate, - }) { - return Message( - role: role ?? this.role, - content: content ?? this.content, - meta: meta ?? this.meta, - timestamp: timestamp ?? this.timestamp, - isIntermediate: isIntermediate ?? this.isIntermediate, - ); - } -} diff --git a/lib/modules/chat/views/animated_gradient_background.dart b/lib/modules/chat/views/animated_gradient_background.dart deleted file mode 100644 index 3a96b8bc5..000000000 --- a/lib/modules/chat/views/animated_gradient_background.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:flutter/material.dart'; - -class AnimatedGradientBackground extends StatefulWidget { - final Widget child; - - const AnimatedGradientBackground(this.child, {super.key}); - - @override - State createState() => - _AnimatedGradientBackgroundState(); -} - -class _AnimatedGradientBackgroundState extends State - with SingleTickerProviderStateMixin { - late AnimationController _controller; - late Animation _beginAnimation; - late Animation _endAnimation; - - final Color color1 = const Color(0xFFfbcfdd); - final Color color2 = const Color(0xFFe2e9fb); - - @override - void initState() { - super.initState(); - - _controller = AnimationController( - vsync: this, - duration: const Duration(seconds: 10), - )..repeat(reverse: true); - - // 创建起始点动画 - _beginAnimation = AlignmentTween( - begin: Alignment.topCenter, - end: Alignment.bottomLeft, - ).animate(_controller); - - // 创建结束点动画 - _endAnimation = AlignmentTween( - begin: Alignment.bottomCenter, - end: Alignment.topRight, - ).animate(_controller); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _controller, - builder: (context, child) { - return Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [color1, color2], - begin: _beginAnimation.value!, - end: _endAnimation.value!, - ), - ), - child: child, - ); - }, - child: widget.child, - ); - } -} diff --git a/lib/modules/chat/views/chat_music_card.dart b/lib/modules/chat/views/chat_music_card.dart deleted file mode 100644 index 10a6ce757..000000000 --- a/lib/modules/chat/views/chat_music_card.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; - -import '../controllers/chat_controller.dart'; - -class ChatMusicCard extends GetView { - final int type; - final Map? data; - final bool isDarkMode; - - const ChatMusicCard(this.type, this.data, this.isDarkMode, {super.key}); - - @override - Widget build(BuildContext context) { - // final Map musiccards = data?['musiccards'].first; - // final Map musicaudio = data?['musicaudio']; - - return Row( - children: [ - Container( - width: 60.w, - height: 60.w, - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6.r), - ), - child: Image.network( - data?['image'], - fit: BoxFit.cover, - ), - ), - 10.horizontalSpace, - Expanded( - child: Text( - data?['name'], - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black, - fontSize: 16.sp, - height: 1.5, - ), - ), - ), - 10.horizontalSpace, - Obx( - () => controller.audiourl.value == data?["url"] && - controller.isPlay.value - ? GestureDetector( - onTap: () { - switch (type) { - case 0: - controller.pause_music(); - break; - case 1: - controller.pause_spotify(); - break; - default: - } - }, - child: Stack( - alignment: Alignment.center, - children: [ - Icon( - Icons.pause_rounded, - color: isDarkMode ? Colors.white : Colors.black, - ), - ], - ), - ) - : GestureDetector( - onTap: () { - switch (type) { - case 0: - controller.play_music(data!); - break; - case 1: - controller.play_spotify(data!); - break; - default: - } - }, - child: Container( - width: 38.w, - height: 38.w, - decoration: BoxDecoration( - color: isDarkMode ? Colors.white : Colors.black, - shape: BoxShape.circle, - ), - child: Icon( - Icons.play_arrow_rounded, - color: isDarkMode ? Colors.black : Colors.white, - ), - ), - ), - ), - ], - ); - } -} diff --git a/lib/modules/chat/views/chat_navigation_card.dart b/lib/modules/chat/views/chat_navigation_card.dart deleted file mode 100644 index ee9085dcd..000000000 --- a/lib/modules/chat/views/chat_navigation_card.dart +++ /dev/null @@ -1,208 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:math'; -import 'dart:math' as math; - -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:http/http.dart' as http; - -class ChatNavigationCard extends StatefulWidget { - final String content; - final Map? data; - final bool isDarkMode; - - const ChatNavigationCard(this.data, this.content, this.isDarkMode, - {super.key}); - - @override - State createState() => _ChatNavigationCardState(); -} - -class _ChatNavigationCardState extends State { - final String apiKey = '8bde023b7e5e00f321ea17929f10fbae'; - LatLng startPoint = LatLng(116.397428, 39.90923); // 起点坐标 - LatLng endPoint = LatLng(116.397026, 39.903303); // 终点坐标 - - late Future _mapUrl; - - @override - void initState() { - super.initState(); - _getLocation(); - } - - void _getLocation() { - List startPointList = widget.data?['location'].split(','); - List endPointList = widget.data?['destination'].split(','); - startPoint = LatLng( - double.parse(startPointList[0]), - double.parse(startPointList[1]), - ); - endPoint = LatLng( - double.parse(endPointList[0]), - double.parse(endPointList[1]), - ); - _mapUrl = _getMapUrl(); - } - - Future _getMapUrl() async { - // 1. 获取路径数据 - final route = await _fetchRouteData(); - - // 2. 解析并简化路径 - List simplified = _simplifyPath(route, epsilon: 0.0001); - - // 3. 计算地图参数 - final zoom = _calculateAutoZoom(simplified); - - // 4. 生成静态地图URL - return _buildStaticMapUrl(simplified, zoom); - } - - Future> _fetchRouteData() async { - final url = 'https://restapi.amap.com/v3/direction/driving?' - 'origin=${startPoint.lng},${startPoint.lat}&' - 'destination=${endPoint.lng},${endPoint.lat}&' - 'key=$apiKey'; - - final response = await http.get(Uri.parse(url)); - if (response.statusCode == 200) { - final data = json.decode(response.body); - return _parsePolyline(data['route']['paths'][0]['steps']); - } - throw Exception('Failed to load route'); - } - - List _parsePolyline(List steps) { - final List path = []; - for (final step in steps) { - final points = (step['polyline'] as String).split(';'); - for (final point in points) { - final coords = point.split(',').map(double.parse).toList(); - path.add(LatLng(coords[0], coords[1])); - } - } - return path; - } - - List _simplifyPath(List points, {double epsilon = 0.0001}) { - return douglasPeucker(points, epsilon); - } - - int _calculateAutoZoom(List points, - {int mapWidth = 1024, int mapHeight = 512}) { - if (points.isEmpty) return 15; // 默认缩放级别 - - // 1. 计算坐标范围 - double minLng = double.infinity; - double maxLng = -double.infinity; - double minLat = double.infinity; - double maxLat = -double.infinity; - - for (final p in points) { - final lng = p.lng; - final lat = p.lat; - - minLng = math.min(minLng, lng); - maxLng = math.max(maxLng, lng); - minLat = math.min(minLat, lat); - maxLat = math.max(maxLat, lat); - } - - // 3. 计算缩放级别(基于Leaflet算法改进) - const worldWidth = 256; // 高德瓦片尺寸 - const worldHeight = 256; - const zoomMax = 18; // 高德最大缩放级别 - - double latRad(double lat) { - final sin = math.sin(lat * math.pi / 180); - return math.log((1 + sin) / (1 - sin)) / 2; - } - - final latFraction = (latRad(maxLat) - latRad(minLat)) / math.pi; - final lngDiff = maxLng - minLng; - final lngFraction = ((lngDiff < 0) ? (lngDiff + 360) : lngDiff) / 360; - - final latZoom = - math.log((mapHeight / worldHeight) / latFraction) / math.ln2; - final lngZoom = math.log((mapWidth / worldWidth) / lngFraction) / math.ln2; - - return math.min(math.min(latZoom, lngZoom).floor(), zoomMax); - } - - String _buildStaticMapUrl(List path, int zoom) { - final pathParam = path.map((p) => '${p.lng},${p.lat}').join(';'); - return 'https://restapi.amap.com/v3/staticmap?' - 'zoom=$zoom&' - 'size=1024*512&' - 'paths=10,0x0000ff,1,,:$pathParam&' - 'markers=mid,,A:${startPoint.lng},${startPoint.lat}|' - 'mid,,B:${endPoint.lng},${endPoint.lat}&' - 'key=$apiKey'; - } - - List douglasPeucker(List points, double epsilon) { - if (points.length < 3) return points; - - double maxDistance = 0; - int index = 0; - - for (int i = 1; i < points.length - 1; i++) { - double d = _perpendicularDistance(points[i], points.first, points.last); - if (d > maxDistance) { - index = i; - maxDistance = d; - } - } - - if (maxDistance > epsilon) { - return [ - ...douglasPeucker(points.sublist(0, index + 1), epsilon), - ...douglasPeucker(points.sublist(index), epsilon).skip(1) - ]; - } - return [points.first, points.last]; - } - - double _perpendicularDistance(LatLng point, LatLng start, LatLng end) { - final area = (end.lng - start.lng) * (point.lat - start.lat) - - (end.lat - start.lat) * (point.lng - start.lng); - final length = - sqrt(pow(end.lng - start.lng, 2) + pow(end.lat - start.lat, 2)); - return area.abs() / length; - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - FutureBuilder( - future: _mapUrl, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - return Image.network(snapshot.data); - } - return const CircularProgressIndicator(); - }, - ), - 10.verticalSpace, - Text( - widget.content, - style: TextStyle( - color: widget.isDarkMode ? Colors.white : Colors.black, - fontSize: 16.sp, - height: 1.5, - ), - ), - ], - ); - } -} - -class LatLng { - final double lat; - final double lng; - - LatLng(this.lng, this.lat); // 高德使用经度在前纬度在后的格式 -} diff --git a/lib/modules/chat/views/chat_view.dart b/lib/modules/chat/views/chat_view.dart deleted file mode 100644 index 8675dae88..000000000 --- a/lib/modules/chat/views/chat_view.dart +++ /dev/null @@ -1,820 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'dart:math'; -import '../controllers/chat_controller.dart'; -import '../models/message_model.dart'; -import '../../../core/theme/app_colors.dart'; -import 'animated_gradient_background.dart'; -import 'chat_music_card.dart'; -import 'chat_navigation_card.dart'; -import 'cloud_background.dart'; -import 'weather_day_card.dart'; - -class ChatView extends GetView { - const ChatView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return Obx( - () => Scaffold( - backgroundColor: - isDarkMode ? AppColors.darkBackground : Colors.grey[50], - appBar: controller.isVoiceRecognitionActive.value - ? null - : _buildAppBar(isDarkMode), - body: controller.isVoiceRecognitionActive.value - ? _buildVoiceInputPanel(isDarkMode) - : _buildBody(isDarkMode), - ), - ); - } - - // 构建AppBar - PreferredSizeWidget _buildAppBar(bool isDarkMode) { - return AppBar( - elevation: 1, - backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white, - leading: IconButton( - icon: Icon(Icons.arrow_back_ios, - color: isDarkMode ? Colors.white : Colors.black87, size: 20.r), - onPressed: () => Get.back(), - ), - titleSpacing: 0, - title: Row( - children: [ - // AI头像 - Container( - width: 40.r, - height: 40.r, - decoration: BoxDecoration( - color: AppColors.primary, - borderRadius: BorderRadius.circular(20.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 3, - offset: const Offset(0, 1), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(20.r), - child: controller.agent.avatarUrl.isNotEmpty - ? Image.asset( - controller.agent.avatarUrl, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Icon( - Icons.smart_toy_rounded, - color: Colors.white, - size: 22.r, - ); - }, - ) - : Icon( - Icons.smart_toy_rounded, - color: Colors.white, - size: 22.r, - ), - ), - ), - SizedBox(width: 12.w), - // AI名称和状态 - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '${controller.agent.name}'.tr, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.bold, - color: isDarkMode ? Colors.white : Colors.black87, - ), - ), - Row( - children: [ - Container( - width: 6.r, - height: 6.r, - decoration: const BoxDecoration( - color: Colors.green, - shape: BoxShape.circle, - ), - ), - SizedBox(width: 4.w), - Text( - 'online'.tr, // 在线 - style: TextStyle( - fontSize: 12.sp, - color: Colors.green, - ), - ), - ], - ), - ], - ), - ), - ], - ), - actions: [ - IconButton( - icon: Icon( - Icons.phone, - color: isDarkMode ? Colors.white : Colors.black87, - ), - onPressed: controller.startVoiceRecognition, - ), - // TTS开关 - Obx(() => IconButton( - icon: Icon( - controller.isTtsEnabled.value - ? Icons.volume_up - : Icons.volume_off, - color: controller.isTtsEnabled.value - ? AppColors.primary - : isDarkMode - ? Colors.grey[400] - : Colors.grey, - ), - onPressed: controller.toggleTts, - )), - // 更多选项 - Container( - width: 34.r, - height: 34.r, - margin: EdgeInsets.only(right: 8.w), - decoration: BoxDecoration( - color: - isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], - shape: BoxShape.circle, - boxShadow: [ - if (!isDarkMode) - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 2, - spreadRadius: 0.5, - ), - ], - ), - child: PopupMenuButton( - icon: Icon(Icons.more_vert, - color: isDarkMode ? Colors.white : Colors.black87, size: 20.r), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.r), - ), - color: isDarkMode ? AppColors.cardBackground : Colors.white, - itemBuilder: (context) => [ - PopupMenuItem( - value: 'clear', - child: Row( - children: [ - const Icon(Icons.delete_outline, color: Colors.red), - SizedBox(width: 8.w), - Text( - 'clearChatHistory'.tr, // 清空聊天记录 - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black87, - ), - ), - ], - ), - onTap: () { - // 延迟执行,避免在弹出菜单关闭前执行 - Future.delayed( - const Duration(milliseconds: 100), - () => controller.clearChat(), - ); - }, - ), - ], - ), - ), - ], - ); - } - - // 构建主体内容 - Widget _buildBody(bool isDarkMode) { - return Column( - children: [ - // 聊天内容区域 - Expanded( - child: Obx(() { - if (controller.messages.isEmpty) { - return _buildEmptyState(isDarkMode); - } else { - return _buildChatList(isDarkMode); - } - }), - ), - - // 底部输入区域 - _buildTextInputPanel(isDarkMode), - ], - ); - } - - // 构建空状态 - Widget _buildEmptyState(bool isDarkMode) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 80.w, - height: 80.w, - decoration: BoxDecoration( - color: - isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], - shape: BoxShape.circle, - ), - child: Icon( - Icons.chat_bubble_outline, - size: 40.w, - color: - isDarkMode ? Colors.white.withOpacity(0.5) : Colors.grey[400], - ), - ), - SizedBox(height: 24.h), - Text( - '你好!我是${controller.agent.name}助手', - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.bold, - color: isDarkMode ? Colors.white : Colors.black87, - ), - ), - SizedBox(height: 12.h), - Padding( - padding: EdgeInsets.symmetric(horizontal: 40.w), - child: Text( - '我能帮你回答问题、实时语音翻译、推荐音乐和播客等。有什么我可以帮你的吗?', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - height: 1.5, - ), - ), - ), - ], - ), - ); - } - - // 构建聊天列表 - Widget _buildChatList(bool isDarkMode) { - return ListView.builder( - controller: controller.scrollController, - padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h), - itemCount: controller.messages.length, - reverse: false, - physics: const AlwaysScrollableScrollPhysics(), - cacheExtent: 1000, // 增加缓存范围,减少重建 - itemBuilder: (context, index) { - final message = controller.messages[index]; - return _buildMessageItem(message, isDarkMode); - }, - ); - } - - // 构建消息项 - Widget _buildMessageItem(Message message, bool isDarkMode) { - final isUser = message.role == 'user'; - - return Padding( - padding: EdgeInsets.only(bottom: 16.h), - child: Row( - mainAxisAlignment: - isUser ? MainAxisAlignment.end : MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isUser) _buildAvatar(false, isDarkMode), - SizedBox(width: 8.w), - Flexible( - child: Column( - crossAxisAlignment: - isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start, - children: [ - // 消息气泡 - Container( - padding: EdgeInsets.all(12.r), - decoration: BoxDecoration( - color: isUser - ? AppColors.primary - : isDarkMode - ? AppColors.cardBackground - : Colors.grey[200], - borderRadius: BorderRadius.circular(16.r), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 消息内容 - _buildMessageContent(message, isDarkMode, isUser), - - // 加载中动画 - if (message.isIntermediate) - Padding( - padding: EdgeInsets.only(top: 8.h), - child: Row( - mainAxisSize: MainAxisSize.min, - children: List.generate( - 3, - (index) => _buildLoadingDot(index, isDarkMode), - ), - ), - ), - ], - ), - ), - - // 时间戳 - Padding( - padding: EdgeInsets.only(top: 4.h, left: 4.w, right: 4.w), - child: Text( - _formatTime(message.timestamp), - style: TextStyle( - fontSize: 10.sp, - color: isDarkMode ? Colors.grey[500] : Colors.grey[500], - ), - ), - ), - ], - ), - ), - SizedBox(width: 8.w), - if (isUser) _buildAvatar(true, isDarkMode), - ], - ), - ); - } - - Widget _buildMessageContent(Message message, bool isDarkMode, bool isUser) { - Widget box = const SizedBox(); - if (message.content.isNotEmpty) { - box = SelectableText( - message.content.tr, - style: TextStyle( - color: isUser - ? Colors.white - : isDarkMode - ? Colors.white - : Colors.black, - fontSize: 16.sp, - height: 1.5, - ), - ); - } - if (message.meta != null) { - if (message.meta?['card_weather'] != null) { - box = WeatherDayCard(message.meta?['card_weather'], isDarkMode); - } - if (message.meta?['card_music'] != null) { - box = ChatMusicCard(0, message.meta?['card_music'], isDarkMode); - } - if (message.meta?['card_spotify'] != null) { - box = ChatMusicCard(1, message.meta?['card_spotify'], isDarkMode); - } - if (message.meta?['card_navigation'] != null) { - box = ChatNavigationCard( - message.meta?['card_navigation'], message.content, isDarkMode); - } - } - return box; - } - - // 构建文本输入面板 - Widget _buildTextInputPanel(bool isDarkMode) { - final textController = - TextEditingController(text: controller.inputText.value); - - return Container( - height: 48.w, - margin: EdgeInsets.only(left: 16.w, right: 16.w, top: 10.w, bottom: 20.w), - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - color: controller.isHoldSpeak.value - ? AppColors.primary - : isDarkMode - ? AppColors.cardBackground - : Colors.white, - borderRadius: BorderRadius.circular(12.w), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 3, - offset: const Offset(0, -1), - ), - ], - ), - child: Row( - children: [ - if (!controller.isHoldSpeak.value) 6.horizontalSpace, - if (!controller.isHoldSpeak.value) - Padding( - padding: EdgeInsets.all(6.w), - child: Icon( - Icons.camera_alt_outlined, - size: 28.w, - ), - ), - Expanded( - child: Obx( - () => controller.isTextInput.value - ? Center( - child: TextField( - controller: textController, - style: TextStyle( - fontSize: 15.sp, - color: isDarkMode ? Colors.white : Colors.black87, - ), - textInputAction: TextInputAction.send, - decoration: InputDecoration( - hintText: 'inputMessage'.tr, // 输入消息... - hintStyle: TextStyle(color: Colors.grey[500]), - filled: false, - border: InputBorder.none, - focusedBorder: InputBorder.none, - contentPadding: EdgeInsets.only(left: 6.w), - ), - onSubmitted: (value) { - if (value.trim().isNotEmpty) { - controller.sendMessage(value); - textController.clear(); - controller.inputText.value = ''; // 清空输入框 - } - }, - ), - ) - : Listener( - onPointerDown: (event) { - controller.talkLongPress(); - }, - onPointerUp: (event) { - controller.talkLongPressUp(); - }, - child: Container( - padding: EdgeInsets.only( - left: controller.isHoldSpeak.value ? 0 : 40.w), - color: Colors.transparent, - child: controller.isHoldSpeak.value - ? Row( - mainAxisAlignment: MainAxisAlignment.center, - children: List.generate(40, (int index) { - return Container( - width: 3.w, - height: 10.w, - margin: - EdgeInsets.symmetric(horizontal: 2.w), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(3.r), - ), - ); - }), - ) - : Center( - child: Text( - '按住说话', - style: TextStyle( - fontSize: 16.sp, - color: isDarkMode - ? Colors.white - : Colors.black87, - ), - ), - ), - ), - ), - ), - ), - if (!controller.isHoldSpeak.value) - GestureDetector( - onTap: () { - controller.isTextInput.value = !controller.isTextInput.value; - }, - child: Container( - width: 28.w, - height: 28.w, - margin: EdgeInsets.all(6.w), - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(28.r), - border: Border.all( - color: isDarkMode ? Colors.white : Colors.black87, - width: 2, - ), - ), - child: Obx( - () => Icon( - controller.isTextInput.value ? Icons.mic : Icons.keyboard, - size: 20.w, - ), - ), - ), - ), - if (!controller.isHoldSpeak.value) - Container( - width: 28.w, - height: 28.w, - margin: EdgeInsets.all(6.w), - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(28.r), - border: Border.all( - color: isDarkMode ? Colors.white : Colors.black87, - width: 2, - ), - ), - child: Icon( - Icons.add, - size: 20.w, - ), - ), - if (!controller.isHoldSpeak.value) 6.horizontalSpace, - ], - ), - ); - } - - // 构建语音输入面板 - Widget _buildVoiceInputPanel(bool isDarkMode) { - return AnimatedGradientBackground( - SafeArea( - child: Column( - children: [ - Row( - children: [], - ), - Expanded( - child: Center( - child: Container( - width: 200.w, - height: 200.w, - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Colors.blue.withOpacity(0.4), - Colors.purple.withOpacity(0.4) - ], - begin: Alignment.centerLeft, - end: Alignment.centerRight, - ), - shape: BoxShape.circle, - ), - child: CircularFlowAnimation(), - ), - ), - ), - SizedBox( - height: 80.h, - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 20.w), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // 语音录制按钮 - Container( - width: 60.r, - height: 60.r, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withOpacity(0.1) - : Colors.white, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 8, - spreadRadius: 1, - ), - ], - ), - child: Center( - child: Obx(() => Icon( - controller.isListening.value - ? Icons.mic - : Icons.mic_none, - color: controller.isListening.value - ? AppColors.primary - : Colors.grey.shade400, - size: 28.r, - )), - ), - ), - - // 中间音频波形和状态文本 - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // 音频波形区域 - Container( - height: 50.h, - child: Obx(() { - final isSpeaking = - controller.isSpeechDetected.value; - return Center( - child: Container( - width: 220.w, - height: 30.h, - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: List.generate( - 23, - (index) { - // 随机高度模拟波形 - double height = isSpeaking - ? (Random().nextDouble() * 20.0 + - 5.0) - .h - : 2.h; - - return SizedBox( - width: 6.w, - child: Center( - child: AnimatedContainer( - duration: const Duration( - milliseconds: 500), - width: 2.5.w, - height: height, - margin: EdgeInsets.symmetric( - horizontal: 1.5.w), - decoration: BoxDecoration( - color: isSpeaking - ? AppColors.primary - : isDarkMode - ? Colors.grey.shade600 - : Colors.grey.shade400, - borderRadius: - BorderRadius.circular(2.r), - ), - ), - ), - ); - }, - ), - ), - ), - ); - }), - ), - - // 语音状态文本 - Obx(() => Text( - controller.isSpeechDetected.value - ? 'imListening'.tr // 正在聆听... - : 'canStartTalking'.tr, // 你可以开始说话 - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode - ? Colors.grey[400] - : Colors.grey[700], - fontWeight: FontWeight.w500, - ), - )), - ], - ), - ), - - // 关闭按钮 - Container( - width: 60.r, - height: 60.r, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withOpacity(0.1) - : Colors.white, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 8, - spreadRadius: 1, - ), - ], - ), - child: Center( - child: IconButton( - icon: Icon( - Icons.close, - color: isDarkMode - ? Colors.white - : Colors.grey.shade600, - size: 28.r, - ), - padding: EdgeInsets.zero, - onPressed: controller.stopVoiceRecognition, - ), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - } - - // 构建头像 - Widget _buildAvatar(bool isUser, bool isDarkMode) { - return Container( - width: 36.r, - height: 36.r, - decoration: BoxDecoration( - color: isUser - ? (isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[200]) - : AppColors.primary, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 2, - offset: const Offset(0, 1), - ), - ], - ), - child: isUser - ? Center( - child: Icon( - Icons.person, - color: isDarkMode ? Colors.white : Colors.grey[700], - size: 20.r, - ), - ) - : ClipRRect( - borderRadius: BorderRadius.circular(18.r), - child: controller.agent.avatarUrl.isNotEmpty - ? Image.asset( - controller.agent.avatarUrl, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Icon( - Icons.smart_toy_rounded, - color: Colors.white, - size: 20.r, - ); - }, - ) - : Icon( - Icons.smart_toy_rounded, - color: Colors.white, - size: 20.r, - ), - ), - ); - } - - // 构建加载动画点 - Widget _buildLoadingDot(int index, bool isDarkMode) { - return AnimatedBuilder( - animation: const AlwaysStoppedAnimation(0), - builder: (context, child) { - return Container( - margin: EdgeInsets.symmetric(horizontal: 2.w), - width: 8.r, - height: 8.r, - decoration: BoxDecoration( - color: - isDarkMode ? Colors.white.withOpacity(0.5) : Colors.grey[400], - shape: BoxShape.circle, - ), - ); - }, - ); - } - - // 格式化时间 - String _formatTime(DateTime time) { - final now = DateTime.now(); - final today = DateTime(now.year, now.month, now.day); - //final yesterday = today.subtract(const Duration(days: 1)); - final messageDate = DateTime(time.year, time.month, time.day); - - String timeStr = - '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}'; - - if (messageDate == today) { - return timeStr; // 只显示时间,如 "14:30" - } - // else if (messageDate == yesterday) { - // return 'Yesterday $timeStr'; // 如 "Yesterday 14:30" - // } - else { - // 国际通用日期格式 "MM-dd HH:mm" - return '${time.month.toString().padLeft(2, '0')}-${time.day.toString().padLeft(2, '0')} $timeStr'; - } - } -} diff --git a/lib/modules/chat/views/cloud_background.dart b/lib/modules/chat/views/cloud_background.dart deleted file mode 100644 index 995e6a168..000000000 --- a/lib/modules/chat/views/cloud_background.dart +++ /dev/null @@ -1,144 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; - -class CircularFlowAnimation extends StatefulWidget { - const CircularFlowAnimation({super.key}); - - @override - State createState() => _CircularFlowAnimationState(); -} - -class _CircularFlowAnimationState extends State - with SingleTickerProviderStateMixin { - late AnimationController _controller; - - @override - void initState() { - super.initState(); - _controller = AnimationController( - vsync: this, - duration: const Duration(seconds: 20), - )..repeat(); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return CustomPaint( - painter: CircularFlowPainter(_controller), - size: Size.infinite, - ); - } -} - -class CircularFlowPainter extends CustomPainter { - final Animation animation; - - CircularFlowPainter(this.animation) : super(repaint: animation); - - @override - void paint(Canvas canvas, Size size) { - final center = Offset(size.width / 2, size.height / 2); - const baseRadius = 150.0; - final time = animation.value * 2 * pi; - - // 绘制圆形路径 - _drawCircularPath(canvas, center, baseRadius); - - // 绘制三个围绕圆形运动的渐变云朵 - _drawOrbitingCloud( - canvas: canvas, - center: center, - time: time, - colors: [Colors.blue, Colors.purple], - orbitRadius: baseRadius, - speed: 0.5, - direction: 1, - ); - - _drawOrbitingCloud( - canvas: canvas, - center: center, - time: time, - colors: [Colors.pink, Colors.orange], - orbitRadius: baseRadius * 0.8, - speed: 1.0, - direction: -1, - ); - - _drawOrbitingCloud( - canvas: canvas, - center: center, - time: time, - colors: [Colors.green, Colors.yellow], - orbitRadius: baseRadius * 1.2, - speed: 0.3, - direction: 1, - ); - - _drawOrbitingCloud( - canvas: canvas, - center: center, - time: time, - colors: [Colors.red, Colors.deepOrangeAccent], - orbitRadius: baseRadius * 0.5, - speed: 0.8, - direction: -1, - ); - } - - void _drawCircularPath(Canvas canvas, Offset center, double radius) { - final paint = Paint() - ..color = Colors.red - ..style = PaintingStyle.stroke - ..strokeWidth = 2 - ..maskFilter = MaskFilter.blur(BlurStyle.normal, 4); - - canvas.drawCircle(center, radius, paint); - } - - void _drawOrbitingCloud({ - required Canvas canvas, - required Offset center, - required double time, - required List colors, - required double orbitRadius, - required double speed, - required int direction, - }) { - final angle = time * speed * direction; - final position = Offset( - center.dx + orbitRadius * cos(angle), - center.dy + orbitRadius * sin(angle), - ); - - final gradient = RadialGradient( - colors: colors, - stops: const [0.3, 1.0], - radius: 0.6, - focal: Alignment( - 0.1 + cos(angle * 2) * 0.3, - 0.1 + sin(angle * 2) * 0.3, - ), - ); - - final paint = Paint() - ..shader = gradient.createShader(Rect.fromCircle( - center: position, - radius: 100 + 30 * sin(time * 3), - )) - ..maskFilter = MaskFilter.blur(BlurStyle.normal, 40) - ..blendMode = BlendMode.plus; - - canvas.drawCircle(position, 80 + 20 * cos(time * 2), paint); - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => true; -} diff --git a/lib/modules/chat/views/weather_day_card.dart b/lib/modules/chat/views/weather_day_card.dart deleted file mode 100644 index afb14dd70..000000000 --- a/lib/modules/chat/views/weather_day_card.dart +++ /dev/null @@ -1,92 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; - -class WeatherDayCard extends StatelessWidget { - final Map? data; - final bool isDarkMode; - - const WeatherDayCard(this.data, this.isDarkMode, {super.key}); - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - data?['city'], - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black, - fontSize: 10.sp, - ), - ), - Text( - '${data?['temperature']}°', - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black, - fontSize: 30.sp, - ), - ), - ], - ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Icon( - Icons.sunny, - color: isDarkMode ? Colors.white : Colors.amber[600]!, - size: 18.w, - ), - Text( - '${data?['weatherCondition']},${data?['windDirection']}风', - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black, - fontSize: 14.sp, - ), - ), - Row( - children: [ - Text( - '湿\n度', - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black, - fontSize: 10.sp, - height: 1, - ), - ), - 5.horizontalSpace, - Text( - '${data?['humidity']}', - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black, - fontSize: 20.sp, - ), - ), - 10.horizontalSpace, - Text( - '风\n力', - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black, - fontSize: 10.sp, - height: 1, - ), - ), - 5.horizontalSpace, - Text( - '${data?['windForce']}', - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black, - fontSize: 20.sp, - ), - ), - ], - ), - ], - ), - ], - ); - } -} From 9937c8489fc1461e15dda9e0dbef8713fe71c847 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Wed, 25 Jun 2025 16:02:16 +0800 Subject: [PATCH 3/5] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E8=81=8A?= =?UTF-8?q?=E5=A4=A9=E7=9B=B8=E5=85=B3=E4=BB=A3=E7=A0=81=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E9=9F=B3=E4=B9=90=E6=92=AD=E6=94=BE=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(pairing): 修复配对页面跳过绑定按钮逻辑 style: 统一pubspec.yaml中的引号格式 perf(music): 优化音乐播放列表的响应式布局 chore: 更新应用版本号至1.0.0+6 --- lib/core/bindings/initial_binding.dart | 7 +- lib/core/widgets/message_bubble.dart | 94 ---- lib/data/services/chat_history_service.dart | 135 ----- lib/data/services/version_update_service.dart | 8 +- .../android_voice_interaction_service.dart | 343 ------------- .../ios_voice_interaction_service.dart | 466 ------------------ .../music/views/music_playlist_view.dart | 319 ++++++------ .../controllers/pairing_controller.dart | 25 +- lib/modules/pairing/views/pairing_view.dart | 21 + lib/routes/app_pages.dart | 28 +- pubspec.yaml | 10 +- 11 files changed, 212 insertions(+), 1244 deletions(-) delete mode 100644 lib/core/widgets/message_bubble.dart delete mode 100644 lib/data/services/chat_history_service.dart delete mode 100644 lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart delete mode 100644 lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index c372b985a..c3ee74f7b 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -3,7 +3,6 @@ import 'package:deep_voice/data/services/music_manager.dart'; import 'package:deep_voice/data/services/navigation_manager.dart'; import 'package:get/get.dart'; import '../../data/services/speech_factory.dart'; -import '../../data/services/chat_history_service.dart'; import '../../data/services/volcano_translation_service.dart'; import '../../data/services/language_manager.dart'; import '../../core/utils/logger.dart'; @@ -26,9 +25,6 @@ class InitialBinding extends Bindings { Get.lazyPut(() => SpeechFactory(), fenix: true); Get.find().initialize(initialType: SpeechServiceType.azure); - // 历史记录服务 - Get.lazyPut(() => ChatHistoryService(), fenix: true); - // 火山翻译服务 Get.lazyPut(() => VolcanoTranslationService(), fenix: true); @@ -48,7 +44,8 @@ class InitialBinding extends Bindings { Get.lazyPut(() => AccountService(), fenix: true); // 版本更新服务 - Get.lazyPut(() => VersionUpdateService(), fenix: true); + Get.lazyPut(() => VersionUpdateService(), + fenix: true); // 杰理蓝牙服务 diff --git a/lib/core/widgets/message_bubble.dart b/lib/core/widgets/message_bubble.dart deleted file mode 100644 index 0ddbd836e..000000000 --- a/lib/core/widgets/message_bubble.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import '../../modules/chat/models/message_model.dart'; -import '../theme/app_colors.dart'; - -class MessageBubble extends StatelessWidget { - final Message message; - final bool isUser; - final String? userAvatar; - final String? agentAvatar; - - const MessageBubble({ - Key? key, - required this.message, - required this.isUser, - this.userAvatar, - this.agentAvatar, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.only(bottom: 16.h), - child: Row( - mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Flexible( - child: Container( - padding: EdgeInsets.all(12.r), - decoration: BoxDecoration( - color: isUser ? AppColors.primary : Colors.grey[200], - borderRadius: BorderRadius.circular(16.r), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 消息内容 - if (message.content.isNotEmpty) - SelectableText( - message.content, - style: TextStyle( - color: isUser ? Colors.white : Colors.black, - fontSize: 16.sp, - height: 1.5, - letterSpacing: 0.2, - ), - textAlign: TextAlign.left, - maxLines: null, - enableInteractiveSelection: true, - textWidthBasis: TextWidthBasis.longestLine, - ), - - // 加载指示器 - if (message.isIntermediate) - Padding( - padding: EdgeInsets.only(top: message.content.isNotEmpty ? 8.h : 0), - child: _buildLoadingIndicator(), - ), - ], - ), - ), - ), - ], - ), - ); - } - - Widget _buildLoadingIndicator() { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - width: 12.w, - height: 12.h, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - isUser ? Colors.white : AppColors.primary, - ), - ), - ), - SizedBox(width: 8.w), - Text( - '思考中...', - style: TextStyle( - color: isUser ? Colors.white : Colors.black, - fontSize: 12.sp, - ), - ), - ], - ); - } -} \ No newline at end of file diff --git a/lib/data/services/chat_history_service.dart b/lib/data/services/chat_history_service.dart deleted file mode 100644 index e1c017902..000000000 --- a/lib/data/services/chat_history_service.dart +++ /dev/null @@ -1,135 +0,0 @@ -import 'dart:convert'; -import 'package:get/get.dart'; -import '../../modules/chat/models/message_model.dart'; -import '../../core/utils/logger.dart'; -import 'package:chat_storage/chat_storage.dart'; - -/// 聊天记录管理服务,负责保存和加载聊天记录 -class ChatHistoryService extends GetxService { - // 最大历史记录数量 - static const int _maxHistoryCount = 200; // 每个对话最多保存的消息数 - - @override - void onInit() { - super.onInit(); - Logger.info('ChatHistoryService 已初始化'); - } - - /// 保存聊天记录 - /// - /// [agentId] 代理ID - /// [messages] 消息列表 - Future saveHistory(String agentId, List messages) async { - try { - if (messages.isEmpty) return; - - // 获取最新的消息 - final latestMessage = messages.last; - - // 如果是加载中的消息则跳过 - if (latestMessage.isIntermediate) return; - - // 直接插入最新的消息到数据库 - await ChatStorage.saveMessage( - sessionId: agentId, - message: latestMessage.content, - sender: latestMessage.role, - ); - - // 检查并清理过多的历史记录 - await _cleanupOldMessages(agentId); - } catch (e) { - Logger.error('保存聊天记录失败', e.toString()); - } - } - - /// 清理旧消息,保持每个会话的消息数量不超过最大限制 - Future _cleanupOldMessages(String agentId) async { - try { - final pagedResult = await ChatStorage.getMessages( - sessionId: agentId, - page: 1, - pageSize: 1, // 只获取总数信息 - ); - - // 如果消息总数超过最大限制,删除最早的消息 - if (pagedResult.totalCount > _maxHistoryCount) { - final messagesToDelete = pagedResult.totalCount - _maxHistoryCount; - // 获取最早的N条消息ID - final oldestMessages = await ChatStorage.getMessages( - sessionId: agentId, - page: 1, - pageSize: messagesToDelete, - ); - - // 构建要删除的消息ID列表 - final List messageIds = oldestMessages.messages - .where((m) => m.id != null) - .map((m) => m.id!) - .toList(); - - // 删除这些消息 - if (messageIds.isNotEmpty) { - await ChatStorage.deleteMessages(messageIds: messageIds); - } - } - } catch (e) { - Logger.error('清理旧消息失败', e); - } - } - - /// 加载聊天记录 - /// - /// [agentId] 代理ID - /// 返回消息列表,如果没有找到或解析错误则返回空列表 - Future> loadHistory(String agentId) async { - try { - final pagedResult = await ChatStorage.getMessages( - sessionId: agentId, - page: 1, - pageSize: _maxHistoryCount, - ); - - if (pagedResult.messages.isEmpty) { - return []; - } - - // 将ChatStorage.ChatMessage转换为Message - final messages = pagedResult.messages.map((chatMessage) { - return Message( - role: chatMessage.sender, - content: chatMessage.message, - timestamp: DateTime.parse( - chatMessage.timestamp), // 直接使用chatMessage的timestamp - // isLoading 默认false - // isLoading: metadata['isLoading'] ?? false, - ); - }).toList(); - - return messages; - } catch (e) { - Logger.error('加载聊天记录失败', e); - return []; - } - } - - /// 清除特定代理的聊天记录 - Future clearHistory(String agentId) async { - try { - await ChatStorage.deleteMessages(sessionId: agentId); - Logger.info('已清除聊天记录,代理: $agentId'); - } catch (e) { - Logger.error('清除聊天记录失败', e); - } - } - - /// 清除所有聊天记录 - Future clearAllHistory() async { - try { - await ChatStorage.clearDatabase(); - Logger.info('已清除所有聊天记录'); - } catch (e) { - Logger.error('清除所有聊天记录失败', e); - } - } -} diff --git a/lib/data/services/version_update_service.dart b/lib/data/services/version_update_service.dart index f0bf31f05..a71930c80 100644 --- a/lib/data/services/version_update_service.dart +++ b/lib/data/services/version_update_service.dart @@ -65,7 +65,7 @@ class VersionUpdateService extends GetxService { return const PlatformInfo( platform: 'android', storeType: AppStoreType.official, - channel: '官方内测', + channel: 'officialTesting', ); // switch (brand) { @@ -178,17 +178,17 @@ class VersionUpdateService extends GetxService { // 拆分版本号与构建号 final currentParts = current.split('+'); final latestParts = latest.split('+'); - + // 解析版本号 (major.minor.patch) final currentVersions = currentParts[0].split('.').map(int.parse).toList(); final latestVersions = latestParts[0].split('.').map(int.parse).toList(); - + // 逐级比较版本号 for (var i = 0; i < currentVersions.length; i++) { if (latestVersions[i] > currentVersions[i]) return true; if (latestVersions[i] < currentVersions[i]) return false; } - + // 版本号相同则比较构建号 if (latestParts.length > 1 && currentParts.length > 1) { return int.parse(latestParts[1]) > int.parse(currentParts[1]); diff --git a/lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart b/lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart deleted file mode 100644 index 5dd9f96e7..000000000 --- a/lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart +++ /dev/null @@ -1,343 +0,0 @@ -import 'dart:async'; -import 'package:flutter/services.dart'; -import 'package:get/get.dart'; -import '../../../core/utils/logger.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import '../../../modules/chat/models/message_model.dart'; -import '../../models/events/voice_interaction_event.dart'; -import '../chat_history_service.dart'; - -/// Android语音交互服务 -/// -/// 该服务提供了与Android端的VoiceInteractionService.kt通信的接口, -/// 用于管理后台语音交互服务的生命周期和接收语音交互事件 -class AndroidVoiceInteractionService extends GetxService { - static AndroidVoiceInteractionService get to => Get.find(); - // 方法通道和事件通道 - static const MethodChannel _channel = MethodChannel('com.deep_voice.voice_interaction'); - static const EventChannel _eventChannel = EventChannel('com.deep_voice.voice_interaction_events'); - - // 服务状态 - final _isServiceRunning = false.obs; - bool get isServiceRunning => _isServiceRunning.value; - - // 事件流控制器 - StreamController? _eventStreamController; - Stream? _eventStream; - @override - Stream get eventStream => _eventStream ?? Stream.empty(); - - // 事件通道状态 - StreamSubscription? _eventSubscription; - - // 配置信息 - late String _azureSpeechKey; - late String _azureSpeechRegion; - late String _volcanoAiApiKey; - - // 初始化状态标志 - static bool _isInitialized = false; - - // 当前会话历史 - final List> _messageHistory = []; - - // 服务ID - static const String _serviceId = 'android_assistant'; - - /// 构造函数 - AndroidVoiceInteractionService() { - - } - - /// 从环境变量加载配置 - void _loadConfig() { - _azureSpeechKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; - _azureSpeechRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? ''; - _volcanoAiApiKey = dotenv.env['VOLCANO_AI_API_KEY'] ?? ''; - - if (_azureSpeechKey.isEmpty || _azureSpeechRegion.isEmpty) { - Logger.warning('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION'); - } - - if (_volcanoAiApiKey.isEmpty) { - Logger.warning('未找到火山 AI API 密钥。请在 .env 文件中设置 VOLCANO_AI_API_KEY'); - } - } - - /// 设置事件通道 - void _setupEventChannel() { - _eventSubscription = _eventChannel - .receiveBroadcastStream() - .listen((event) { - if (event is Map) { - final String eventType = event['type'] as String? ?? ''; - - // 直接处理事件,不再等待channelReady - _handleVoiceInteractionEvent(event); - } - }, onError: (error) { - Logger.error('语音交互事件通道错误: ${error.toString()}'); - }); - } - - /// 创建事件流 - void _createEventStream() { - _eventStreamController = StreamController.broadcast(); - _eventStream = _eventStreamController?.stream; - } - - /// 初始化服务 - @override - Future initialize() async { - try { - if (_isInitialized) { - return this; - } - _isInitialized = true; - - _loadConfig(); - _setupEventChannel(); - _createEventStream(); - // 检查服务是否正在运行 - await _checkServiceStatus(); - - await _startService(); - - Logger.info('语音交互服务初始化完成'); - return this; - } catch (e) { - Logger.error('语音交互服务初始化失败: ${e.toString()}'); - return this; - } - } - - @override - void onWakeup() { - Logger.info('语音交互服务唤醒'); - } - - /// 检查服务状态 - Future _checkServiceStatus() async { - try { - final bool isRunning = await _channel.invokeMethod('isVoiceInteractionServiceRunning') ?? false; - _isServiceRunning.value = isRunning; - Logger.info('语音交互服务状态: ${isRunning ? "运行中" : "未运行"}'); - } catch (e) { - Logger.error('检查语音交互服务状态失败: ${e.toString()}'); - _isServiceRunning.value = false; - } - } - - /// 启动语音交互服务 - Future _startService() async { - if (_isServiceRunning.value) { - Logger.info('语音交互服务已经在运行'); - return true; - } - - try { - final bool result = await _channel.invokeMethod('startVoiceInteractionService', { - 'azure_speech_key': _azureSpeechKey, - 'azure_speech_region': _azureSpeechRegion, - 'volcano_ai_api_key': _volcanoAiApiKey, - }) ?? false; - - if (result) { - _isServiceRunning.value = true; - Logger.info('语音交互服务启动成功'); - } else { - Logger.error('语音交互服务启动失败'); - } - - return result; - } catch (e) { - Logger.error('启动语音交互服务失败: ${e.toString()}'); - return false; - } - } - - /// 停止语音交互服务 - Future _stopService() async { - if (!_isServiceRunning.value) { - Logger.info('语音交互服务未运行'); - return true; - } - - try { - final bool result = await _channel.invokeMethod('stopVoiceInteractionService') ?? false; - - if (result) { - _isServiceRunning.value = false; - Logger.info('语音交互服务停止成功'); - } else { - Logger.error('语音交互服务停止失败'); - } - - return result; - } catch (e) { - Logger.error('停止语音交互服务失败: ${e.toString()}'); - return false; - } - } - - /// 暂停语音交互(停止TTS和语音识别,但保持服务运行) - @override - Future pauseVoiceInteraction() async { - if (!_isServiceRunning.value) { - Logger.info('语音交互服务未运行,无法暂停'); - return false; - } - - try { - final bool result = await _channel.invokeMethod('pauseVoiceInteraction') ?? false; - - if (result) { - Logger.info('语音交互暂停成功'); - } else { - Logger.error('语音交互暂停失败'); - } - - return result; - } catch (e) { - Logger.error('暂停语音交互失败: ${e.toString()}'); - return false; - } - } - - /// 处理来自原生端的语音交互事件 - void _handleVoiceInteractionEvent(dynamic event) { - if (event is! Map || _eventStreamController == null) return; - - final Map eventMap = event; - final String eventType = eventMap['type'] as String? ?? ''; - final int timestamp = eventMap['timestamp'] as int? ?? 0; - - // 添加时间戳日志,帮助调试 - Logger.info('收到原生端事件: ${eventType.toString()}, 时间戳: $timestamp, 当前时间: ${DateTime.now().millisecondsSinceEpoch}'); - - switch (eventType) { - case 'chatHistory': - final String agentId = eventMap['agentId'] as String? ?? ''; - final String userMessage = eventMap['userMessage'] as String? ?? ''; - final String assistantMessage = eventMap['assistantMessage'] as String? ?? ''; - - // 添加到消息历史 - if (userMessage.isNotEmpty) { - _messageHistory.add({'role': 'user', 'content': userMessage}); - _messageHistory.add({'role': 'assistant', 'content': assistantMessage}); - - // 保持历史记录在一定长度 - while (_messageHistory.length > 10) { - _messageHistory.removeAt(0); - } - } - - _eventStreamController?.add(ChatHistoryEvent( - agentId: agentId, - userMessage: userMessage, - assistantMessage: assistantMessage, - timestamp: timestamp, - )); - - // 保存聊天记录 - _saveChatHistory(agentId, userMessage, assistantMessage, timestamp); - - Logger.info('收到聊天历史事件: agentId=$agentId'); - break; - - case 'recognitionStarted': - _eventStreamController?.add(RecognitionStartedEvent( - timestamp: timestamp, - )); - break; - - default: - Logger.warning('收到未知类型的语音交互事件: ${eventType.toString()}'); - break; - } - } - - @override - void onClose() { - // 清理资源 - _stopService(); - _eventSubscription?.cancel(); - _eventStreamController?.close(); - super.onClose(); - } - - /// 保存聊天记录 - Future _saveChatHistory(String agentId, String userMessage, String assistantMessage, int timestamp) async { - try { - if (userMessage.isEmpty) { - return; - } - - final userMsg = Message( - role: 'user', - content: userMessage, - timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp), - ); - - final assistantMsg = Message( - role: 'assistant', - content: assistantMessage, - timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp + 1), // 确保助手消息时间戳晚于用户消息 - ); - - // 获取ChatHistoryService实例 - final chatHistoryService = Get.find(); - - // 加载现有历史记录 - final existingMessages = await chatHistoryService.loadHistory(agentId); - - // 添加新消息 - existingMessages.addAll([userMsg, assistantMsg]); - - // 保存更新后的历史记录 - await chatHistoryService.saveHistory(agentId, existingMessages); - } catch (e) { - Logger.error('保存聊天记录失败: ${e.toString()}'); - } - } - - /// 是否处于活跃状态 - @override - bool isActive() { - return _isServiceRunning.value; - } - - /// 获取当前对话历史 - @override - List> getMessageHistory() { - return List>.from(_messageHistory); - } - - /// 获取当前服务ID - @override - String getServiceId() { - return _serviceId; - } -} - - -/// 聊天历史事件 -class ChatHistoryEvent extends VoiceInteractionEvent { - final String agentId; - final String userMessage; - final String assistantMessage; - - ChatHistoryEvent({ - required this.agentId, - required this.userMessage, - required this.assistantMessage, - required int timestamp, - }) : super(timestamp: timestamp); -} - -/// 语音识别开始事件 -class RecognitionStartedEvent extends VoiceInteractionEvent { - RecognitionStartedEvent({ - required int timestamp, - }) : super(timestamp: timestamp); -} \ No newline at end of file diff --git a/lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart b/lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart deleted file mode 100644 index 1fc80ba89..000000000 --- a/lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart +++ /dev/null @@ -1,466 +0,0 @@ -import 'dart:async'; -import 'package:get/get.dart'; -import '../asr_service.dart'; -import '../tts_service.dart'; -import '../volcano_ai_service.dart'; -import '../chat_history_service.dart'; -import '../../../modules/chat/models/message_model.dart'; -import '../../models/events/voice_interaction_event.dart'; -import '../../../core/utils/logger.dart'; - -enum VoiceInteractionState { - idle, // 等待唤醒 - active, // 活跃状态 - 可以同时识别和播放 -} - -class IosVoiceInteractionService extends GetxService { - final AsrService _asrService = Get.find(); - final TtsService _ttsService = Get.find(); - final VolcanoAIService _aiService = Get.find(); - final ChatHistoryService _chatHistoryService = Get.find(); - - // 当前状态 - final Rx state = VoiceInteractionState.idle.obs; - - // 是否识别到用户语音 - final RxBool isSpeechDetected = false.obs; - - // 是否正在处理AI响应 - final RxBool isProcessingAI = false.obs; - - // 是否正在播放TTS - final RxBool isSpeaking = false.obs; - - // 对话历史记录 - final List> _messageHistory = []; - - // 最大历史记录数 - static const int _maxHistorySize = 10; - - // 超时计时器 - Timer? _inactivityTimer; - - // 流订阅 - StreamSubscription? _recognitionSubscription; - - // 当前识别的文本 - String _currentRecognizedText = ''; - // 音频输入源 - bool _audioSourceType = false; - // 事件流控制器 - final StreamController _eventStreamController = - StreamController.broadcast(); - - // 获取事件流 - @override - Stream get eventStream => - _eventStreamController.stream; - - // 标识符,用于存储聊天历史 - final String _agentId = 'ios_assistant'; - - /// 初始化服务 - @override - Future initialize() async { - try { - // 保持idle状态,不自动启动语音识别 - Logger.info('iOS语音交互服务初始化完成,处于idle状态'); - } catch (e) { - Logger.error('iOS语音交互服务初始化失败: ${e.toString()}'); - } - - return this; - } - - // 处理唤醒事件 - void onWakeup() { - _ttsService.speakOnce('我在呢!'); - isSpeaking.value = true; - - if (state.value == VoiceInteractionState.idle) { - _activateInteraction(); - } - } - - // 激活交互 - void _activateInteraction() { - // 设置状态为活跃 - state.value = VoiceInteractionState.active; - - // 开始连续监听 - _startContinuousListening(); - - // 启动不活动计时器 - _startInactivityTimer(); - - // 发送识别开始事件 - _sendRecognitionStartedEvent(); - } - - // 开始连续监听 - Future _startContinuousListening() async { - // 停止之前的监听 - _recognitionSubscription?.cancel(); - - try { - // 开始连续识别 - final recognitionStream = - await _asrService.startContinuousRecognition(_audioSourceType); - - // 监听识别结果 - _recognitionSubscription = - recognitionStream.listen(_handleRecognitionEvent, onError: (error) { - Logger.error('语音识别错误: ${error.toString()}'); - _startContinuousListening(); // 尝试重新启动 - }); - - Logger.info('开始连续语音识别'); - } catch (e) { - Logger.error('启动语音识别失败: ${e.toString()}'); - } - } - - // 处理识别事件 - void _handleRecognitionEvent(RecognitionEvent event) { - // 更新活动时间 - _updateActivityTime(); - - switch (event.type) { - case RecognitionEventType.intermediateResult: - // 检测用户是否开始说话 - if (event.text.isNotEmpty && !isSpeechDetected.value) { - isSpeechDetected.value = true; - - // 用户开始讲话时,立即中断当前响应 - _interruptCurrentResponse("检测到用户开始讲话,中断当前响应"); - } - break; - - case RecognitionEventType.finalResult: - if (event.text.isNotEmpty) { - // 最终结果,处理用户输入 - _processUserInput(event.text); - } - // 重置语音检测状态 - isSpeechDetected.value = false; - break; - - case RecognitionEventType.sessionStarted: - Logger.info('语音识别会话开始'); - break; - - case RecognitionEventType.sessionStopped: - Logger.info('语音识别会话结束'); - - break; - - case RecognitionEventType.error: - case RecognitionEventType.canceled: - Logger.error('语音识别错误: ${event.error}'); - - break; - } - } - - // 中断当前响应 - void _interruptCurrentResponse(String reason) { - Logger.info(reason); - - // 如果正在活跃状态,需要中断当前操作 - if (state.value == VoiceInteractionState.active) { - // 停止TTS播放 - _ttsService.stop(); - isSpeaking.value = false; - isProcessingAI.value = false; - } - } - - // 处理用户输入 - Future _processUserInput(String text) async { - Logger.info('处理用户输入: $text'); - - // 设置为处理状态 - isProcessingAI.value = true; - - // 添加用户消息到历史记录 - final userMessage = {'role': 'user', 'content': text}; - _addToHistory(userMessage); - - // 创建用户消息对象 - final userMsg = Message( - role: 'user', - content: text, - timestamp: DateTime.now(), - ); - - // AI响应处理标志 - bool isProcessingCancelled = false; - - try { - // 构建消息历史 - final messageHistory = _buildMessageHistory(); - - // 调用AI服务获取响应 - final responseStream = _aiService.sendMessageStream( - messages: messageHistory, - systemPrompt: "你是一个智能助手,请简明扼要地回答问题。", - ); - - String aiResponse = ''; - - // 等待AI响应 - await for (final chunk in responseStream) { - // 检查是否被用户打断 - if (isSpeechDetected.value) { - // 用户开始说话,标记处理被取消 - isProcessingCancelled = true; - Logger.info('AI响应生成过程中被用户打断'); - break; - } - - // 如果状态已改变(可能由其他原因导致),停止处理 - if (state.value != VoiceInteractionState.active) { - isProcessingCancelled = true; - break; - } - - aiResponse += chunk; - - // 立即播放当前文本块,实现边生成边播放 - _speakStreamResponse(chunk); - } - - // 如果处理被取消,不继续后续操作 - if (isProcessingCancelled) { - isProcessingAI.value = false; - return; - } - - // 确保完整响应被处理 - if (aiResponse.isNotEmpty) { - // 将AI响应添加到历史记录 - final assistantMessage = {'role': 'assistant', 'content': aiResponse}; - _addToHistory(assistantMessage); - - // 创建助手消息对象 - final assistantMsg = Message( - role: 'assistant', - content: aiResponse, - timestamp: DateTime.now(), - ); - - // 保存聊天历史 - await _saveChatHistory(userMsg, assistantMsg); - - // 发送聊天历史事件 - _sendChatHistoryEvent(userMsg.content, assistantMsg.content); - - // 刷新TTS流,确保所有文本都被播放 - await _ttsService.flushStream(); - isSpeaking.value = false; - } - - // 处理完成 - isProcessingAI.value = false; - } catch (e) { - Logger.error('AI响应处理失败: ${e.toString()}'); - // 错误恢复 - isProcessingAI.value = false; - } - } - - // 构建用于AI服务的消息历史 - List> _buildMessageHistory() { - return List>.from(_messageHistory); - } - - // 添加消息到历史记录 - void _addToHistory(Map message) { - _messageHistory.add(message); - - // 限制历史记录大小 - while (_messageHistory.length > _maxHistorySize) { - _messageHistory.removeAt(0); - } - } - - // 播放流式响应 - void _speakStreamResponse(String chunk) { - if (chunk.isEmpty) return; - - // 更新播放状态 - isSpeaking.value = true; - - // 使用流式TTS播放 - _ttsService.speakStream(chunk); - - // 更新活动时间 - _updateActivityTime(); - } - - // 停止TTS - void _stopTts() { - _ttsService.stop(); - isSpeaking.value = false; - } - - // 更新最后活动时间 - DateTime _lastActivityTime = DateTime.now(); - void _updateActivityTime() { - _lastActivityTime = DateTime.now(); - } - - // 启动不活动计时器(长时间无交互会切换到空闲状态) - void _startInactivityTimer() { - _cancelInactivityTimer(); - - _inactivityTimer = Timer.periodic(Duration(seconds: 5), (timer) { - // 计算空闲时间 - final idleTime = DateTime.now().difference(_lastActivityTime).inSeconds; - - // 如果空闲超过30秒,且不在播放或检测到语音,切换到空闲 - if (idleTime > 8 && - !isSpeaking.value && - !isProcessingAI.value && - !isSpeechDetected.value) { - _resetToIdle(); - timer.cancel(); - } - }); - } - - // 取消不活动计时器 - void _cancelInactivityTimer() { - _inactivityTimer?.cancel(); - _inactivityTimer = null; - } - - // 重置到空闲状态 - void _resetToIdle() { - _ttsService.speakOnce('没有听到声音,暂停对话,双击耳机唤醒!'); - _cancelInactivityTimer(); - - // 停止TTS - _stopTts(); - - // 停止语音识别 - _recognitionSubscription?.cancel(); - _asrService.stopContinuousRecognition(); - - // 重置状态 - state.value = VoiceInteractionState.idle; - isSpeechDetected.value = false; - isProcessingAI.value = false; - isSpeaking.value = false; - - // 清空历史记录 - _messageHistory.clear(); - - Logger.info('语音交互已重置为空闲状态'); - } - - // 暂停语音交互 - @override - void pauseVoiceInteraction() { - if (state.value != VoiceInteractionState.idle) { - _stopTts(); - _recognitionSubscription?.cancel(); - _asrService.stopContinuousRecognition(); - - // 重置所有状态 - state.value = VoiceInteractionState.idle; - isSpeechDetected.value = false; - isProcessingAI.value = false; - isSpeaking.value = false; - - // 清空历史记录 - _messageHistory.clear(); - } - } - - // 恢复语音交互 - void resumeVoiceInteraction() { - if (state.value == VoiceInteractionState.idle) { - _activateInteraction(); - } - } - - // 手动停止交互 - void stopInteraction() { - _resetToIdle(); - } - - // 获取当前状态 - VoiceInteractionState getCurrentState() { - return state.value; - } - - // 是否处于活跃状态 - @override - bool isActive() { - return state.value != VoiceInteractionState.idle; - } - - // 获取当前对话历史 - @override - List> getMessageHistory() { - return List>.from(_messageHistory); - } - - // 获取当前服务ID - @override - String getServiceId() { - return _agentId; - } - - // 释放资源 - @override - void onClose() { - _cancelInactivityTimer(); - _recognitionSubscription?.cancel(); - _resetToIdle(); - _eventStreamController.close(); - super.onClose(); - } - - // 保存聊天历史 - Future _saveChatHistory(Message userMsg, Message assistantMsg) async { - try { - // 加载现有历史记录 - final existingMessages = await _chatHistoryService.loadHistory(_agentId); - - // 添加新消息 - existingMessages.addAll([userMsg, assistantMsg]); - - // 保存更新后的历史记录 - await _chatHistoryService.saveHistory(_agentId, existingMessages); - - Logger.info('保存聊天历史记录成功'); - } catch (e) { - Logger.error('保存聊天历史记录失败: ${e.toString()}'); - } - } - - // 发送聊天历史事件 - void _sendChatHistoryEvent(String userMessage, String assistantMessage) { - final event = ChatHistoryEvent( - agentId: _agentId, - userMessage: userMessage, - assistantMessage: assistantMessage, - timestamp: DateTime.now().millisecondsSinceEpoch, - ); - - _eventStreamController.add(event); - Logger.info('已发送聊天历史事件'); - } - - // 发送识别开始事件 - void _sendRecognitionStartedEvent() { - final event = RecognitionStartedEvent( - timestamp: DateTime.now().millisecondsSinceEpoch, - ); - - _eventStreamController.add(event); - Logger.info('已发送识别开始事件'); - } -} \ No newline at end of file diff --git a/lib/modules/music/views/music_playlist_view.dart b/lib/modules/music/views/music_playlist_view.dart index eaffa21c2..bdf72da2a 100644 --- a/lib/modules/music/views/music_playlist_view.dart +++ b/lib/modules/music/views/music_playlist_view.dart @@ -10,6 +10,7 @@ class MusicPlaylistView extends StatelessWidget { @override Widget build(BuildContext context) { + ScreenUtil.init(context, designSize: const Size(375, 812)); // 添加设计尺寸参考 final controller = Get.find(); final List> playlist = Get.arguments ?? []; final isDarkMode = Theme.of(context).brightness == Brightness.dark; @@ -32,14 +33,14 @@ class MusicPlaylistView extends StatelessWidget { children: [ Icon( Icons.music_off, - size: 64, + size: 64.w, color: Colors.grey[400], ), - const SizedBox(height: 16), + SizedBox(height: 16.h), Text( '暂无音乐', style: TextStyle( - fontSize: 16, + fontSize: 16.sp, color: Colors.grey[600], ), ), @@ -48,10 +49,9 @@ class MusicPlaylistView extends StatelessWidget { ) : Column( children: [ - // 上部:当前播放音乐详情 - 进一步优化高度占比 - SizedBox( - height: MediaQuery.of(context).size.height * - 0.35, // 从35%减少到32%,为进度条优化腾出空间 + // 上部:当前播放音乐详情 - 响应式高度 + Container( + height: MediaQuery.of(context).size.height * 0.38, // 增大高度比例 child: _buildCurrentPlayingSection( controller, playlist, isDarkMode), ), @@ -70,105 +70,101 @@ class MusicPlaylistView extends StatelessWidget { // 构建当前播放音乐详情部分 Widget _buildCurrentPlayingSection(AgentController controller, List> playlist, bool isDarkMode) { - return Obx(() { - // 查找当前播放的歌曲 - 修复自动切换时不更新的问题 - Map? currentSong; - if (controller.currentId.value != 0) { - try { - currentSong = playlist.firstWhere( - (song) => - int.tryParse(song['id']?.toString() ?? '0') == - controller.currentId.value, - ); - } catch (e) { - // 如果没找到,使用第一首歌曲 + return SingleChildScrollView( + // 添加滚动支持 + child: Obx(() { + // 查找当前播放的歌曲 + Map? currentSong; + if (controller.currentId.value != 0) { + try { + currentSong = playlist.firstWhere( + (song) => + int.tryParse(song['id']?.toString() ?? '0') == + controller.currentId.value, + ); + } catch (e) { + // 如果没找到,使用第一首歌曲 + currentSong = playlist.isNotEmpty ? playlist.first : null; + } + } else { currentSong = playlist.isNotEmpty ? playlist.first : null; } - } else { - currentSong = playlist.isNotEmpty ? playlist.first : null; - } - if (currentSong == null) { - return Container( - padding: const EdgeInsets.all(16), // 减少padding - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.music_note, - size: 60, // 减小图标尺寸 - color: Colors.grey[400], - ), - const SizedBox(height: 12), // 减少间距 - Text( - '暂无播放音乐', - style: TextStyle( - fontSize: 16, // 减小字体 - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + if (currentSong == null) { + return Container( + padding: EdgeInsets.all(16.w), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.music_note, + size: 60.w, + color: Colors.grey[400], ), - ), - ], + SizedBox(height: 12.h), + Text( + '暂无播放音乐', + style: TextStyle( + fontSize: 16.sp, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + ), + ], + ), ), - ), - ); - } + ); + } - final title = currentSong['name'] ?? currentSong['title'] ?? '未知歌曲'; - final artist = currentSong['singer'] ?? - currentSong['sgener'] ?? - currentSong['artist'] ?? - '未知歌手'; - final coverUrl = currentSong['image'] ?? currentSong['coverUrl'] ?? ''; + final title = currentSong['name'] ?? currentSong['title'] ?? '未知歌曲'; + final artist = currentSong['singer'] ?? + currentSong['sgener'] ?? + currentSong['artist'] ?? + '未知歌手'; + final coverUrl = currentSong['image'] ?? currentSong['coverUrl'] ?? ''; - return SingleChildScrollView( - // 添加滚动支持 - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 20, vertical: 12), // 优化padding:水平增加,垂直减少 + return Container( + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 12.h), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - // 封面图片 - 进一步优化尺寸 - _buildCoverImage(coverUrl, isDarkMode, - size: 110), // 从120减小到110,为进度条优化腾出空间 - const SizedBox(height: 12), // 减少间距从16到14 + // 封面图片 - 响应式尺寸 + _buildCoverImage(coverUrl, isDarkMode, size: 110.w), + SizedBox(height: 12.h), // 歌曲信息 _buildSongInfo(title, artist, isDarkMode), - const SizedBox(height: 16), // 增加歌曲信息和进度条之间的间距,提升视觉层次 - // 进度条 - 现在是水平布局,需要更多关注 + SizedBox(height: 16.h), + // 进度条 _buildProgressBar(controller, isDarkMode), - const SizedBox(height: 15), // 增加进度条和控制按钮之间的间距 + SizedBox(height: 15.h), // 播放控制按钮 _buildPlayControls(controller, playlist, isDarkMode), ], ), - ), - ); - }); + ); + }), + ); } // 构建播放全部按钮部分 Widget _buildPlayAllSection(AgentController controller, List> playlist, bool isDarkMode) { return Container( - padding: const EdgeInsets.symmetric( - horizontal: 20, vertical: 12), // 优化padding,与音乐详情区域保持一致 + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 12.h), child: SizedBox( width: double.infinity, - height: 48, // 从50减小到48,更紧凑 + height: 48.h, child: ElevatedButton.icon( onPressed: () async { if (playlist.isNotEmpty) { await controller.playPlaylist(playlist); } }, - icon: const Icon(Icons.play_arrow, - color: Colors.white, size: 20), // 调整图标大小 - label: const Text( + icon: Icon(Icons.play_arrow, color: Colors.white, size: 20.w), + label: Text( '播放全部', style: TextStyle( - fontSize: 15, // 从16减小到15 + fontSize: 15.sp, fontWeight: FontWeight.w600, color: Colors.white, ), @@ -176,12 +172,10 @@ class MusicPlaylistView extends StatelessWidget { style: ElevatedButton.styleFrom( backgroundColor: Theme.of(Get.context!).primaryColor, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), // 从25减小到14,更协调 + borderRadius: BorderRadius.circular(14.r), ), - elevation: 3, // 从2增加到3,增强视觉层次 - shadowColor: Theme.of(Get.context!) - .primaryColor - .withValues(alpha: 0.4), // 添加阴影颜色 + elevation: 3, + shadowColor: Theme.of(Get.context!).primaryColor.withAlpha(102), ), ), ), @@ -192,8 +186,7 @@ class MusicPlaylistView extends StatelessWidget { Widget _buildPlaylistSection(AgentController controller, List> playlist, bool isDarkMode) { return ListView.builder( - padding: const EdgeInsets.symmetric( - horizontal: 20, vertical: 8), // 优化padding,与其他区域保持一致 + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 8.h), itemCount: playlist.length, itemBuilder: (context, index) { final item = playlist[index]; @@ -237,40 +230,34 @@ class MusicPlaylistView extends StatelessWidget { return GestureDetector( onTap: () { - // 点击音乐卡片直接播放对应歌曲 _playSelectedSong(controller, song, Get.arguments ?? []); }, child: Container( - margin: const EdgeInsets.only(bottom: 10), // 从12减小到10,更紧凑 - padding: const EdgeInsets.all(14), // 从12增加到14,增加内部空间 + margin: EdgeInsets.only(bottom: 10.h), + padding: EdgeInsets.all(14.w), decoration: BoxDecoration( color: isDarkMode ? Colors.grey[800] : Colors.white, - borderRadius: BorderRadius.circular(14), // 从12增加到14,更现代的圆角 + borderRadius: BorderRadius.circular(14.r), boxShadow: [ BoxShadow( - color: Colors.black - .withValues(alpha: isDarkMode ? 0.25 : 0.05), // 增强阴影效果 - blurRadius: 4, // 从3增加到4 - spreadRadius: 0, // 从1减小到0,更自然的阴影 - offset: const Offset(0, 2), // 从(0,1)调整到(0,2),更明显的深度感 + color: Colors.black.withOpacity(isDarkMode ? 0.25 : 0.05), + blurRadius: 4, + offset: const Offset(0, 2), ), ], ), child: Row( children: [ - // 封面图片 - 优化设计 + // 封面图片 Container( - width: 52, // 从50增加到52,稍微大一点 - height: 52, + width: 52.w, + height: 52.w, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), // 从8增加到10,更现代的圆角 - color: isDarkMode - ? Colors.grey[700] - : Colors.grey[200], // 根据主题调整背景色 + borderRadius: BorderRadius.circular(10.r), + color: isDarkMode ? Colors.grey[700] : Colors.grey[200], boxShadow: [ BoxShadow( - color: - Colors.black.withValues(alpha: isDarkMode ? 0.2 : 0.08), + color: Colors.black.withOpacity(isDarkMode ? 0.2 : 0.08), blurRadius: 2, offset: const Offset(0, 1), ), @@ -278,7 +265,7 @@ class MusicPlaylistView extends StatelessWidget { ), child: image.isNotEmpty ? ClipRRect( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(10.r), child: Image.network( image, fit: BoxFit.cover, @@ -288,7 +275,7 @@ class MusicPlaylistView extends StatelessWidget { color: isDarkMode ? Colors.grey[500] : Colors.grey[600], - size: 24, // 指定图标大小 + size: 24.w, ); }, ), @@ -296,34 +283,34 @@ class MusicPlaylistView extends StatelessWidget { : Icon( Icons.music_note, color: isDarkMode ? Colors.grey[500] : Colors.grey[600], - size: 24, // 指定图标大小 + size: 24.w, ), ), - const SizedBox(width: 14), // 从12增加到14,增加间距 - // 歌曲信息 - 优化文本样式 + SizedBox(width: 14.w), + // 歌曲信息 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, // 垂直居中对齐 + mainAxisAlignment: MainAxisAlignment.center, children: [ Text( name, style: TextStyle( - fontSize: 15.5, // 从16减小到15.5,更协调 - fontWeight: FontWeight.w600, // 从w500增加到w600,更突出 + fontSize: 15.5.sp, + fontWeight: FontWeight.w600, color: isDarkMode ? Colors.white : Colors.black87, - height: 1.2, // 添加行高控制 + height: 1.2, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), - const SizedBox(height: 3), // 从4减小到3,更紧凑 + SizedBox(height: 3.h), Text( singer, style: TextStyle( - fontSize: 13, // 从14减小到13 + fontSize: 13.sp, color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - height: 1.1, // 添加行高控制 + height: 1.1, ), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -331,15 +318,12 @@ class MusicPlaylistView extends StatelessWidget { ], ), ), - const SizedBox(width: 14), // 从12增加到14,与封面图片间距保持一致 + SizedBox(width: 14.w), // 播放按钮 Obx(() { - // 修复类型比较问题:将id转换为int进行比较 final currentIdInt = int.tryParse(id) ?? 0; final isThisPlaying = controller.isPlaying.value && controller.currentId.value == currentIdInt; - - // 只有当前播放的歌曲才显示进度 final currentProgress = isThisPlaying ? controller.progress.value : 0.0; @@ -348,7 +332,7 @@ class MusicPlaylistView extends StatelessWidget { progress: currentProgress, primaryColor: Theme.of(context).primaryColor, backgroundColor: Colors.grey, - size: 40.0, + size: 40.w, strokeWidth: 2.5, onPlayPause: () async { try { @@ -356,7 +340,6 @@ class MusicPlaylistView extends StatelessWidget { '播放按钮被点击,当前播放状态: ${controller.isPlaying.value}'); Logger.d('MusicPlaylistView', '当前歌曲ID: ${controller.currentId.value}, 点击歌曲ID: $currentIdInt'); - if (controller.isPlaying.value) { if (controller.currentId.value == currentIdInt) { Logger.d('MusicPlaylistView', '暂停当前播放的歌曲'); @@ -391,15 +374,15 @@ class MusicPlaylistView extends StatelessWidget { // 构建封面图片 Widget _buildCoverImage(String coverUrl, bool isDarkMode, - {double size = 200}) { + {required double size}) { return Container( width: size, height: size, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(16.r), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: isDarkMode ? 0.3 : 0.1), + color: Colors.black.withOpacity(isDarkMode ? 0.3 : 0.1), blurRadius: 15, spreadRadius: 2, offset: const Offset(0, 6), @@ -407,27 +390,27 @@ class MusicPlaylistView extends StatelessWidget { ], ), child: ClipRRect( - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(16.r), child: coverUrl.isNotEmpty ? Image.network( coverUrl, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { - return _buildDefaultCover(isDarkMode); + return _buildDefaultCover(isDarkMode, size: size); }, ) - : _buildDefaultCover(isDarkMode), + : _buildDefaultCover(isDarkMode, size: size), ), ); } // 构建默认封面 - Widget _buildDefaultCover(bool isDarkMode) { + Widget _buildDefaultCover(bool isDarkMode, {required double size}) { return Container( color: isDarkMode ? Colors.grey[800] : Colors.grey[300], child: Icon( Icons.music_note, - size: 60, // 从20改为60,更合适的大小 + size: size * 0.55, color: isDarkMode ? Colors.grey[600] : Colors.grey[500], ), ); @@ -440,7 +423,7 @@ class MusicPlaylistView extends StatelessWidget { Text( title, style: TextStyle( - fontSize: 18, // 从20减小到18 + fontSize: 18.sp, fontWeight: FontWeight.bold, color: isDarkMode ? Colors.white : Colors.black87, ), @@ -448,11 +431,11 @@ class MusicPlaylistView extends StatelessWidget { maxLines: 2, overflow: TextOverflow.ellipsis, ), - const SizedBox(height: 6), // 从8减小到6 + SizedBox(height: 6.h), Text( artist, style: TextStyle( - fontSize: 14, // 从16减小到14 + fontSize: 14.sp, color: isDarkMode ? Colors.grey[400] : Colors.grey[600], ), textAlign: TextAlign.center, @@ -463,39 +446,36 @@ class MusicPlaylistView extends StatelessWidget { ); } - // 构建进度条 - 优化布局:将进度条和时间显示合并到同一水平线 + // 构建进度条 Widget _buildProgressBar(AgentController controller, bool isDarkMode) { return Obx(() { return Row( children: [ - // 左侧:当前播放时间 SizedBox( - width: 35.w, // 固定宽度确保对齐,足够显示 "99:59" 格式 - + width: 40.w, child: Text( _formatDuration(controller.currentPosition.value), style: TextStyle( - fontSize: 12, + fontSize: 12.sp, color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - fontFeatures: const [FontFeature.tabularFigures()], // 使用等宽数字 + fontFeatures: const [FontFeature.tabularFigures()], ), textAlign: TextAlign.left, ), ), - SizedBox(width: 6.w), // 左侧时间和进度条之间的间距 - // 中间:可交互的进度条 + SizedBox(width: 8.w), Expanded( child: SliderTheme( - data: SliderTheme.of(Get.context!).copyWith( + 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.withValues(alpha: 0.2), + Theme.of(Get.context!).primaryColor.withOpacity(0.2), trackHeight: 4.h, - thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 0), - overlayShape: const RoundSliderOverlayShape(overlayRadius: 0), + thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8), + overlayShape: const RoundSliderOverlayShape(overlayRadius: 14), ), child: Slider( value: controller.progress.value.clamp(0.0, 1.0), @@ -507,16 +487,15 @@ class MusicPlaylistView extends StatelessWidget { ), ), ), - SizedBox(width: 6.w), // 进度条和右侧时间之间的间距 - // 右侧:音乐总时长 + SizedBox(width: 8.w), SizedBox( - width: 35.w, // 固定宽度确保对齐,足够显示 "99:59" 格式 + width: 40.w, child: Text( _formatDuration(controller.duration.value), style: TextStyle( - fontSize: 12, + fontSize: 12.sp, color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - fontFeatures: const [FontFeature.tabularFigures()], // 使用等宽数字 + fontFeatures: const [FontFeature.tabularFigures()], ), textAlign: TextAlign.right, ), @@ -526,14 +505,6 @@ class MusicPlaylistView extends StatelessWidget { }); } - // 格式化时间显示 - 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')}'; - } - // 构建播放控制按钮 Widget _buildPlayControls(AgentController controller, List> playlist, bool isDarkMode) { @@ -541,50 +512,48 @@ class MusicPlaylistView extends StatelessWidget { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - // 上一曲按钮 IconButton( onPressed: () => _playPrevious(controller, playlist), + iconSize: 32.w, + padding: EdgeInsets.all(8.w), // 增大触摸区域 icon: Icon( Icons.skip_previous, - size: 32, // 从36减小到32 color: isDarkMode ? Colors.white70 : Colors.black54, ), ), - const SizedBox(width: 16), // 从20减小到16 - // 播放/暂停按钮 + SizedBox(width: 16.w), Container( - width: 56, // 从64减小到56 - height: 56, + width: 56.w, + height: 56.w, decoration: BoxDecoration( color: Theme.of(Get.context!).primaryColor, shape: BoxShape.circle, boxShadow: [ BoxShadow( - color: Theme.of(Get.context!) - .primaryColor - .withValues(alpha: 0.3), - blurRadius: 10, // 从12减小到10 - spreadRadius: 1, // 从2减小到1 - offset: const Offset(0, 3), // 从4减小到3 + color: Theme.of(Get.context!).primaryColor.withOpacity(0.3), + blurRadius: 10, + spreadRadius: 1, + offset: const Offset(0, 3), ), ], ), child: IconButton( onPressed: () => _togglePlayPause(controller), + padding: EdgeInsets.zero, icon: Icon( controller.isPlaying.value ? Icons.pause : Icons.play_arrow, - size: 28, // 从32减小到28 + size: 28.w, color: Colors.white, ), ), ), - const SizedBox(width: 16), // 从20减小到16 - // 下一曲按钮 + SizedBox(width: 16.w), IconButton( onPressed: () => _playNext(controller, playlist), + iconSize: 32.w, + padding: EdgeInsets.all(8.w), // 增大触摸区域 icon: Icon( Icons.skip_next, - size: 32, // 从36减小到32 color: isDarkMode ? Colors.white70 : Colors.black54, ), ), @@ -593,6 +562,15 @@ class MusicPlaylistView extends StatelessWidget { }); } + // ...(其他辅助方法保持不变,如_formatDuration, _togglePlayPause等) + // 格式化时间显示 + String _formatDuration(int milliseconds) { + final duration = Duration(milliseconds: milliseconds); + final minutes = duration.inMinutes; + final seconds = duration.inSeconds % 60; + return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; + } + // 播放/暂停切换 Future _togglePlayPause(AgentController controller) async { if (controller.isPlaying.value) { @@ -605,7 +583,6 @@ class MusicPlaylistView extends StatelessWidget { // 播放上一曲 void _playPrevious( AgentController controller, List> playlist) { - // 确保播放列表上下文是最新的 controller.updateCurrentPlaylistContext( playlist, controller.currentId.value); controller.playPrevious(); @@ -614,7 +591,6 @@ class MusicPlaylistView extends StatelessWidget { // 播放下一曲 void _playNext( AgentController controller, List> playlist) { - // 确保播放列表上下文是最新的 controller.updateCurrentPlaylistContext( playlist, controller.currentId.value); controller.playNext(); @@ -623,11 +599,8 @@ class MusicPlaylistView extends StatelessWidget { // 播放选中的歌曲 void _playSelectedSong(AgentController controller, Map song, List> playlist) { - // 更新播放列表上下文 final songId = int.tryParse(song['id']?.toString() ?? '0') ?? 0; controller.updateCurrentPlaylistContext(playlist, songId); - - // 播放选中的歌曲 controller.musicPlay(song); } } diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index 1915aaba3..7a4505126 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -60,6 +60,9 @@ class PairingController extends GetxController { final showRetryButton = false.obs; final showAutoCodeInput = false.obs; final showSkipButton = false.obs; + // 增加临时跳过绑定按钮状态 + final showSkipBindingButton = false.obs; // 新增临时跳过绑定按钮 + final connectButtonText = '连接设备'.obs; final retryButtonText = '重试'.obs; @@ -242,6 +245,7 @@ class PairingController extends GetxController { isLoading.value = false; showConnectButton.value = false; showRetryButton.value = false; + showSkipBindingButton.value = false; // 重置临时跳过绑定按钮 showSkipButton.value = false; showAutoCodeInput.value = false; @@ -285,10 +289,11 @@ class PairingController extends GetxController { }); break; case PairingState.manualbinding: - statusText.value = '需要手动版定授权码...'; + statusText.value = '需要手动绑定授权码...'; isLoading.value = false; showAutoCodeInput.value = true; - showSkipButton.value = true; + // showSkipButton.value = true; + showSkipBindingButton.value = true; // 显示临时跳过绑定按钮 break; case PairingState.completed: statusText.value = '配对成功!'; @@ -557,6 +562,7 @@ class PairingController extends GetxController { }); Logger.i(_tag, '手动授权结果 ${response}'); if (response != null) { + User.instance.earphonemac = currentDevice?.address ?? ''; _transition(PairingState.verifying); _verifyDevice(_currentClassicDevice!); } else { @@ -575,7 +581,7 @@ class PairingController extends GetxController { } catch (e) { Logger.e(_tag, '启动AgentService失败: $e'); } - + Get.offAllNamed(Routes.home); }); } @@ -631,10 +637,19 @@ class PairingController extends GetxController { } } + /// 用户点击跳过绑定按钮 + void onSkipBindingTap() { + Logger.i(_tag, '用户选择临时跳过手动绑定'); + + User.instance.earphonemac = currentDevice?.address ?? ''; + _transition(PairingState.verifying); + _verifyDevice(_currentClassicDevice!); + } + /// 用户点击跳过按钮 void onSkipTap() async { Logger.i(_tag, '用户选择跳过配对'); - + // 启动AgentService服务 try { Logger.i(_tag, '启动AgentService服务'); @@ -642,7 +657,7 @@ class PairingController extends GetxController { } catch (e) { Logger.e(_tag, '启动AgentService失败: $e'); } - + Get.offAllNamed(Routes.home); } diff --git a/lib/modules/pairing/views/pairing_view.dart b/lib/modules/pairing/views/pairing_view.dart index 542e22ec9..9594f3760 100644 --- a/lib/modules/pairing/views/pairing_view.dart +++ b/lib/modules/pairing/views/pairing_view.dart @@ -436,6 +436,7 @@ class PairingView extends GetView { final showRetryButton = controller.showRetryButton.value; final showSkipButton = controller.showSkipButton.value; final showAutoCodeInput = controller.showAutoCodeInput.value; + final showSkipBindingButton = controller.showSkipBindingButton.value; return Container( width: double.infinity, child: Column( @@ -557,6 +558,26 @@ class PairingView extends GetView { ), ), ], + + // 跳过绑定按钮 + if (showSkipBindingButton) ...[ + SizedBox(height: 12.h), + TextButton( + onPressed: () => controller.onSkipBindingTap(), + style: TextButton.styleFrom( + foregroundColor: Color(0xFF0066CC), + padding: EdgeInsets.symmetric(vertical: 12.h), + minimumSize: Size(double.infinity, 48.h), + ), + child: Text( + '跳过绑定'.tr, // 稍后再说 + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + ), + ), + ), + ], ], ), ); diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index 8f3a14799..1d544a9e7 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -15,8 +15,8 @@ import '../modules/opus_test/bindings/opus_test_binding.dart'; import '../modules/opus_test/views/opus_test_view.dart'; import '../modules/profile/views/profile_view.dart'; import '../modules/profile/bindings/profile_binding.dart'; -import '../modules/chat/bindings/chat_binding.dart'; -import '../modules/chat/views/chat_view.dart'; +// import '../modules/chat/bindings/chat_binding.dart'; +//import '../modules/chat/views/chat_view.dart'; import '../modules/timbre/bindings/timbre_binding.dart'; import '../modules/timbre/views/timbre_view.dart'; @@ -91,18 +91,18 @@ abstract class AppPages { transition: Transition.noTransition, transitionDuration: Duration(milliseconds: 250), ), - GetPage( - name: Routes.chat, - page: () => const ChatView(), - binding: ChatBinding(), - transition: Transition.rightToLeft, - transitionDuration: Duration(milliseconds: 250), - ), - GetPage( - name: Routes.recording, - page: () => const ChatView(), - binding: ChatBinding(), - ), + // GetPage( + // name: Routes.chat, + // page: () => const ChatView(), + // binding: ChatBinding(), + // transition: Transition.rightToLeft, + // transitionDuration: Duration(milliseconds: 250), + // ), + // GetPage( + // name: Routes.recording, + // page: () => const ChatView(), + // binding: ChatBinding(), + // ), GetPage( name: Routes.OTA_UPGRADE, page: () => const OtaUpgradeView(), diff --git a/pubspec.yaml b/pubspec.yaml index 200cfc6fb..398b7aa98 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,10 +1,10 @@ name: deep_voice description: "A new Flutter project." -publish_to: 'none' -version: 1.0.0+1 +publish_to: "none" +version: 1.0.0+6 environment: - sdk: '>=3.3.0 <4.0.0' + sdk: ">=3.3.0 <4.0.0" dependencies: flutter: @@ -107,8 +107,8 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.0 - build_runner: ^2.3.3 # 用于生成代码 flutter pub run build_runner build - json_serializable: ^6.6.0 # 用于自动生成代码 + build_runner: ^2.3.3 # 用于生成代码 flutter pub run build_runner build + json_serializable: ^6.6.0 # 用于自动生成代码 dependency_overrides: permission_handler: ^12.0.0 From 879e0a5620914d162fe678b1a3d6cbc1058ffaa8 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Thu, 26 Jun 2025 15:55:07 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E7=BB=91=E5=AE=9A?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E4=BB=A3=E7=A0=81=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/services/network/api.dart | 9 +++++++++ lib/modules/pairing/controllers/pairing_controller.dart | 9 ++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/data/services/network/api.dart b/lib/data/services/network/api.dart index 1eb3f41a0..bed184ddc 100644 --- a/lib/data/services/network/api.dart +++ b/lib/data/services/network/api.dart @@ -135,4 +135,13 @@ class Api { params: params, ); } + + //绑定设备 + static bindauthcode([params]) { + return DioManager().request( + NWMethod.post, + '/api/home/user_bindauthcode', + params: params, + ); + } } diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index 1915aaba3..6ea666b8b 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -535,7 +535,6 @@ class PairingController extends GetxController { // 调用BLE管理器的自动绑定方法 final response = await Api.bindearphone({ "earphonemac": currentDevice?.address, - "earphoneauthcode": "", }); Logger.i(_tag, '手动授权结果 ${response}'); if (response != null) { @@ -551,7 +550,7 @@ class PairingController extends GetxController { /// 用户点击授权码输入框 void onAutoCodeInputTap() async { // 调用BLE管理器的自动绑定方法 - final response = await Api.bindearphone({ + final response = await Api.bindauthcode({ "earphonemac": currentDevice?.address, "earphoneauthcode": codeInputController.text.trim(), }); @@ -575,7 +574,7 @@ class PairingController extends GetxController { } catch (e) { Logger.e(_tag, '启动AgentService失败: $e'); } - + Get.offAllNamed(Routes.home); }); } @@ -634,7 +633,7 @@ class PairingController extends GetxController { /// 用户点击跳过按钮 void onSkipTap() async { Logger.i(_tag, '用户选择跳过配对'); - + // 启动AgentService服务 try { Logger.i(_tag, '启动AgentService服务'); @@ -642,7 +641,7 @@ class PairingController extends GetxController { } catch (e) { Logger.e(_tag, '启动AgentService失败: $e'); } - + Get.offAllNamed(Routes.home); } From bff8543b690674d748d147ad8de21bf6cb608255 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Thu, 26 Jun 2025 16:26:11 +0800 Subject: [PATCH 5/5] =?UTF-8?q?feat(profile):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=98=B5=E7=A7=B0=E5=86=85=E8=81=94=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E5=92=8C=E4=BC=98=E5=8C=96=E5=A4=B4=E5=83=8F=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增昵称内联编辑功能,替换原有的对话框编辑方式 - 添加加载状态指示器和错误处理机制 - 实现乐观更新和状态回滚功能 - 优化头像上传流程,增加压缩和验证逻辑 - 改进错误提示和用户反馈体验 --- .../controllers/profile_controller.dart | 396 +++++++++++++++--- lib/modules/profile/views/profile_view.dart | 254 ++++++----- .../yunqiinnovation/ble_service/BleConst.kt | 7 +- 3 files changed, 497 insertions(+), 160 deletions(-) diff --git a/lib/modules/profile/controllers/profile_controller.dart b/lib/modules/profile/controllers/profile_controller.dart index 1dae96f4f..2480174d3 100644 --- a/lib/modules/profile/controllers/profile_controller.dart +++ b/lib/modules/profile/controllers/profile_controller.dart @@ -28,6 +28,13 @@ class ProfileController extends GetxController { final Rx userInfo = User.instance.obs; // 响应式用户信息对象,使用单例模式确保全局唯一性 final RxString localAvatar = ''.obs; // 本地头像路径,便于响应式更新UI final RxBool hasEdits = false.obs; // 是否有编辑 + final RxBool isUpdating = false.obs; // 是否正在更新用户信息 + final RxBool isEditingNickname = false.obs; // 是否正在编辑昵称(内联编辑状态) + + // 用于乐观更新的原始状态备份 + String _originalName = ''; + String _originalAvatar = ''; + String _originalLocalAvatar = ''; // 使用响应式变量 final currentLanguage = 'zh'.obs; @@ -35,6 +42,33 @@ class ProfileController extends GetxController { // 账号服务 late final AccountService _accountService; + // 错误处理工具方法 + String _getErrorMessage(dynamic error) { + final errorStr = error.toString().toLowerCase(); + + if (errorStr.contains('network') || + errorStr.contains('socket') || + errorStr.contains('timeout') || + errorStr.contains('connection')) { + return '网络连接失败,请检查网络设置后重试'; + } else if (errorStr.contains('server') || + errorStr.contains('500') || + errorStr.contains('502') || + errorStr.contains('503')) { + return '服务器暂时无法响应,请稍后重试'; + } else if (errorStr.contains('401') || errorStr.contains('unauthorized')) { + return '登录已过期,请重新登录'; + } else if (errorStr.contains('403') || errorStr.contains('forbidden')) { + return '没有权限执行此操作'; + } else if (errorStr.contains('404')) { + return '请求的资源不存在'; + } else if (errorStr.contains('file') || errorStr.contains('image')) { + return '图片处理失败,请重新选择图片'; + } else { + return '操作失败,请重试'; + } + } + @override void onInit() { super.onInit(); @@ -158,6 +192,8 @@ class ProfileController extends GetxController { // 更新用户信息到后端 Future _syncWithBackend() async { try { + Logger().d('开始同步用户信息到后端'); + // 创建FormData对象用于上传 dio.FormData formData = dio.FormData(); @@ -182,10 +218,12 @@ class ProfileController extends GetxController { ); Logger().d('已添加头像文件到请求'); } else { - Logger().e('头像文件不存在'); + Logger().e('头像文件不存在: ${localAvatar.value}'); + throw Exception('头像文件不存在'); } } catch (e) { Logger().e('读取头像文件失败: $e'); + throw Exception('读取头像文件失败: $e'); } } @@ -194,25 +232,53 @@ class ProfileController extends GetxController { // 调用API,使用FormData final response = await Api.updateUserSetting(formData); Logger().d('--更新用户信息响应: $response'); - if (response != null) { + + if (response != null && response is Map) { + // 检查响应是否包含错误 + if (response.containsKey('error') || response.containsKey('msg')) { + final errorMsg = response['error'] ?? response['msg'] ?? '服务器返回错误'; + Logger().e('服务器返回错误: $errorMsg'); + throw Exception('服务器错误: $errorMsg'); + } + Logger().d('更新成功: $response'); - Get.snackbar('成功', '同步到服务器成功'); + + // 更新用户信息为服务器返回的最新数据 userInfo.update((u) { if (u != null) { - u.name = response['name'] ?? ''; - u.avatar = response['avatar'] ?? ''; + u.name = response['name'] ?? u.name; + u.avatar = response['avatar'] ?? u.avatar; } }); + + // 如果服务器返回了新的头像URL,下载并保存到本地 + if (response['avatar'] != null && response['avatar'].toString().isNotEmpty) { + final serverAvatar = response['avatar'].toString(); + if (serverAvatar.startsWith('http')) { + // 异步下载新头像,不阻塞当前流程 + _downloadAndSaveAvatar(serverAvatar); + } + } + Logger().d('更新后的用户信息: ${userInfo.value.toJson()}'); } else { - Logger().e('更新失败: $response'); - Get.snackbar('错误', '同步到服务器失败,请重试'); - return; + Logger().e('服务器响应为空或格式错误: $response'); + throw Exception('服务器响应异常'); } } catch (e) { Logger().e('同步到后端失败: $e'); - Get.snackbar('Error', '同步到服务器失败,请重试'); - throw e; + + // 根据错误类型抛出更具体的异常 + if (e.toString().contains('SocketException') || + e.toString().contains('TimeoutException') || + e.toString().contains('network')) { + throw Exception('network: 网络连接失败,请检查网络设置'); + } else if (e.toString().contains('500') || + e.toString().contains('server')) { + throw Exception('server: 服务器内部错误,请稍后重试'); + } else { + throw Exception('unknown: ${e.toString()}'); + } } } @@ -221,12 +287,127 @@ class ProfileController extends GetxController { if (userJson != null) { userInfo.value = User.instance; Logger().d('从本地加载用户信息: $userJson'); + + // 加载本地头像路径并验证文件有效性 + final savedLocalAvatar = _storage.read('local_avatar'); + if (savedLocalAvatar != null && savedLocalAvatar.toString().isNotEmpty) { + final avatarPath = savedLocalAvatar.toString(); + // 验证文件是否存在 + final file = File(avatarPath); + file.exists().then((exists) { + if (exists) { + localAvatar.value = avatarPath; + Logger().d('加载本地头像路径: ${localAvatar.value}'); + } else { + // 文件不存在,清除无效路径 + localAvatar.value = ''; + _storage.remove('local_avatar'); + Logger().w('本地头像文件不存在,已清除路径: $avatarPath'); + } + }); + } else { + // 确保本地头像路径为空 + localAvatar.value = ''; + Logger().d('没有本地头像路径'); + } + + // 保存原始状态用于回滚 + _saveOriginalState(); + // 如果有远程头像地址,下载并保存到本地 if (userInfo.value.avatar.isNotEmpty) { Logger().d('远程头像地址: ${userInfo.value.avatar}'); _downloadAndSaveAvatar(userInfo.value.avatar); } + + // 确保状态一致性 + _ensureStateConsistency(); + } + } + + // 确保本地状态与服务器状态的一致性 + void _ensureStateConsistency() { + // 检查本地头像文件是否存在 + if (localAvatar.value.isNotEmpty) { + final file = File(localAvatar.value); + file.exists().then((exists) { + if (!exists) { + Logger().w('本地头像文件不存在,清除本地头像路径: ${localAvatar.value}'); + localAvatar.value = ''; + _storage.remove('local_avatar'); + + // 如果有远程头像,重新下载 + if (userInfo.value.avatar.isNotEmpty) { + _downloadAndSaveAvatar(userInfo.value.avatar); + } + } + }); + } + } + + // 保存原始状态用于乐观更新回滚 + void _saveOriginalState() { + _originalName = userInfo.value.name; + _originalAvatar = userInfo.value.avatar; + _originalLocalAvatar = localAvatar.value; + Logger().d('已保存原始状态 - 名称: $_originalName, 头像: $_originalAvatar, 本地头像: $_originalLocalAvatar'); + } + + // 回滚到原始状态 + Future _rollbackToOriginalState() async { + Logger().d('开始回滚到原始状态'); + + // 回滚内存中的状态 + userInfo.update((user) { + if (user != null) { + user.name = _originalName; + user.avatar = _originalAvatar; + } + }); + localAvatar.value = _originalLocalAvatar; + + // 重置编辑状态 + hasEdits.value = false; + + // 关键修复:回滚后立即同步到本地存储 + try { + await _syncLocalStorage(); + Logger().d('回滚状态已同步到本地存储'); + } catch (e) { + Logger().e('回滚状态同步到本地存储失败: $e'); } + + Logger().d('已回滚到原始状态 - 名称: $_originalName, 头像: $_originalAvatar, 本地头像: $_originalLocalAvatar'); + } + + // 开始编辑昵称(内联编辑) + void startEditingNickname() { + isEditingNickname.value = true; + Logger().d('开始内联编辑昵称'); + } + + // 完成编辑昵称(内联编辑) + void finishEditingNickname(String newName) { + isEditingNickname.value = false; + + if (newName.trim().isNotEmpty && newName.trim() != _originalName) { + // 乐观更新 + userInfo.update((user) { + if (user != null) { + user.name = newName.trim(); + } + }); + hasEdits.value = true; + Logger().d('昵称内联编辑完成,新名称: ${newName.trim()}'); + } else { + Logger().d('昵称内联编辑取消或无变化'); + } + } + + // 取消编辑昵称(内联编辑) + void cancelEditingNickname() { + isEditingNickname.value = false; + Logger().d('取消内联编辑昵称'); } // 下载并保存远程头像 @@ -273,6 +454,29 @@ class ProfileController extends GetxController { Future saveUserInfo() async { await _storage.write(userKey, userInfo.value.toJson()); + Logger().d('用户信息已保存到本地存储'); + } + + // 同步所有相关状态到本地存储 + Future _syncLocalStorage() async { + try { + // 保存用户信息 + await saveUserInfo(); + + // 同步本地头像路径 + if (localAvatar.value.isNotEmpty) { + await _storage.write('local_avatar', localAvatar.value); + Logger().d('本地头像路径已保存: ${localAvatar.value}'); + } else { + // 如果本地头像为空,清除存储中的路径 + await _storage.remove('local_avatar'); + Logger().d('本地头像路径已清除'); + } + + Logger().d('所有状态已同步到本地存储'); + } catch (e) { + Logger().e('同步本地存储失败: $e'); + } } // 更新用户头像 @@ -285,82 +489,152 @@ class ProfileController extends GetxController { ); if (image != null) { - // 检查文件大小 - final file = File(image.path); - var fileSize = await file.length(); - var quality = 85; - - // 如果文件大于1MB,进行压缩 - while (fileSize > 1024 * 1024 && quality > 30) { - final compressedImage = await FlutterImageCompress.compressAndGetFile( - file.absolute.path, - file.absolute.path, // 覆盖原文件 - quality: quality, - minWidth: 800, - minHeight: 800, - ); + try { + // 保存当前状态用于可能的回滚 + _saveOriginalState(); + + // 检查文件大小 + final file = File(image.path); + var fileSize = await file.length(); + var quality = 85; + + // 如果文件大于1MB,进行压缩 + while (fileSize > 1024 * 1024 && quality > 30) { + final compressedImage = await FlutterImageCompress.compressAndGetFile( + file.absolute.path, + file.absolute.path, // 覆盖原文件 + quality: quality, + minWidth: 800, + minHeight: 800, + ); + + if (compressedImage == null) { + Get.snackbar('错误', '图片压缩失败'); + return; + } - if (compressedImage == null) { - Get.snackbar('错误', '图片压缩失败'); - return; + //更新文件大小和质量 + fileSize = await compressedImage.length(); + quality -= 5; + Logger().d('压缩后文件大小:$fileSize'); } - //更新文件大小和质量 - fileSize = await compressedImage.length(); - quality -= 5; - Logger().d('压缩后文件大小:$fileSize'); - } - - userInfo.update((user) { - if (user != null) { - user.avatar = image.path; - } - }); + // 乐观更新:立即更新UI + userInfo.update((user) { + if (user != null) { + user.avatar = image.path; + } + }); - // 更新本地头像路径 - localAvatar.value = image.path; - await _storage.write('local_avatar', image.path); - // await saveUserInfo(); //保存到本地 - //await _syncWithBackend(); //同步到后端 - - // 通知 SettingsController 更新 - // if (Get.isRegistered()) { - // Get.find().loadUserInfo(); - // } - hasEdits.value = true; //表示有编辑 + // 更新本地头像路径 + localAvatar.value = image.path; + await _storage.write('local_avatar', image.path); + hasEdits.value = true; //表示有编辑 + + Logger().d('头像已乐观更新,等待用户保存'); + } catch (e) { + Logger().e('更新头像失败: $e'); + final errorMessage = _getErrorMessage(e); + Get.snackbar('头像更新失败', errorMessage, + duration: const Duration(seconds: 3), + snackPosition: SnackPosition.TOP, + ); + // 发生错误时回滚状态 + await _rollbackToOriginalState(); + } } } - // 更新用户名 + // 更新用户名和头像(保存操作) Future updateUserNameAndAvatar(String name) async { if (name.trim().isEmpty) { Logger().e('无效名称: $name'); + Get.snackbar('错误', '用户名不能为空'); return; } + // 显示加载状态 + isUpdating.value = true; + try { - userInfo.update((user) { - if (user != null) { - user.name = name.trim(); - Logger().d('名称更新为: ${user.name}'); - } - }); + Logger().d('开始保存用户信息 - 名称: ${name.trim()}, 本地头像: ${localAvatar.value}'); - //await saveUserInfo(); //保存到本地 - await _syncWithBackend(); //同步到后端 + // 同步到后端 + await _syncWithBackend(); - //同步更新 SettingsController + // 同步成功后更新原始状态和本地存储 + _saveOriginalState(); + await _syncLocalStorage(); + + // 同步更新 SettingsController if (Get.isRegistered()) { final settingsController = Get.find(); settingsController.userInfo.value.name = userInfo.value.name; settingsController.userInfo.value.avatar = '${userInfo.value.avatar}?time=${DateTime.now().millisecondsSinceEpoch}'; - //settingsController.loadUserInfo(); settingsController.userInfo.refresh(); } + + // 重置编辑状态 + hasEdits.value = false; + + Logger().d('用户信息保存成功'); + + // 显示成功提示 + Get.snackbar( + '保存成功', + '个人信息已更新', + duration: const Duration(seconds: 3), + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.green.withOpacity(0.1), + colorText: Colors.green.shade700, + icon: Icon( + Icons.check_circle_outline, + color: Colors.green.shade700, + ), + margin: const EdgeInsets.all(16), + borderRadius: 8, + isDismissible: true, + forwardAnimationCurve: Curves.easeOutBack, + ); + + Logger().d('成功提示已显示'); + + // 备用成功提示机制 - 使用ScaffoldMessenger确保用户看到 + Future.delayed(const Duration(milliseconds: 100), () { + if (Get.context != null) { + ScaffoldMessenger.of(Get.context!).showSnackBar( + const SnackBar( + content: Row( + children: [ + Icon(Icons.check_circle, color: Colors.white), + SizedBox(width: 8), + Text('个人信息已更新'), + ], + ), + backgroundColor: Colors.green, + duration: Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + ), + ); + } + }); + } catch (e) { Logger().e('更新用户名和头像错误: $e'); - Get.snackbar('错误', '更新用户名和头像失败'); + + // 同步失败时回滚到原始状态 + await _rollbackToOriginalState(); + + // 显示错误提示 + final errorMessage = _getErrorMessage(e); + Get.snackbar('保存失败', errorMessage, + duration: const Duration(seconds: 4), + snackPosition: SnackPosition.TOP, + ); + } finally { + // 隐藏加载状态 + isUpdating.value = false; } } diff --git a/lib/modules/profile/views/profile_view.dart b/lib/modules/profile/views/profile_view.dart index 678135fd6..76ed99bea 100644 --- a/lib/modules/profile/views/profile_view.dart +++ b/lib/modules/profile/views/profile_view.dart @@ -170,22 +170,46 @@ class EditProfileView extends GetView { actions: [ Obx(() => Visibility( visible: controller.hasEdits.value, - child: TextButton( - onPressed: () { - // 保存用户名 - controller.updateUserNameAndAvatar(nameController.text); - // 返回上一级页面 - Get.back(); - }, - child: Text( - 'save'.tr, - style: TextStyle( - fontSize: 16.sp, - color: isDarkMode ? Colors.blue[300] : Colors.blue, - fontWeight: FontWeight.w500, - ), - ), - ), + child: controller.isUpdating.value + ? Padding( + padding: EdgeInsets.all(16.w), + child: SizedBox( + width: 20.w, + height: 20.w, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + isDarkMode ? Colors.blue[300]! : Colors.blue, + ), + ), + ), + ) + : TextButton( + onPressed: () async { + // 保存用户名和头像 + await controller + .updateUserNameAndAvatar(nameController.text); + // 如果保存成功(没有编辑状态),延迟返回上一级页面让用户看到成功提示 + if (!controller.hasEdits.value) { + // 延迟跳转,让用户看到成功提示 + Future.delayed(const Duration(milliseconds: 500), + () { + if (Get.isRegistered()) { + Get.back(); + } + }); + } + }, + child: Text( + 'save'.tr, + style: TextStyle( + fontSize: 16.sp, + color: + isDarkMode ? Colors.blue[300] : Colors.blue, + fontWeight: FontWeight.w500, + ), + ), + ), )) ], ), @@ -341,41 +365,54 @@ class EditProfileView extends GetView { ); } - // 新增:构建昵称设置项 + // 新增:构建昵称设置项(双模式内联编辑) Widget _buildNicknameSetting(BuildContext context, bool isDarkMode, TextEditingController nameController) { + return Obx(() { + // 根据编辑状态显示不同的UI + if (controller.isEditingNickname.value) { + return _buildNicknameEditingMode(context, isDarkMode, nameController); + } else { + return _buildNicknameDisplayMode(context, isDarkMode, nameController); + } + }); + } + + // 构建昵称显示模式 + Widget _buildNicknameDisplayMode(BuildContext context, bool isDarkMode, + TextEditingController nameController) { return InkWell( - onTap: () => _showEditNicknameDialog(context, isDarkMode, nameController), + key: Key('nickname_edit_button'), // 添加测试Key + onTap: () { + // 开始内联编辑 + nameController.text = controller.userInfo.value.name; + controller.startEditingNickname(); + }, child: Padding( - padding: - EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), // 调整垂直间距 + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), child: Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, // 移除 children: [ - // --- 添加图标 --- + // 图标 Container( width: 32.w, height: 32.w, decoration: BoxDecoration( color: isDarkMode - ? Colors.green[900]!.withOpacity(0.3) // 使用示例颜色 - : Colors.green[50]!, // 使用示例颜色 - borderRadius: BorderRadius.circular(16.r), // 图标背景圆角 + ? Colors.green[900]!.withOpacity(0.3) + : Colors.green[50]!, + borderRadius: BorderRadius.circular(16.r), ), child: Icon( - Icons.edit_outlined, // 编辑图标 + Icons.edit_outlined, size: 18.w, - color: isDarkMode - ? Colors.green[300]! - : Colors.green[600]!, // 图标颜色 + color: isDarkMode ? Colors.green[300]! : Colors.green[600]!, ), ), - SizedBox(width: 12.w), // 图标和文字间距 - // --- --- + SizedBox(width: 12.w), + // 标签 Expanded( - // 让文字占据剩余空间 child: Text( - 'nickname'.tr, // 名字 + 'nickname'.tr, style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w500, @@ -383,14 +420,13 @@ class EditProfileView extends GetView { ), ), ), - // --- 右侧内容保持不变 --- + // 右侧内容 Row( children: [ - // 使用 Obx 监听 userInfo 的变化来显示昵称 Obx(() => Text( controller.userInfo.value.name.isNotEmpty ? controller.userInfo.value.name - : 'notSet'.tr, // 未设置 + : 'notSet'.tr, style: TextStyle( fontSize: 14.sp, color: isDarkMode ? Colors.white70 : Colors.grey[600], @@ -408,87 +444,111 @@ class EditProfileView extends GetView { borderRadius: BorderRadius.circular(16.r), ), child: Icon( - Icons.chevron_right, + Icons.edit, size: 16.w, color: isDarkMode ? Colors.white70 : Colors.grey[500], ), ), ], ), - // --- --- ], ), ), ); } - // 新增:显示编辑昵称对话框的方法 - void _showEditNicknameDialog(BuildContext context, bool isDarkMode, + // 构建昵称编辑模式 + Widget _buildNicknameEditingMode(BuildContext context, bool isDarkMode, TextEditingController nameController) { - // 创建一个临时的 Controller,用于对话框内的编辑,避免直接修改主 nameController - final dialogNameController = - TextEditingController(text: nameController.text); - - Get.dialog( - AlertDialog( - backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, - title: Text( - 'editNickname'.tr, // 编辑昵称 - style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87), - ), - content: TextField( - controller: dialogNameController, // 使用对话框的 Controller - autofocus: true, - decoration: InputDecoration( - hintText: 'enterNickname'.tr, // 请输入昵称 - hintStyle: - TextStyle(color: isDarkMode ? Colors.white54 : Colors.black38), - // 可以添加其他样式,如边框 - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.r), - borderSide: BorderSide( - color: isDarkMode ? Colors.white38 : Colors.grey[400]!), + return Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), // 与显示模式保持一致的padding + child: Row( + children: [ + // 图标 - 保持与显示模式相同的绿色主题 + Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.green[900]!.withOpacity(0.3) + : Colors.green[50]!, + borderRadius: BorderRadius.circular(16.r), ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.r), - borderSide: BorderSide( - color: isDarkMode ? Colors.white38 : Colors.grey[400]!), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.r), - borderSide: BorderSide( - color: isDarkMode ? Colors.blue[300]! : Colors.blue), + child: Icon( + Icons.edit_outlined, // 保持与显示模式相同的图标 + size: 18.w, + color: isDarkMode ? Colors.green[300]! : Colors.green[600]!, ), ), - style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87), - ), - actions: [ - TextButton( - onPressed: () => Get.back(), + SizedBox(width: 12.w), + // 标签 - 保持与显示模式相同的布局 + Expanded( child: Text( - 'cancel'.tr, + 'nickname'.tr, style: TextStyle( - color: isDarkMode ? Colors.white70 : Colors.black54), + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.white : Colors.black87, + ), ), ), - TextButton( - onPressed: () { - // 确认时,将对话框输入的值更新回主 nameController - nameController.text = dialogNameController.text; - controller.userInfo.update((user) { - if (user != null) { - user.name = dialogNameController.text; - } - }); - controller.hasEdits.value = true; // 标记已编辑 - Get.back(); // 关闭对话框 - // 注意:实际的保存操作由 AppBar 中的保存按钮触发 - }, - child: Text( - 'confirm'.tr, - style: - TextStyle(color: isDarkMode ? Colors.blue[300] : Colors.blue), - ), + // 右侧内容区域 - 与显示模式布局保持一致 + Row( + children: [ + // 编辑输入框 - 替换原来的昵称文本显示区域 + Container( + constraints: BoxConstraints( + minWidth: 80.w, // 确保有足够的编辑空间 + maxWidth: 120.w, // 限制最大宽度保持布局稳定 + ), + child: TextField( + controller: nameController, + autofocus: true, + textAlign: TextAlign.right, // 右对齐,与原昵称显示保持一致 + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[600], + ), + decoration: InputDecoration( + hintText: 'enterNickname'.tr, + hintStyle: TextStyle( + color: isDarkMode ? Colors.white54 : Colors.grey[500], + fontSize: 14.sp, + ), + border: InputBorder.none, // 移除边框,保持简洁 + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + contentPadding: EdgeInsets.zero, // 移除内边距 + isDense: true, + ), + onSubmitted: (value) { + // 按回车确认编辑 + controller.finishEditingNickname(value); + }, + onTapOutside: (event) { + // 点击外部区域确认编辑 + controller.finishEditingNickname(nameController.text); + }, + ), + ), + SizedBox(width: 8.w), + // 右侧箭头图标 - 保持与显示模式完全相同 + Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.white.withOpacity(0.05) + : Colors.grey[100], + borderRadius: BorderRadius.circular(16.r), + ), + child: Icon( + Icons.chevron_right, + size: 16.w, + color: isDarkMode ? Colors.white70 : Colors.grey[500], + ), + ), + ], ), ], ), diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt index 0da045011..29246eabf 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt @@ -11,13 +11,16 @@ object BleConst { val PRIMARY_SERVICE_UUID: UUID = UUID.fromString("0000abc0-0000-1111-2222-123456789abc") /** 音频服务UUID - 文档中定义为00001801-0000-1000-8000-00805f9b34fb */ - val AUDIO_SERVICE_UUID: UUID = UUID.fromString("0000ae00-0000-1000-8000-00805f9b34fb") + val AUDIO_SERVICE_UUID: UUID = UUID.fromString("0000ABC0-0001-1111-2222-123456789ABC") /** 接收音频特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */ - val RECEIVE_AUDIO_CHAR_UUID: UUID = UUID.fromString("0000ae02-0000-1000-8000-00805f9b34fb") + val RECEIVE_AUDIO_CHAR_UUID: UUID = UUID.fromString("0000ABC2-0001-1111-2222-123456789ABC") /** 写入特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */ val WRITE_CHAR_UUID: UUID = UUID.fromString("0000abc1-0000-1111-2222-123456789abc") + + /** 音频写入特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */ + val AUDIO_WRITE_CHAR_UUID: UUID = UUID.fromString("0000ABC1-0001-1111-2222-123456789ABC") /** 通知特征UUID - 文档中定义为0000ABC2-0000-1111-2222-123456789ABC */ val NOTIFY_CHAR_UUID: UUID = UUID.fromString("0000abc2-0000-1111-2222-123456789abc")