diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 3648b0639..03bce6c62 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -75,8 +75,11 @@ class AgentController extends GetxController { final isFullScreenEditMode = false.obs; // 是否处于全屏编辑模式 final shouldShowExpandButton = false.obs; // 是否显示展开按钮 - // 图片选择选项展开状态 - final isImageOptionsExpanded = false.obs; // 是否展开图片选择选项 + // "+"号选项展开状态 + final isPlusOptionsExpanded = false.obs; // 是否展开"+"号选项 + + // 打电话模式状态 + final isPhoneCallMode = false.obs; // 是否处于打电话模式 // 键盘状态 final isKeyboardVisible = false.obs; // 键盘是否可见 @@ -904,39 +907,65 @@ class AgentController extends GetxController { isImageInputActive.value = false; } - // 切换图片选项展开状态 - void toggleImageOptions() { - if (isImageOptionsExpanded.value) { + // 切换'+'选项展开状态 + void togglePlusOptions() { + if (isPlusOptionsExpanded.value) { // 如果当前已展开,则收缩 - collapseImageOptions(); + collapsePlusOptions(); + _pendingPlusOptionsExpansion = false; // 取消待展开状态 } else { // 如果当前未展开,则展开并关闭键盘 if (isKeyboardVisible.value) { + // 设置待展开标记 + _pendingPlusOptionsExpansion = true; hideKeyboard(); - // 延迟展开选项,等待键盘完全关闭 - Future.delayed(const Duration(milliseconds: 200), () { - // 再次检查键盘状态,确保键盘已关闭 - if (!isKeyboardVisible.value) { - isImageOptionsExpanded.value = true; - } else { - // 如果键盘还没关闭,再等一会儿 - Future.delayed(const Duration(milliseconds: 100), () { - if (!isKeyboardVisible.value) { - isImageOptionsExpanded.value = true; - } - }); - } - }); + // 使用双重机制:轮询检测 + 键盘状态监听 + _waitForKeyboardToClose(); } else { // 键盘未显示,直接展开 - isImageOptionsExpanded.value = true; + isPlusOptionsExpanded.value = true; } } } - // 收缩图片选项 - void collapseImageOptions() { - isImageOptionsExpanded.value = false; + // 等待键盘关闭的轮询机制 + void _waitForKeyboardToClose() { + int attempts = 0; + const maxAttempts = 30; // 最多尝试30次,总计3秒 + const checkInterval = Duration(milliseconds: 100); // 每100ms检查一次 + + Timer.periodic(checkInterval, (timer) { + attempts++; + + // 检查是否还需要展开(用户可能已经取消了) + if (!_pendingPlusOptionsExpansion) { + timer.cancel(); + return; + } + + if (!isKeyboardVisible.value) { + // 键盘已关闭,展开选项 + timer.cancel(); + _pendingPlusOptionsExpansion = false; + // 添加小延迟确保UI稳定 + Future.delayed(const Duration(milliseconds: 50), () { + if (!isKeyboardVisible.value) { // 再次确认键盘状态 + isPlusOptionsExpanded.value = true; + } + }); + } else if (attempts >= maxAttempts) { + // 超时,强制展开(可能键盘检测有问题) + timer.cancel(); + _pendingPlusOptionsExpansion = false; + Logger.d('AgentController', '键盘关闭检测超时,强制展开选项'); + isPlusOptionsExpanded.value = true; + } + }); + } + + // 收缩'+'选项 + void collapsePlusOptions() { + isPlusOptionsExpanded.value = false; } // 更新键盘可见状态 @@ -944,13 +973,26 @@ class AgentController extends GetxController { final wasVisible = isKeyboardVisible.value; isKeyboardVisible.value = isVisible; - // 只有当键盘从隐藏变为显示时,才收缩图片选项 + // 只有当键盘从隐藏变为显示时,才收缩'+'选项 // 避免键盘关闭时误触发收缩逻辑 - if (!wasVisible && isVisible && isImageOptionsExpanded.value) { - collapseImageOptions(); + if (!wasVisible && isVisible && isPlusOptionsExpanded.value) { + collapsePlusOptions(); + } + + // 如果键盘关闭了,并且有待展开的选项,则展开 + if (wasVisible && !isVisible && _pendingPlusOptionsExpansion) { + _pendingPlusOptionsExpansion = false; + Future.delayed(const Duration(milliseconds: 100), () { + if (!isKeyboardVisible.value) { + isPlusOptionsExpanded.value = true; + } + }); } } + // 标记是否有待展开的'+'选项 + bool _pendingPlusOptionsExpansion = false; + // 隐藏键盘 void hideKeyboard() { FocusManager.instance.primaryFocus?.unfocus(); @@ -985,6 +1027,54 @@ class AgentController extends GetxController { useImagePrompt('请翻译这张图片中的文字'); } + // 开始打电话模式 + void startPhoneCall() async { + Logger.i(TAG, '🔥 开始打电话模式'); + + // 收缩"+"选项 + collapsePlusOptions(); + + // 进入打电话模式 + isPhoneCallMode.value = true; + + // 如果当前是文本模式,切换到语音模式 + if (isTextInputMode.value) { + // 检查麦克风权限 + final hasPermission = await PermissionUtil.instance.requestPermission( + permissionType: Permission.microphone, + permissionName: '麦克风', + explanationText: '打电话功能需要访问麦克风进行语音通话。', + permanentDenialText: '您已永久拒绝麦克风权限,请前往系统设置开启权限。', + settingsButtonText: '去设置', + cancelButtonText: '取消', + ); + + if (!hasPermission) { + // 权限未获取,退出打电话模式 + isPhoneCallMode.value = false; + return; + } + + // 切换到语音模式并开始语音输入 + isTextInputMode.value = false; + await startVoiceInput(); + } + } + + // 结束打电话模式 + void endPhoneCall() async { + Logger.i(TAG, '🔥 结束打电话模式'); + + // 退出打电话模式 + isPhoneCallMode.value = false; + + // 停止语音输入 + await stopVoiceInput(); + + // 切换回文本模式 + isTextInputMode.value = true; + } + // 获取音乐播放状态 Future musicStatus() async { try { diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index a1f8361a2..9e24b7ef6 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -183,10 +183,13 @@ class _AgentViewState extends State with WidgetsBindingObserver { ), ], ), - body: Column( + body: Stack( children: [ - // 聊天内容 - Expanded( + // 主要聊天界面 + Column( + children: [ + // 聊天内容 + Expanded( child: Obx( () => Align( alignment: Alignment.topCenter, @@ -407,12 +410,12 @@ class _AgentViewState extends State with WidgetsBindingObserver { textInputAction: TextInputAction .newline, // 支持回车换行 onTap: () { - // 当点击输入框时,如果图片选项已展开,则收缩 + // 当点击输入框时,如果'+'选项已展开,则收缩 if (controller - .isImageOptionsExpanded + .isPlusOptionsExpanded .value) { controller - .collapseImageOptions(); + .collapsePlusOptions(); } }, style: TextStyle( @@ -607,10 +610,10 @@ class _AgentViewState extends State with WidgetsBindingObserver { // 显示'+'选择按钮 return Obx(() { final isExpanded = - controller.isImageOptionsExpanded.value; + controller.isPlusOptionsExpanded.value; return GestureDetector( onTap: () { - controller.toggleImageOptions(); + controller.togglePlusOptions(); }, child: Container( width: 44, @@ -640,11 +643,11 @@ class _AgentViewState extends State with WidgetsBindingObserver { // 展开的'+'选择选项 Obx(() { final isExpanded = - controller.isImageOptionsExpanded.value; + controller.isPlusOptionsExpanded.value; return AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, - height: isExpanded ? 100.h : 0, // 调整高度以适应按钮尺寸 + height: isExpanded ? 120.h : 0, // 调整高度以适应按钮尺寸 child: isExpanded ? Container( margin: EdgeInsets.only(top: 8.h), @@ -658,7 +661,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { icon: Icons.camera_alt, label: '拍照', onTap: () async { - //controller.collapseImageOptions(); + //controller.collapsePlusOptions(); await controller.takePhoto(); }, isDarkMode: isDarkMode, @@ -669,7 +672,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { icon: Icons.photo_library, label: '相册', onTap: () async { - //controller.collapseImageOptions(); + //controller.collapsePlusOptions(); await controller.pickImage(); }, isDarkMode: isDarkMode, @@ -680,19 +683,19 @@ class _AgentViewState extends State with WidgetsBindingObserver { // icon: Icons.folder, // label: '文件', // onTap: () { - // //controller.collapseImageOptions(); + // //controller.collapsePlusOptions(); // // TODO: 实现文件选择功能 // }, // isDarkMode: isDarkMode, // ), - // 通话按钮(预留位置) + // 通话按钮 _buildPlusOptionButton( context: context, icon: Icons.phone, label: '打电话', onTap: () { - //controller.collapseImageOptions(); - // TODO: 实现通话功能 + // 启动打电话模式 + controller.startPhoneCall(); }, isDarkMode: isDarkMode, ), @@ -707,6 +710,16 @@ class _AgentViewState extends State with WidgetsBindingObserver { ], ), ), + ], + ), + + // 打电话模式覆盖层 + Obx(() { + if (controller.isPhoneCallMode.value) { + return _buildPhoneCallOverlay(context, isDarkMode); + } + return const SizedBox.shrink(); + }), ], ), ); @@ -808,6 +821,239 @@ class _AgentViewState extends State with WidgetsBindingObserver { ), ); } + + // 构建打电话模式覆盖层 + Widget _buildPhoneCallOverlay(BuildContext context, bool isDarkMode) { + return Container( + color: isDarkMode ? Colors.black : Colors.white, + child: SafeArea( + child: Column( + children: [ + // 顶部标题栏 + _buildPhoneCallHeader(isDarkMode), + + // 中间音频可视化区域 + Expanded( + child: _buildPhoneCallVisualization(isDarkMode), + ), + + // 底部控制按钮 + _buildPhoneCallControls(isDarkMode), + ], + ), + ), + ); + } + + // 构建打电话模式标题栏 + Widget _buildPhoneCallHeader(bool isDarkMode) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + // 返回按钮 + GestureDetector( + onTap: () => controller.endPhoneCall(), + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[800] : Colors.grey[200], + borderRadius: BorderRadius.circular(20), + ), + child: Icon( + Icons.arrow_back, + color: isDarkMode ? Colors.white : Colors.black, + size: 20, + ), + ), + ), + + const SizedBox(width: 16), + + // 标题 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'AI语音通话', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + Obx(() { + final status = controller.isListening.value + ? '正在通话中...' + : controller.isSpeaking.value + ? 'AI正在回复...' + : '等待开始...'; + return Text( + status, + style: TextStyle( + fontSize: 14, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + ); + }), + ], + ), + ), + ], + ), + ); + } + + // 构建打电话模式音频可视化 + Widget _buildPhoneCallVisualization(bool isDarkMode) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 音频可视化圆圈 + Obx(() { + final isActive = controller.isListening.value || controller.isSpeaking.value; + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: isActive ? 200 : 160, + height: isActive ? 200 : 160, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isDarkMode + ? Colors.blue[900]?.withOpacity(0.3) + : Colors.blue[100], + border: Border.all( + color: Colors.blue, + width: isActive ? 3 : 2, + ), + ), + child: Icon( + Icons.phone, + size: isActive ? 80 : 60, + color: Colors.blue, + ), + ); + }), + + const SizedBox(height: 32), + + // 状态文字 + Obx(() { + String statusText = '点击开始通话'; + if (controller.isListening.value) { + statusText = '正在听您说话...'; + } else if (controller.isSpeaking.value) { + statusText = 'AI正在回复...'; + } else if (controller.isProcessing.value) { + statusText = '正在处理...'; + } + + return Text( + statusText, + style: TextStyle( + fontSize: 16, + color: isDarkMode ? Colors.grey[300] : Colors.grey[700], + fontWeight: FontWeight.w500, + ), + ); + }), + ], + ), + ); + } + + // 构建打电话模式控制按钮 + Widget _buildPhoneCallControls(bool isDarkMode) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // 静音按钮 + Obx(() => _buildPhoneControlButton( + icon: controller.isTtsEnabled.value ? Icons.volume_up : Icons.volume_off, + onTap: () => controller.toggleTts(), + isDarkMode: isDarkMode, + isActive: controller.isTtsEnabled.value, + )), + + // 主要通话按钮 + Obx(() { + final isActive = controller.isListening.value; + return GestureDetector( + onTap: () { + if (isActive) { + controller.stopVoiceInput(); + } else { + controller.startVoiceInput(); + } + }, + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isActive ? Colors.red : Colors.green, + boxShadow: [ + BoxShadow( + color: (isActive ? Colors.red : Colors.green).withOpacity(0.3), + blurRadius: 12, + spreadRadius: 2, + ), + ], + ), + child: Icon( + isActive ? Icons.call_end : Icons.call, + color: Colors.white, + size: 36, + ), + ), + ); + }), + + // 结束通话按钮 + _buildPhoneControlButton( + icon: Icons.call_end, + onTap: () => controller.endPhoneCall(), + isDarkMode: isDarkMode, + isActive: false, + backgroundColor: Colors.red, + ), + ], + ), + ); + } + + // 构建打电话控制按钮 + Widget _buildPhoneControlButton({ + required IconData icon, + required VoidCallback onTap, + required bool isDarkMode, + required bool isActive, + Color? backgroundColor, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 56, + height: 56, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: backgroundColor ?? (isDarkMode ? Colors.grey[800] : Colors.grey[200]), + border: isActive ? Border.all(color: Colors.blue, width: 2) : null, + ), + child: Icon( + icon, + color: backgroundColor != null + ? Colors.white + : (isDarkMode ? Colors.white : Colors.black), + size: 24, + ), + ), + ); + } } diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index fd7463c4a..0ebed9187 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -376,7 +376,11 @@ class TranslationController extends GetxController { } final recognitionStream = await _asrService.startContinuousRecognition(_audioSourceType); - _asrService.setcontinue(); + if (currentMode.value == 'faceToFace') { + _asrService.setPause(); + } else { + _asrService.setcontinue(); + } isRecognizing.value = true; if (isRecording.value && isRecognizing.value) { startRecording();