Browse Source

Merge branch 'new_dev' of https://github.com/deepcloud2048/deep_voice into new_dev

newdev_shunjiawei
liwei1dao 1 year ago
parent
commit
e8ec27f4ec
  1. 7
      lib/core/bindings/initial_binding.dart
  2. 94
      lib/core/widgets/message_bubble.dart
  3. 135
      lib/data/services/chat_history_service.dart
  4. 52
      lib/data/services/music_manager.dart
  5. 8
      lib/data/services/version_update_service.dart
  6. 343
      lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart
  7. 466
      lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart
  8. 221
      lib/modules/agent/controllers/agent_controller.dart
  9. 32
      lib/modules/agent/views/circular_progress_button.dart
  10. 10
      lib/modules/chat/bindings/chat_binding.dart
  11. 992
      lib/modules/chat/controllers/chat_controller.dart
  12. 64
      lib/modules/chat/models/message_model.dart
  13. 69
      lib/modules/chat/views/animated_gradient_background.dart
  14. 100
      lib/modules/chat/views/chat_music_card.dart
  15. 208
      lib/modules/chat/views/chat_navigation_card.dart
  16. 820
      lib/modules/chat/views/chat_view.dart
  17. 144
      lib/modules/chat/views/cloud_background.dart
  18. 92
      lib/modules/chat/views/weather_day_card.dart
  19. 12
      lib/modules/login/controllers/login_controller.dart
  20. 141
      lib/modules/meeting/views/ai_bottom_sheet.dart
  21. 18
      lib/modules/meeting/views/overview_tab.dart
  22. 19
      lib/modules/meeting/views/speech_tab.dart
  23. 18
      lib/modules/meeting/views/summary_tab.dart
  24. 5
      lib/modules/music/controllers/music_playlist_controller.dart
  25. 651
      lib/modules/music/views/music_playlist_view.dart
  26. 19
      lib/modules/pairing/controllers/pairing_controller.dart
  27. 21
      lib/modules/pairing/views/pairing_view.dart
  28. 396
      lib/modules/profile/controllers/profile_controller.dart
  29. 254
      lib/modules/profile/views/profile_view.dart
  30. 44
      lib/modules/settings/views/settings_view.dart
  31. 28
      lib/routes/app_pages.dart
  32. 14
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt
  33. 20
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  34. 122
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt
  35. 25
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt
  36. 156
      local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift
  37. 62
      local_plugins/music_service/lib/music_service.dart
  38. 2
      local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/OTAManager.kt
  39. 10
      pubspec.yaml

7
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>(() => SpeechFactory(), fenix: true);
Get.find<SpeechFactory>().initialize(initialType: SpeechServiceType.azure);
// 历史记录服务
Get.lazyPut<ChatHistoryService>(() => ChatHistoryService(), fenix: true);
// 火山翻译服务
Get.lazyPut<VolcanoTranslationService>(() => VolcanoTranslationService(),
fenix: true);
@ -48,7 +44,8 @@ class InitialBinding extends Bindings {
Get.lazyPut<AccountService>(() => AccountService(), fenix: true);
// 版本更新服务
Get.lazyPut<VersionUpdateService>(() => VersionUpdateService(), fenix: true);
Get.lazyPut<VersionUpdateService>(() => VersionUpdateService(),
fenix: true);
// 杰理蓝牙服务

94
lib/core/widgets/message_bubble.dart

@ -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<Color>(
isUser ? Colors.white : AppColors.primary,
),
),
),
SizedBox(width: 8.w),
Text(
'思考中...',
style: TextStyle(
color: isUser ? Colors.white : Colors.black,
fontSize: 12.sp,
),
),
],
);
}
}

135
lib/data/services/chat_history_service.dart

@ -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<void> saveHistory(String agentId, List<Message> 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<void> _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<int> 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<List<Message>> 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<void> clearHistory(String agentId) async {
try {
await ChatStorage.deleteMessages(sessionId: agentId);
Logger.info('已清除聊天记录,代理: $agentId');
} catch (e) {
Logger.error('清除聊天记录失败', e);
}
}
/// 清除所有聊天记录
Future<void> clearAllHistory() async {
try {
await ChatStorage.clearDatabase();
Logger.info('已清除所有聊天记录');
} catch (e) {
Logger.error('清除所有聊天记录失败', e);
}
}
}

52
lib/data/services/music_manager.dart

