Browse Source

refactor: 移除聊天相关代码并优化音乐播放界面

fix(pairing): 修复配对页面跳过绑定按钮逻辑

style: 统一pubspec.yaml中的引号格式

perf(music): 优化音乐播放列表的响应式布局

chore: 更新应用版本号至1.0.0+6
weicu
lxm 1 year ago
parent
commit
9937c8489f
  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. 8
      lib/data/services/version_update_service.dart
  5. 343
      lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart
  6. 466
      lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart
  7. 319
      lib/modules/music/views/music_playlist_view.dart
  8. 25
      lib/modules/pairing/controllers/pairing_controller.dart
  9. 21
      lib/modules/pairing/views/pairing_view.dart
  10. 28
      lib/routes/app_pages.dart
  11. 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);
}
}
}

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('已发送识别开始事件');
}
}

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

@ -10,6 +10,7 @@ class MusicPlaylistView extends StatelessWidget {
@override
Widget build(BuildContext context) {
ScreenUtil.init(context, designSize: const Size(375, 812)); // 添加设计尺寸参考
final controller = Get.find<AgentController>();
final List<Map<String, dynamic>> playlist = Get.arguments ?? [];
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
@ -32,14 +33,14 @@ class MusicPlaylistView extends StatelessWidget {
children: [
Icon(
Icons.music_off,
size: 64,
size: 64.w,
color: Colors.grey[400],
),
const SizedBox(height: 16),
SizedBox(height: 16.h),
Text(
'暂无音乐',
style: TextStyle(
fontSize: 16,
fontSize: 16.sp,
color: Colors.grey[600],
),
),
@ -48,10 +49,9 @@ class MusicPlaylistView extends StatelessWidget {
)
: Column(
children: [
// 上部:当前播放音乐详情 - 进一步优化高度占比
SizedBox(
height: MediaQuery.of(context).size.height *
0.35, // 从35%减少到32%,为进度条优化腾出空间
// 上部:当前播放音乐详情 - 响应式高度
Container(
height: MediaQuery.of(context).size.height * 0.38, // 增大高度比例
child: _buildCurrentPlayingSection(
controller, playlist, isDarkMode),
),
@ -70,105 +70,101 @@ class MusicPlaylistView extends StatelessWidget {
// 构建当前播放音乐详情部分
Widget _buildCurrentPlayingSection(AgentController controller,
List<Map<String, dynamic>> playlist, bool isDarkMode) {
return 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) {
// 如果没找到,使用第一首歌曲
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;
}
} else {
currentSong = playlist.isNotEmpty ? playlist.first : null;
}
if (currentSong == null) {
return Container(
padding: const EdgeInsets.all(16), // 减少padding
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.music_note,
size: 60, // 减小图标尺寸
color: Colors.grey[400],
),
const SizedBox(height: 12), // 减少间距
Text(
'暂无播放音乐',
style: TextStyle(
fontSize: 16, // 减小字体
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
if (currentSong == null) {
return Container(
padding: EdgeInsets.all(16.w),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.music_note,
size: 60.w,
color: Colors.grey[400],
),
),
],
SizedBox(height: 12.h),
Text(
'暂无播放音乐',
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
],
),
),
),
);
}
);
}
final title = currentSong['name'] ?? currentSong['title'] ?? '未知歌曲';
final artist = currentSong['singer'] ??
currentSong['sgener'] ??
currentSong['artist'] ??
'未知歌手';
final coverUrl = currentSong['image'] ?? currentSong['coverUrl'] ?? '';
final title = currentSong['name'] ?? currentSong['title'] ?? '未知歌曲';
final artist = currentSong['singer'] ??
currentSong['sgener'] ??
currentSong['artist'] ??
'未知歌手';
final coverUrl = currentSong['image'] ?? currentSong['coverUrl'] ?? '';
return SingleChildScrollView(
// 添加滚动支持
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 12), // 优化padding:水平增加,垂直减少
return Container(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 12.h),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 封面图片 - 进一步优化尺寸
_buildCoverImage(coverUrl, isDarkMode,
size: 110), // 从120减小到110,为进度条优化腾出空间
const SizedBox(height: 12), // 减少间距从16到14
// 封面图片 - 响应式尺寸
_buildCoverImage(coverUrl, isDarkMode, size: 110.w),
SizedBox(height: 12.h),
// 歌曲信息
_buildSongInfo(title, artist, isDarkMode),
const SizedBox(height: 16), // 增加歌曲信息和进度条之间的间距,提升视觉层次
// 进度条 - 现在是水平布局,需要更多关注
SizedBox(height: 16.h),
// 进度条
_buildProgressBar(controller, isDarkMode),
const SizedBox(height: 15), // 增加进度条和控制按钮之间的间距
SizedBox(height: 15.h),
// 播放控制按钮
_buildPlayControls(controller, playlist, isDarkMode),
],
),
),
);
});
);
}),
);
}
// 构建播放全部按钮部分
Widget _buildPlayAllSection(AgentController controller,
List<Map<String, dynamic>> playlist, bool isDarkMode) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 12), // 优化padding,与音乐详情区域保持一致
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 12.h),
child: SizedBox(
width: double.infinity,
height: 48, // 从50减小到48,更紧凑
height: 48.h,
child: ElevatedButton.icon(
onPressed: () async {
if (playlist.isNotEmpty) {
await controller.playPlaylist(playlist);
}
},
icon: const Icon(Icons.play_arrow,
color: Colors.white, size: 20), // 调整图标大小
label: const Text(
icon: Icon(Icons.play_arrow, color: Colors.white, size: 20.w),
label: Text(
'播放全部',
style: TextStyle(
fontSize: 15, // 从16减小到15
fontSize: 15.sp,
fontWeight: FontWeight.w600,
color: Colors.white,
),
@ -176,12 +172,10 @@ class MusicPlaylistView extends StatelessWidget {
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(Get.context!).primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14), // 从25减小到14,更协调
borderRadius: BorderRadius.circular(14.r),
),
elevation: 3, // 从2增加到3,增强视觉层次
shadowColor: Theme.of(Get.context!)
.primaryColor
.withValues(alpha: 0.4), // 添加阴影颜色
elevation: 3,
shadowColor: Theme.of(Get.context!).primaryColor.withAlpha(102),
),
),
),
@ -192,8 +186,7 @@ class MusicPlaylistView extends StatelessWidget {
Widget _buildPlaylistSection(AgentController controller,
List<Map<String, dynamic>> playlist, bool isDarkMode) {
return ListView.builder(
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 8), // 优化padding,与其他区域保持一致
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 8.h),
itemCount: playlist.length,
itemBuilder: (context, index) {
final item = playlist[index];
@ -237,40 +230,34 @@ class MusicPlaylistView extends StatelessWidget {
return GestureDetector(
onTap: () {
// 点击音乐卡片直接播放对应歌曲
_playSelectedSong(controller, song, Get.arguments ?? []);
},
child: Container(
margin: const EdgeInsets.only(bottom: 10), // 从12减小到10,更紧凑
padding: const EdgeInsets.all(14), // 从12增加到14,增加内部空间
margin: EdgeInsets.only(bottom: 10.h),
padding: EdgeInsets.all(14.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.white,
borderRadius: BorderRadius.circular(14), // 从12增加到14,更现代的圆角
borderRadius: BorderRadius.circular(14.r),
boxShadow: [
BoxShadow(
color: Colors.black
.withValues(alpha: isDarkMode ? 0.25 : 0.05), // 增强阴影效果
blurRadius: 4, // 从3增加到4
spreadRadius: 0, // 从1减小到0,更自然的阴影
offset: const Offset(0, 2), // 从(0,1)调整到(0,2),更明显的深度感
color: Colors.black.withOpacity(isDarkMode ? 0.25 : 0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
// 封面图片 - 优化设计
// 封面图片
Container(
width: 52, // 从50增加到52,稍微大一点
height: 52,
width: 52.w,
height: 52.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10), // 从8增加到10,更现代的圆角
color: isDarkMode
? Colors.grey[700]
: Colors.grey[200], // 根据主题调整背景色
borderRadius: BorderRadius.circular(10.r),
color: isDarkMode ? Colors.grey[700] : Colors.grey[200],
boxShadow: [
BoxShadow(
color:
Colors.black.withValues(alpha: isDarkMode ? 0.2 : 0.08),
color: Colors.black.withOpacity(isDarkMode ? 0.2 : 0.08),
blurRadius: 2,
offset: const Offset(0, 1),
),
@ -278,7 +265,7 @@ class MusicPlaylistView extends StatelessWidget {
),
child: image.isNotEmpty
? ClipRRect(
borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(10.r),
child: Image.network(
image,
fit: BoxFit.cover,
@ -288,7 +275,7 @@ class MusicPlaylistView extends StatelessWidget {
color: isDarkMode
? Colors.grey[500]
: Colors.grey[600],
size: 24, // 指定图标大小
size: 24.w,
);
},
),
@ -296,34 +283,34 @@ class MusicPlaylistView extends StatelessWidget {
: Icon(
Icons.music_note,
color: isDarkMode ? Colors.grey[500] : Colors.grey[600],
size: 24, // 指定图标大小
size: 24.w,
),
),
const SizedBox(width: 14), // 从12增加到14,增加间距
// 歌曲信息 - 优化文本样式
SizedBox(width: 14.w),
// 歌曲信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center, // 垂直居中对齐
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
name,
style: TextStyle(
fontSize: 15.5, // 从16减小到15.5,更协调
fontWeight: FontWeight.w600, // 从w500增加到w600,更突出
fontSize: 15.5.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black87,
height: 1.2, // 添加行高控制
height: 1.2,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 3), // 从4减小到3,更紧凑
SizedBox(height: 3.h),
Text(
singer,
style: TextStyle(
fontSize: 13, // 从14减小到13
fontSize: 13.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
height: 1.1, // 添加行高控制
height: 1.1,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
@ -331,15 +318,12 @@ class MusicPlaylistView extends StatelessWidget {
],
),
),
const SizedBox(width: 14), // 从12增加到14,与封面图片间距保持一致
SizedBox(width: 14.w),
// 播放按钮
Obx(() {
// 修复类型比较问题:将id转换为int进行比较
final currentIdInt = int.tryParse(id) ?? 0;
final isThisPlaying = controller.isPlaying.value &&
controller.currentId.value == currentIdInt;
// 只有当前播放的歌曲才显示进度
final currentProgress =
isThisPlaying ? controller.progress.value : 0.0;
@ -348,7 +332,7 @@ class MusicPlaylistView extends StatelessWidget {
progress: currentProgress,
primaryColor: Theme.of(context).primaryColor,
backgroundColor: Colors.grey,
size: 40.0,
size: 40.w,
strokeWidth: 2.5,
onPlayPause: () async {
try {
@ -356,7 +340,6 @@ class MusicPlaylistView extends StatelessWidget {
'播放按钮被点击,当前播放状态: ${controller.isPlaying.value}');
Logger.d('MusicPlaylistView',
'当前歌曲ID: ${controller.currentId.value}, 点击歌曲ID: $currentIdInt');
if (controller.isPlaying.value) {
if (controller.currentId.value == currentIdInt) {
Logger.d('MusicPlaylistView', '暂停当前播放的歌曲');
@ -391,15 +374,15 @@ class MusicPlaylistView extends StatelessWidget {
// 构建封面图片
Widget _buildCoverImage(String coverUrl, bool isDarkMode,
{double size = 200}) {
{required double size}) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: isDarkMode ? 0.3 : 0.1),
color: Colors.black.withOpacity(isDarkMode ? 0.3 : 0.1),
blurRadius: 15,
spreadRadius: 2,
offset: const Offset(0, 6),
@ -407,27 +390,27 @@ class MusicPlaylistView extends StatelessWidget {
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(16.r),
child: coverUrl.isNotEmpty
? Image.network(
coverUrl,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return _buildDefaultCover(isDarkMode);
return _buildDefaultCover(isDarkMode, size: size);
},
)
: _buildDefaultCover(isDarkMode),
: _buildDefaultCover(isDarkMode, size: size),
),
);
}
// 构建默认封面
Widget _buildDefaultCover(bool isDarkMode) {
Widget _buildDefaultCover(bool isDarkMode, {required double size}) {
return Container(
color: isDarkMode ? Colors.grey[800] : Colors.grey[300],
child: Icon(
Icons.music_note,
size: 60, // 从20改为60,更合适的大小
size: size * 0.55,
color: isDarkMode ? Colors.grey[600] : Colors.grey[500],
),
);
@ -440,7 +423,7 @@ class MusicPlaylistView extends StatelessWidget {
Text(
title,
style: TextStyle(
fontSize: 18, // 从20减小到18
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black87,
),
@ -448,11 +431,11 @@ class MusicPlaylistView extends StatelessWidget {
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 6), // 从8减小到6
SizedBox(height: 6.h),
Text(
artist,
style: TextStyle(
fontSize: 14, // 从16减小到14
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
textAlign: TextAlign.center,
@ -463,39 +446,36 @@ class MusicPlaylistView extends StatelessWidget {
);
}
// 构建进度条 - 优化布局:将进度条和时间显示合并到同一水平线
// 构建进度条
Widget _buildProgressBar(AgentController controller, bool isDarkMode) {
return Obx(() {
return Row(
children: [
// 左侧:当前播放时间
SizedBox(
width: 35.w, // 固定宽度确保对齐,足够显示 "99:59" 格式
width: 40.w,
child: Text(
_formatDuration(controller.currentPosition.value),
style: TextStyle(
fontSize: 12,
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
fontFeatures: const [FontFeature.tabularFigures()], // 使用等宽数字
fontFeatures: const [FontFeature.tabularFigures()],
),
textAlign: TextAlign.left,
),
),
SizedBox(width: 6.w), // 左侧时间和进度条之间的间距
// 中间:可交互的进度条
SizedBox(width: 8.w),
Expanded(
child: SliderTheme(
data: SliderTheme.of(Get.context!).copyWith(
data: SliderThemeData(
activeTrackColor: Theme.of(Get.context!).primaryColor,
inactiveTrackColor:
isDarkMode ? Colors.grey[700] : Colors.grey[300],
thumbColor: Theme.of(Get.context!).primaryColor,
overlayColor:
Theme.of(Get.context!).primaryColor.withValues(alpha: 0.2),
Theme.of(Get.context!).primaryColor.withOpacity(0.2),
trackHeight: 4.h,
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 0),
overlayShape: const RoundSliderOverlayShape(overlayRadius: 0),
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8),
overlayShape: const RoundSliderOverlayShape(overlayRadius: 14),
),
child: Slider(
value: controller.progress.value.clamp(0.0, 1.0),
@ -507,16 +487,15 @@ class MusicPlaylistView extends StatelessWidget {
),
),
),
SizedBox(width: 6.w), // 进度条和右侧时间之间的间距
// 右侧:音乐总时长
SizedBox(width: 8.w),
SizedBox(
width: 35.w, // 固定宽度确保对齐,足够显示 "99:59" 格式
width: 40.w,
child: Text(
_formatDuration(controller.duration.value),
style: TextStyle(
fontSize: 12,
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
fontFeatures: const [FontFeature.tabularFigures()], // 使用等宽数字
fontFeatures: const [FontFeature.tabularFigures()],
),
textAlign: TextAlign.right,
),
@ -526,14 +505,6 @@ class MusicPlaylistView extends StatelessWidget {
});
}
// 格式化时间显示
String _formatDuration(int milliseconds) {
final duration = Duration(milliseconds: milliseconds);
final minutes = duration.inMinutes;
final seconds = duration.inSeconds % 60;
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
// 构建播放控制按钮
Widget _buildPlayControls(AgentController controller,
List<Map<String, dynamic>> playlist, bool isDarkMode) {
@ -541,50 +512,48 @@ class MusicPlaylistView extends StatelessWidget {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 上一曲按钮
IconButton(
onPressed: () => _playPrevious(controller, playlist),
iconSize: 32.w,
padding: EdgeInsets.all(8.w), // 增大触摸区域
icon: Icon(
Icons.skip_previous,
size: 32, // 从36减小到32
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
const SizedBox(width: 16), // 从20减小到16
// 播放/暂停按钮
SizedBox(width: 16.w),
Container(
width: 56, // 从64减小到56
height: 56,
width: 56.w,
height: 56.w,
decoration: BoxDecoration(
color: Theme.of(Get.context!).primaryColor,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Theme.of(Get.context!)
.primaryColor
.withValues(alpha: 0.3),
blurRadius: 10, // 从12减小到10
spreadRadius: 1, // 从2减小到1
offset: const Offset(0, 3), // 从4减小到3
color: Theme.of(Get.context!).primaryColor.withOpacity(0.3),
blurRadius: 10,
spreadRadius: 1,
offset: const Offset(0, 3),
),
],
),
child: IconButton(
onPressed: () => _togglePlayPause(controller),
padding: EdgeInsets.zero,
icon: Icon(
controller.isPlaying.value ? Icons.pause : Icons.play_arrow,
size: 28, // 从32减小到28
size: 28.w,
color: Colors.white,
),
),
),
const SizedBox(width: 16), // 从20减小到16
// 下一曲按钮
SizedBox(width: 16.w),
IconButton(
onPressed: () => _playNext(controller, playlist),
iconSize: 32.w,
padding: EdgeInsets.all(8.w), // 增大触摸区域
icon: Icon(
Icons.skip_next,
size: 32, // 从36减小到32
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
@ -593,6 +562,15 @@ class MusicPlaylistView extends StatelessWidget {
});
}
// ...(其他辅助方法保持不变,如_formatDuration, _togglePlayPause等)
// 格式化时间显示
String _formatDuration(int milliseconds) {
final duration = Duration(milliseconds: milliseconds);
final minutes = duration.inMinutes;
final seconds = duration.inSeconds % 60;
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
// 播放/暂停切换
Future<void> _togglePlayPause(AgentController controller) async {
if (controller.isPlaying.value) {
@ -605,7 +583,6 @@ class MusicPlaylistView extends StatelessWidget {
// 播放上一曲
void _playPrevious(
AgentController controller, List<Map<String, dynamic>> playlist) {
// 确保播放列表上下文是最新的
controller.updateCurrentPlaylistContext(
playlist, controller.currentId.value);
controller.playPrevious();
@ -614,7 +591,6 @@ class MusicPlaylistView extends StatelessWidget {
// 播放下一曲
void _playNext(
AgentController controller, List<Map<String, dynamic>> playlist) {
// 确保播放列表上下文是最新的
controller.updateCurrentPlaylistContext(
playlist, controller.currentId.value);
controller.playNext();
@ -623,11 +599,8 @@ class MusicPlaylistView extends StatelessWidget {
// 播放选中的歌曲
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);
}
}

25
lib/modules/pairing/controllers/pairing_controller.dart

@ -60,6 +60,9 @@ class PairingController extends GetxController {
final showRetryButton = false.obs;
final showAutoCodeInput = false.obs;
final showSkipButton = false.obs;
// 增加临时跳过绑定按钮状态
final showSkipBindingButton = false.obs; // 新增临时跳过绑定按钮
final connectButtonText = '连接设备'.obs;
final retryButtonText = '重试'.obs;
@ -242,6 +245,7 @@ class PairingController extends GetxController {
isLoading.value = false;
showConnectButton.value = false;
showRetryButton.value = false;
showSkipBindingButton.value = false; // 重置临时跳过绑定按钮
showSkipButton.value = false;
showAutoCodeInput.value = false;
@ -285,10 +289,11 @@ class PairingController extends GetxController {
});
break;
case PairingState.manualbinding:
statusText.value = '需要手动版定授权码...';
statusText.value = '需要手动绑定授权码...';
isLoading.value = false;
showAutoCodeInput.value = true;
showSkipButton.value = true;
// showSkipButton.value = true;
showSkipBindingButton.value = true; // 显示临时跳过绑定按钮
break;
case PairingState.completed:
statusText.value = '配对成功!';
@ -557,6 +562,7 @@ class PairingController extends GetxController {
});
Logger.i(_tag, '手动授权结果 ${response}');
if (response != null) {
User.instance.earphonemac = currentDevice?.address ?? '';
_transition(PairingState.verifying);
_verifyDevice(_currentClassicDevice!);
} else {
@ -575,7 +581,7 @@ class PairingController extends GetxController {
} catch (e) {
Logger.e(_tag, '启动AgentService失败: $e');
}
Get.offAllNamed(Routes.home);
});
}
@ -631,10 +637,19 @@ class PairingController extends GetxController {
}
}
/// 用户点击跳过绑定按钮
void onSkipBindingTap() {
Logger.i(_tag, '用户选择临时跳过手动绑定');
User.instance.earphonemac = currentDevice?.address ?? '';
_transition(PairingState.verifying);
_verifyDevice(_currentClassicDevice!);
}
/// 用户点击跳过按钮
void onSkipTap() async {
Logger.i(_tag, '用户选择跳过配对');
// 启动AgentService服务
try {
Logger.i(_tag, '启动AgentService服务');
@ -642,7 +657,7 @@ class PairingController extends GetxController {
} catch (e) {
Logger.e(_tag, '启动AgentService失败: $e');
}
Get.offAllNamed(Routes.home);
}

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

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

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