@ -85,4 +85,56 @@ class MusicManager extends GetxService {
Future<void> playPlaylist(List<Map<String, dynamic>> playlist) async {
await _musicService.playMusicPlaylist(playlist);
}
/// 更新媒体会话进度信息
Future<void> updateMediaSessionProgress(
int currentPosition, int duration, double progress) async {
try {
await _musicService.updateMediaSessionProgress(
currentPosition, duration, progress);
} catch (e) {
Logger.d('MusicManager', '更新媒体会话进度失败: $e');
}
}
/// 设置媒体会话回调
Future<void> 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<void> updateMediaSessionMetadata(Map<String, dynamic> song) async {
try {
await _musicService.updateMediaSessionMetadata(song);
} catch (e) {
Logger.d('MusicManager', '更新媒体会话元数据失败: $e');
}
}
/// 更新媒体会话播放状态
Future<void> updateMediaSessionPlaybackState(bool isPlaying) async {
try {
await _musicService.updateMediaSessionPlaybackState(isPlaying);
} catch (e) {
Logger.d('MusicManager', '更新媒体会话播放状态失败: $e');
}
}
}

8
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]);

343
lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart

@ -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<VoiceInteractionEvent>? _eventStreamController;
Stream<VoiceInteractionEvent>? _eventStream;
@override
Stream<VoiceInteractionEvent> get eventStream => _eventStream ?? Stream.empty();
// 事件通道状态
StreamSubscription? _eventSubscription;
// 配置信息
late String _azureSpeechKey;
late String _azureSpeechRegion;
late String _volcanoAiApiKey;
// 初始化状态标志
static bool _isInitialized = false;
// 当前会话历史
final List<Map<String, String>> _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<VoiceInteractionEvent>.broadcast();
_eventStream = _eventStreamController?.stream;
}
/// 初始化服务
@override
Future<AndroidVoiceInteractionService> 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<void> _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<bool> _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<bool> _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<bool> 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<dynamic, dynamic> 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<void> _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<ChatHistoryService>();
// 加载现有历史记录
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<Map<String, String>> getMessageHistory() {
return List<Map<String, String>>.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);
}

466
lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart

@ -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<AsrService>();
final TtsService _ttsService = Get.find<TtsService>();
final VolcanoAIService _aiService = Get.find<VolcanoAIService>();
final ChatHistoryService _chatHistoryService = Get.find<ChatHistoryService>();
// 当前状态
final Rx<VoiceInteractionState> state = VoiceInteractionState.idle.obs;
// 是否识别到用户语音
final RxBool isSpeechDetected = false.obs;
// 是否正在处理AI响应
final RxBool isProcessingAI = false.obs;
// 是否正在播放TTS
final RxBool isSpeaking = false.obs;
// 对话历史记录
final List<Map<String, String>> _messageHistory = [];
// 最大历史记录数
static const int _maxHistorySize = 10;
// 超时计时器
Timer? _inactivityTimer;
// 流订阅
StreamSubscription? _recognitionSubscription;
// 当前识别的文本
String _currentRecognizedText = '';
// 音频输入源
bool _audioSourceType = false;
// 事件流控制器
final StreamController<VoiceInteractionEvent> _eventStreamController =
StreamController<VoiceInteractionEvent>.broadcast();
// 获取事件流
@override
Stream<VoiceInteractionEvent> get eventStream =>
_eventStreamController.stream;
// 标识符,用于存储聊天历史
final String _agentId = 'ios_assistant';
/// 初始化服务
@override
Future<IosVoiceInteractionService> 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<void> _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<void> _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<Map<String, String>> _buildMessageHistory() {
return List<Map<String, String>>.from(_messageHistory);
}
// 添加消息到历史记录
void _addToHistory(Map<String, String> 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<Map<String, String>> getMessageHistory() {
return List<Map<String, String>>.from(_messageHistory);
}
// 获取当前服务ID
@override
String getServiceId() {
return _agentId;
}
// 释放资源
@override
void onClose() {
_cancelInactivityTimer();
_recognitionSubscription?.cancel();
_resetToIdle();
_eventStreamController.close();
super.onClose();
}
// 保存聊天历史
Future<void> _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('已发送识别开始事件');
}
}

221
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 = <Map<String, dynamic>>[].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<String, dynamic>;
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<void> _updateMediaSessionProgress() async {
try {
// 这里将调用music_service插件的方法来更新系统媒体会话进度
// 具体实现将在music_service插件中添加
await _musiceManager.updateMediaSessionProgress(
currentPosition.value,
duration.value,
progress.value,
);
} catch (e) {
// 忽略错误,避免影响正常播放
Logger.d('AgentController', '更新媒体会话进度失败: $e');
}
}
/// 更新系统媒体会话的元数据
Future<void> _updateMediaSessionMetadata(Map<String, dynamic> song) async {
try {
await _musiceManager.updateMediaSessionMetadata(song);
} catch (e) {
Logger.d('AgentController', '更新媒体会话元数据失败: $e');
}
}
/// 更新系统媒体会话的播放状态
Future<void> _updateMediaSessionPlaybackState(bool isPlaying) async {
try {
await _musiceManager.updateMediaSessionPlaybackState(isPlaying);
} catch (e) {
Logger.d('AgentController', '更新媒体会话播放状态失败: $e');
}
}
// 跳转到指定位置
Future<void> 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<void> 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<void> 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<Map<String, dynamic>> 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<void> onMediaSessionPlay() async {
Logger.d(TAG, '收到媒体会话播放回调');
if (!isPlaying.value) {
await musicResume();
}
}
/// 处理来自系统通知栏的暂停操作
Future<void> onMediaSessionPause() async {
Logger.d(TAG, '收到媒体会话暂停回调');
if (isPlaying.value) {
await musicPause();
}
}
/// 处理来自系统通知栏的下一曲操作
Future<void> onMediaSessionNext() async {
Logger.d(TAG, '收到媒体会话下一曲回调');
await playNext();
}
/// 处理来自系统通知栏的上一曲操作
Future<void> onMediaSessionPrevious() async {
Logger.d(TAG, '收到媒体会话上一曲回调');
await playPrevious();
}
/// 处理来自系统通知栏的跳转操作
Future<void> onMediaSessionSeekTo(int positionMs) async {
Logger.d(TAG, '收到媒体会话跳转回调: ${positionMs}ms');
await seekToPosition(positionMs);
}
/// 处理来自系统通知栏的停止操作
Future<void> onMediaSessionStop() async {
Logger.d(TAG, '收到媒体会话停止回调');
await musicPause();
// 重置播放状态
currentPosition.value = 0;
progress.value = 0.0;
_stopProgressTimer();
}
}

32
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,

10
lib/modules/chat/bindings/chat_binding.dart

@ -1,10 +0,0 @@
import 'package:get/get.dart';
import '../controllers/chat_controller.dart';
class ChatBinding extends Bindings {
@override
void dependencies() {
// 注册控制器
Get.lazyPut<ChatController>(() => ChatController());
}
}

992
lib/modules/chat/controllers/chat_controller.dart

@ -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<AsrService>();
late SpotifService _spotifService;
// 添加ScrollController用于控制列表滚动
final ScrollController scrollController = ScrollController();
StreamSubscription? _locationStream;
String _latLng = '';
String _city = '深圳市';
// Agent 信息
late final Agent agent;
// 消息列表
final RxList<Message> messages = <Message>[].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<void> onInit() async {
super.onInit();
Logger.info('ChatController onInit');
// 从路由参数获取Agent信息
final Map<String, dynamic> 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<KouziAIService>();
break;
case 'health_advisor':
// 多功能AI聊天
Get.put(DeapsoundAIService());
_aiService = Get.find<DeapsoundAIService>();
break;
default:
// 默认使用OpenAIServiceAdapter
_aiService = Get.find<OpenAIServiceAdapter>();
Logger.info('使用OpenAIServiceAdapter');
}
Get.put(SpotifService());
_spotifService = Get.find<SpotifService>();
// 使用克隆音色语音合成
final GetStorage storage = GetStorage();
String timbre = storage.read('timbre') ?? '1';
if (timbre == '1') {
_ttsService = Get.find<TtsService>();
} else {
Get.put(VoiceCloneTtsService());
_ttsService = Get.find<VoiceCloneTtsService>();
}
_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<void> 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<Map<String, String>> 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<Map<String, String>> _buildMessageHistory() {
final List<Map<String, String>> 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<void> 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<void> 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<void> _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<void> _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<void> _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);
}
}

64
lib/modules/chat/models/message_model.dart

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

69
lib/modules/chat/views/animated_gradient_background.dart

@ -1,69 +0,0 @@
import 'package:flutter/material.dart';
class AnimatedGradientBackground extends StatefulWidget {
final Widget child;
const AnimatedGradientBackground(this.child, {super.key});
@override
State<AnimatedGradientBackground> createState() =>
_AnimatedGradientBackgroundState();
}
class _AnimatedGradientBackgroundState extends State<AnimatedGradientBackground>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<Alignment?> _beginAnimation;
late Animation<Alignment?> _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,
);
}
}

100
lib/modules/chat/views/chat_music_card.dart

@ -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<ChatController> {
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,
),
),
),
),
],
);
}
}

208
lib/modules/chat/views/chat_navigation_card.dart

@ -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<ChatNavigationCard> createState() => _ChatNavigationCardState();
}
class _ChatNavigationCardState extends State<ChatNavigationCard> {
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<String> _getMapUrl() async {
// 1. 获取路径数据
final route = await _fetchRouteData();
// 2. 解析并简化路径
List<LatLng> simplified = _simplifyPath(route, epsilon: 0.0001);
// 3. 计算地图参数
final zoom = _calculateAutoZoom(simplified);
// 4. 生成静态地图URL
return _buildStaticMapUrl(simplified, zoom);
}
Future<List<LatLng>> _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<LatLng> _parsePolyline(List<dynamic> steps) {
final List<LatLng> 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<LatLng> _simplifyPath(List<LatLng> points, {double epsilon = 0.0001}) {
return douglasPeucker(points, epsilon);
}
int _calculateAutoZoom(List<LatLng> 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<LatLng> 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<LatLng> douglasPeucker(List<LatLng> 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); // 高德使用经度在前纬度在后的格式
}

820
lib/modules/chat/views/chat_view.dart

@ -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<ChatController> {
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';
}
}
}

144
lib/modules/chat/views/cloud_background.dart

@ -1,144 +0,0 @@
import 'dart:math';
import 'package:flutter/material.dart';
class CircularFlowAnimation extends StatefulWidget {
const CircularFlowAnimation({super.key});
@override
State<CircularFlowAnimation> createState() => _CircularFlowAnimationState();
}
class _CircularFlowAnimationState extends State<CircularFlowAnimation>
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<double> 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<Color> 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;
}

92
lib/modules/chat/views/weather_day_card.dart

@ -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,
),
),
],
),
],
),
],
);
}
}

12
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()");

141
lib/modules/meeting/views/ai_bottom_sheet.dart

@ -0,0 +1,141 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../controllers/meeting_details_controller.dart';
class AIBottomSheet extends StatefulWidget {
const AIBottomSheet({super.key});
@override
State<AIBottomSheet> createState() => _AIBottomSheetState();
}
class _AIBottomSheetState extends State<AIBottomSheet> {
final _controller = Get.find<MeetingDetailsController>();
List _dataList = [];
final List _cruxList = [
'添加代办事项',
'提取结论',
'生成关键指标',
];
@override
Widget build(BuildContext context) {
return Container(
height: 1.sh - 50.h,
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: Column(
children: [
Container(
padding: EdgeInsets.symmetric(vertical: 10.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
26.horizontalSpace,
Text(
'Ask AI',
style: TextStyle(
fontSize: 16.sp,
color: Colors.black87,
fontWeight: FontWeight.bold,
),
),
GestureDetector(
onTap: () => Get.back(),
child: Container(
width: 26.w,
height: 26.w,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(26.r),
),
child: Icon(
Icons.close,
size: 18.w,
color: Colors.grey[400],
),
),
)
],
),
),
Expanded(
child: _dataList.isNotEmpty ? SizedBox() : _empty(),
),
_inputBox(),
],
),
);
}
Widget _empty() {
return Padding(
padding: EdgeInsets.symmetric(vertical: 10.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'今天我能帮您做点什么呢?',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
),
),
Padding(
padding: EdgeInsets.only(top: 10.w, bottom: 30.w),
child: Row(
children: [
Icon(
Icons.audio_file_outlined,
size: 14.w,
color: Colors.grey[500],
),
2.horizontalSpace,
Text(
_controller.meetingData.value.title,
style: TextStyle(
color: Colors.grey[500],
fontSize: 12.sp,
),
),
],
),
),
...List.generate(
_cruxList.length,
(int index) => Container(
width: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 8.w),
margin: EdgeInsets.symmetric(vertical: 5.w),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(20.r),
border: Border.all(
color: Colors.grey[300]!,
),
),
child: Text(
_cruxList[index],
style: TextStyle(fontSize: 12.sp),
),
),
),
],
),
);
}
Widget _inputBox() {
return Container(
height: 40.w,
padding: EdgeInsets.symmetric(horizontal: 10.w),
decoration: BoxDecoration(),
child: Row(
children: [],
),
);
}
}

18
lib/modules/meeting/views/overview_tab.dart

@ -4,6 +4,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../controllers/meeting_details_controller.dart';
import 'ai_bottom_sheet.dart';
import 'generate_bottom_sheet.dart';
class OverviewTab extends StatefulWidget {
@ -94,6 +95,23 @@ class _OverviewTabState extends State<OverviewTab> {
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_controller.meetingDetails.value.tasktype >= 3)
_button(
Icons.auto_awesome,
'Ask AI',
() {
Get.bottomSheet(
const AIBottomSheet(),
isScrollControlled: true,
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
);
},
),
if (_controller.meetingDetails.value.tasktype == 0)
SizedBox(
width: 150.w,

19
lib/modules/meeting/views/speech_tab.dart

@ -5,6 +5,7 @@ import 'package:get/get.dart';
import 'package:lottie/lottie.dart';
import '../controllers/meeting_details_controller.dart';
import 'ai_bottom_sheet.dart';
import 'generate_bottom_sheet.dart';
import 'speaker_all_bottom_sheet.dart';
import 'speaker_bottom_sheet.dart';
@ -278,6 +279,24 @@ class SpeechTab extends GetView<MeetingDetailsController> {
);
},
),
if (controller.meetingDetails.value.tasktype >= 2 &&
!controller.isSpeakerText.value)
_button(
Icons.auto_awesome,
'Ask AI',
() {
Get.bottomSheet(
const AIBottomSheet(),
isScrollControlled: true,
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
);
},
),
if (controller.meetingDetails.value.tasktype == 0)
SizedBox(
width: 150.w,

18
lib/modules/meeting/views/summary_tab.dart

@ -6,6 +6,7 @@ import 'package:get/get.dart';
import '../../../data/services/db/sqflite_api.dart';
import '../controllers/meeting_details_controller.dart';
import 'ai_bottom_sheet.dart';
import 'generate_bottom_sheet.dart';
import 'speaker_all_bottom_sheet.dart';
@ -310,6 +311,23 @@ class _SummaryTabState extends State<SummaryTab> {
);
},
),
if (_controller.meetingDetails.value.tasktype >= 3)
_button(
Icons.auto_awesome,
'Ask AI',
() {
Get.bottomSheet(
const AIBottomSheet(),
isScrollControlled: true,
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
);
},
),
if (_controller.meetingDetails.value.tasktype == 0)
SizedBox(
width: 150.w,

5
lib/modules/music/controllers/music_playlist_controller.dart

@ -12,8 +12,5 @@ class MusicPlaylistController extends GetxController {
playlist.assignAll(playlistData);
}
@override
void onClose() {
super.onClose();
}
}

651
lib/modules/music/views/music_playlist_view.dart

@ -1,13 +1,16 @@
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);
@override
Widget build(BuildContext context) {
ScreenUtil.init(context, designSize: const Size(375, 812)); // 添加设计尺寸参考
final controller = Get.find<AgentController>();
final List<Map<String, dynamic>> playlist = Get.arguments ?? [];
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
@ -18,7 +21,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,
@ -31,43 +33,180 @@ 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],
),
),
],
),
)
: 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: [
// 上部:当前播放音乐详情 - 响应式高度
Container(
height: MediaQuery.of(context).size.height * 0.38, // 增大高度比例
child: _buildCurrentPlayingSection(
controller, playlist, isDarkMode),
),
// 中部:播放全部按钮
_buildPlayAllSection(controller, playlist, isDarkMode),
// 下部:歌单列表 - 使用剩余空间
Expanded(
child:
_buildPlaylistSection(controller, playlist, isDarkMode),
),
],
),
);
}
// 构建当前播放音乐详情部分
Widget _buildCurrentPlayingSection(AgentController controller,
List<Map<String, dynamic>> playlist, bool isDarkMode) {
return SingleChildScrollView(
// 添加滚动支持
child: Obx(() {
// 查找当前播放的歌曲
Map<String, dynamic>? 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: 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'] ?? '';
return Container(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 12.h),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 封面图片 - 响应式尺寸
_buildCoverImage(coverUrl, isDarkMode, size: 110.w),
SizedBox(height: 12.h),
// 歌曲信息
_buildSongInfo(title, artist, isDarkMode),
SizedBox(height: 16.h),
// 进度条
_buildProgressBar(controller, isDarkMode),
SizedBox(height: 15.h),
// 播放控制按钮
_buildPlayControls(controller, playlist, isDarkMode),
],
),
);
}),
);
}
// 构建播放全部按钮部分
Widget _buildPlayAllSection(AgentController controller,
List<Map<String, dynamic>> playlist, bool isDarkMode) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 12.h),
child: SizedBox(
width: double.infinity,
height: 48.h,
child: ElevatedButton.icon(
onPressed: () async {
if (playlist.isNotEmpty) {
await controller.playPlaylist(playlist);
}
},
icon: Icon(Icons.play_arrow, color: Colors.white, size: 20.w),
label: Text(
'播放全部',
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(Get.context!).primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14.r),
),
elevation: 3,
shadowColor: Theme.of(Get.context!).primaryColor.withAlpha(102),
),
),
),
);
}
// 构建歌单列表部分
Widget _buildPlaylistSection(AgentController controller,
List<Map<String, dynamic>> playlist, bool isDarkMode) {
return ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 8.h),
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,
);
},
);
}
@ -89,123 +228,379 @@ class MusicPlaylistView extends StatelessWidget {
'coverUrl': image,
};
return GestureDetector(
onTap: () {
_playSelectedSong(controller, song, Get.arguments ?? []);
},
child: Container(
margin: EdgeInsets.only(bottom: 10.h),
padding: EdgeInsets.all(14.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.white,
borderRadius: BorderRadius.circular(14.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(isDarkMode ? 0.25 : 0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
// 封面图片
Container(
width: 52.w,
height: 52.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.r),
color: isDarkMode ? Colors.grey[700] : Colors.grey[200],
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(isDarkMode ? 0.2 : 0.08),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: image.isNotEmpty
? ClipRRect(
borderRadius: BorderRadius.circular(10.r),
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.w,
);
},
),
)
: Icon(
Icons.music_note,
color: isDarkMode ? Colors.grey[500] : Colors.grey[600],
size: 24.w,
),
),
SizedBox(width: 14.w),
// 歌曲信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
name,
style: TextStyle(
fontSize: 15.5.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black87,
height: 1.2,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 3.h),
Text(
singer,
style: TextStyle(
fontSize: 13.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
height: 1.1,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
SizedBox(width: 14.w),
// 播放按钮
Obx(() {
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.w,
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,
{required double size}) {
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.r),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: isDarkMode ? 0.2 : 0.03),
blurRadius: 3,
spreadRadius: 1,
offset: const Offset(0, 1),
color: Colors.black.withOpacity(isDarkMode ? 0.3 : 0.1),
blurRadius: 15,
spreadRadius: 2,
offset: const Offset(0, 6),
),
],
),
child: Row(
child: ClipRRect(
borderRadius: BorderRadius.circular(16.r),
child: coverUrl.isNotEmpty
? Image.network(
coverUrl,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return _buildDefaultCover(isDarkMode, size: size);
},
)
: _buildDefaultCover(isDarkMode, size: size),
),
);
}
// 构建默认封面
Widget _buildDefaultCover(bool isDarkMode, {required double size}) {
return Container(
color: isDarkMode ? Colors.grey[800] : Colors.grey[300],
child: Icon(
Icons.music_note,
size: size * 0.55,
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.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black87,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 6.h),
Text(
artist,
style: TextStyle(
fontSize: 14.sp,
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: 40.w,
child: Text(
_formatDuration(controller.currentPosition.value),
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
fontFeatures: const [FontFeature.tabularFigures()],
),
textAlign: TextAlign.left,
),
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: 8.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: SliderThemeData(
activeTrackColor: Theme.of(Get.context!).primaryColor,
inactiveTrackColor:
isDarkMode ? Colors.grey[700] : Colors.grey[300],
thumbColor: Theme.of(Get.context!).primaryColor,
overlayColor:
Theme.of(Get.context!).primaryColor.withOpacity(0.2),
trackHeight: 4.h,
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8),
overlayShape: const RoundSliderOverlayShape(overlayRadius: 14),
),
child: Slider(
value: controller.progress.value.clamp(0.0, 1.0),
onChanged: (value) {
final targetPosition =
(controller.duration.value * value).round();
controller.seekToPosition(targetPosition);
},
),
),
),
SizedBox(width: 8.w),
SizedBox(
width: 40.w,
child: Text(
_formatDuration(controller.duration.value),
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
fontFeatures: const [FontFeature.tabularFigures()],
),
textAlign: TextAlign.right,
),
),
],
);
});
}
// 构建播放控制按钮
Widget _buildPlayControls(AgentController controller,
List<Map<String, dynamic>> playlist, bool isDarkMode) {
return Obx(() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: () => _playPrevious(controller, playlist),
iconSize: 32.w,
padding: EdgeInsets.all(8.w), // 增大触摸区域
icon: Icon(
Icons.skip_previous,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
SizedBox(width: 16.w),
Container(
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.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.w,
color: Colors.white,
),
),
),
SizedBox(width: 16.w),
IconButton(
onPressed: () => _playNext(controller, playlist),
iconSize: 32.w,
padding: EdgeInsets.all(8.w), // 增大触摸区域
icon: Icon(
Icons.skip_next,
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,
);
}),
],
),
);
);
});
}
// ...(其他辅助方法保持不变,如_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<void> _togglePlayPause(AgentController controller) async {
if (controller.isPlaying.value) {
await controller.musicPause();
} else {
await controller.musicResume();
}
}
// 播放上一曲
void _playPrevious(
AgentController controller, List<Map<String, dynamic>> playlist) {
controller.updateCurrentPlaylistContext(
playlist, controller.currentId.value);
controller.playPrevious();
}
// 播放下一曲
void _playNext(
AgentController controller, List<Map<String, dynamic>> playlist) {
controller.updateCurrentPlaylistContext(
playlist, controller.currentId.value);
controller.playNext();
}
// 播放选中的歌曲
void _playSelectedSong(AgentController controller, Map<String, dynamic> song,
List<Map<String, dynamic>> playlist) {
final songId = int.tryParse(song['id']?.toString() ?? '0') ?? 0;
controller.updateCurrentPlaylistContext(playlist, songId);
controller.musicPlay(song);
}
}

19
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 = '配对成功!';
@ -556,6 +561,7 @@ class PairingController extends GetxController {
});
Logger.i(_tag, '手动授权结果 ${response}');
if (response != null) {
User.instance.earphonemac = currentDevice?.address ?? '';
_transition(PairingState.verifying);
_verifyDevice(_currentClassicDevice!);
} else {
@ -630,6 +636,15 @@ 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, '用户选择跳过配对');

21
lib/modules/pairing/views/pairing_view.dart

@ -436,6 +436,7 @@ class PairingView extends GetView<PairingController> {
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<PairingController> {
),
),
],
// 跳过绑定按钮
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,
),
),
),
],
],
),
);

396
lib/modules/profile/controllers/profile_controller.dart

@ -28,6 +28,13 @@ class ProfileController extends GetxController {
final Rx<User> 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<void> _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<String, dynamic>) {
// 检查响应是否包含错误
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<void> _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<void> saveUserInfo() async {
await _storage.write(userKey, userInfo.value.toJson());
Logger().d('用户信息已保存到本地存储');
}
// 同步所有相关状态到本地存储
Future<void> _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<SettingsController>()) {
// Get.find<SettingsController>().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<void> 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<SettingsController>()) {
final settingsController = Get.find<SettingsController>();
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;
}
}

254
lib/modules/profile/views/profile_view.dart

@ -170,22 +170,46 @@ class EditProfileView extends GetView<ProfileController> {
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<Color>(
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<ProfileController>()) {
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<ProfileController> {
);
}
// 新增:构建昵称设置项
// 新增:构建昵称设置项(双模式内联编辑)
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<ProfileController> {
),
),
),
// --- 右侧内容保持不变 ---
// 右侧内容
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<ProfileController> {
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],
),
),
],
),
],
),

44
lib/modules/settings/views/settings_view.dart

@ -569,28 +569,28 @@ class SettingsView extends GetView<SettingsController> {
// color: isDarkMode
// ? Colors.white.withOpacity(0.1)
// : Colors.grey[200]),
// // BLE测试
// _buildSimpleNavigationSetting(
// title: 'opus解码测试',
// subtitle: '测试opus解码',
// icon: Icons.bluetooth_searching,
// iconBgColor: isDarkMode
// ? Colors.green[900]!.withOpacity(0.3)
// : Colors.green[100]!,
// iconColor:
// isDarkMode ? Colors.green[300]! : Colors.green[600]!,
// titleColor: isDarkMode ? Colors.white : null,
// subtitleColor: isDarkMode ? Colors.white70 : null,
// onTap: () {
// Get.toNamed(Routes.opusTest);
// },
// isDarkMode: isDarkMode,
// ),
// Divider(
// height: 1,
// color: isDarkMode
// ? Colors.white.withOpacity(0.1)
// : Colors.grey[200]),
// BLE测试
_buildSimpleNavigationSetting(
title: 'opus解码测试',
subtitle: '测试opus解码',
icon: Icons.bluetooth_searching,
iconBgColor: isDarkMode
? Colors.green[900]!.withOpacity(0.3)
: Colors.green[100]!,
iconColor:
isDarkMode ? Colors.green[300]! : Colors.green[600]!,
titleColor: isDarkMode ? Colors.white : null,
subtitleColor: isDarkMode ? Colors.white70 : null,
onTap: () {
Get.toNamed(Routes.opusTest);
},
isDarkMode: isDarkMode,
),
Divider(
height: 1,
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
// BLE测试
// _buildSimpleNavigationSetting(

28
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(),

14
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt

@ -10,14 +10,24 @@ object BleConst {
/** 主服务UUID - 文档中定义为0000ABC0-0000-1111-2222-123456789ABC */
val PRIMARY_SERVICE_UUID: UUID = UUID.fromString("0000abc0-0000-1111-2222-123456789abc")
/** 音频服务UUID - 文档中定义为00001801-0000-1000-8000-00805f9b34fb */
/** 音频服务UUID - 文档中定义为0000ABC0-0001-1111-2222-123456789ABC */
val AUDIO_SERVICE_UUID1: UUID = UUID.fromString("0000ABC0-0001-1111-2222-123456789ABC")
/** 接收音频特征UUID - 文档中定义为0000ABC2-0001-1111-2222-123456789ABC */
val RECEIVE_AUDIO_CHAR_UUID1: UUID = UUID.fromString("0000ABC2-0001-1111-2222-123456789ABC")
/** 音频服务UUID - 文档中定义为00001801-0000-1000-8000-00805f9b34fb */
val AUDIO_SERVICE_UUID: UUID = UUID.fromString("0000ae00-0000-1000-8000-00805f9b34fb")
/** 接收音频特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */
/** 接收音频特征UUID - 文档中定义为0000ABC2-0001-1111-2222-123456789ABC */
val RECEIVE_AUDIO_CHAR_UUID: UUID = UUID.fromString("0000ae02-0000-1000-8000-00805f9b34fb")
/** 写入特征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")

20
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt

@ -72,6 +72,7 @@ object BleService {
private var notifyChar: BluetoothGattCharacteristic? = null
private var writeChar: BluetoothGattCharacteristic? = null
private var audioChar: BluetoothGattCharacteristic? = null
private var audioChar1: BluetoothGattCharacteristic? = null
var recordfile: RecordingFile? = null
// 扫描相关
@ -419,6 +420,7 @@ object BleService {
notifyChar = null
writeChar = null
audioChar = null
audioChar1 = null
}
}
@ -491,6 +493,10 @@ object BleService {
val audioSvc = g.getService(BleConst.AUDIO_SERVICE_UUID)
audioChar = audioSvc?.getCharacteristic(BleConst.RECEIVE_AUDIO_CHAR_UUID)
// 获取音频服务1特征
val audioSvc1 = g.getService(BleConst.AUDIO_SERVICE_UUID1)
audioChar1 = audioSvc1?.getCharacteristic(BleConst.RECEIVE_AUDIO_CHAR_UUID1)
if (notifyChar == null || writeChar == null) {
Log.e(TAG, "未找到主服务所需特征")
updateConnectionState(BleConst.STATE_ERROR)
@ -505,6 +511,12 @@ object BleService {
Log.i(TAG, "音频服务特征找到并设置通知")
} else {
Log.w(TAG, "音频服务特征未找到")
}
if (audioChar1 != null) {
setupNotifications(g, audioChar1)
Log.i(TAG, "音频服务特征找到并设置通知")
} else {
Log.w(TAG, "音频服务特征未找到")
}
// 设置主服务的通知
setupNotifications(g, notifyChar)
@ -518,10 +530,16 @@ object BleService {
// 根据特征UUID区分处理
when (c.uuid) {
// 音频特征数据
BleConst.RECEIVE_AUDIO_CHAR_UUID1 -> {
Log.i(TAG, "RECEIVE_AUDIO_CHAR_UUID1")
processAudioData(data)
}
// 音频特征数据
BleConst.RECEIVE_AUDIO_CHAR_UUID -> {
// Log.i(TAG, "收到音频特征数据")
Log.i(TAG, "RECEIVE_AUDIO_CHAR_UUID")
processAudioData(data)
}
// 通知特征数据(命令和控制)

122
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<String, Any>) {
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(

25
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<Int>("currentPosition") ?: 0
val duration = call.argument<Int>("duration") ?: 0
val progress = call.argument<Double>("progress") ?: 0.0
MusicService.updateMediaSessionProgress(currentPosition, duration, progress.toFloat())
result.success(null)
}
"enableMediaSessionCallbacks" -> {
MusicService.enableMediaSessionCallbacks(channel)
result.success(null)
}
"updateMediaSessionMetadata" -> {
val song = call.argument<Map<String, Any>>("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<Boolean>("isPlaying") ?: false
MusicService.updateMediaSessionPlaybackState(isPlaying)
result.success(null)
}
else -> result.notImplemented()
}
}

156
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
}
}
}

62
local_plugins/music_service/lib/music_service.dart

@ -79,4 +79,66 @@ class MusicService {
Future<void> playMusicPlaylist(List<Map<String, dynamic>> songs) async {
await _methodChannel.invokeMethod('playlist', {'songs': songs});
}
/// 更新媒体会话进度信息
Future<void> updateMediaSessionProgress(
int currentPosition, int duration, double progress) async {
await _methodChannel.invokeMethod('updateMediaSessionProgress', {
'currentPosition': currentPosition,
'duration': duration,
'progress': progress,
});
}
/// 设置媒体会话回调
Future<void> 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<void> updateMediaSessionMetadata(Map<String, dynamic> song) async {
await _methodChannel.invokeMethod('updateMediaSessionMetadata', {
'song': song,
});
}
/// 更新媒体会话播放状态
Future<void> updateMediaSessionPlaybackState(bool isPlaying) async {
await _methodChannel.invokeMethod('updateMediaSessionPlaybackState', {
'isPlaying': isPlaying,
});
}
}

2
local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/OTAManager.kt

@ -106,7 +106,7 @@ class OTAManager(context: Context) : BluetoothOTAManager(context) {
"是否启用设备认证流程(与固件工程师确认) >>> ${configHelper.isUseDeviceAuth()} >>> ${bluetoothOption.isUseReconnect} "
)
//是否启用设备认证流程(与固件工程师确认)
bluetoothOption.isUseAuthDevice = configHelper.isUseDeviceAuth()
bluetoothOption.isUseAuthDevice = false
//设置BLE的MTU
bluetoothOption.mtu = BluetoothConstant.BLE_MTU_MIN
//是否需要改变BLE的MTU

10
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

Loading…
Cancel
Save