import 'dart:async'; import 'dart:ui'; import 'package:ai_chat_assistant/manager.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../pages/full_screen.dart'; import '../services/message_service.dart'; import 'chat/chat_window_content.dart'; import 'chat_floating_icon.dart'; class ChatPopup extends StatefulWidget { final double? iconSize; final Widget? icon; final Size chatSize; final ChatFloatingIcon? child; const ChatPopup({ super.key, this.child, this.iconSize, this.icon, this.chatSize = const Size(320, 450), }); @override State createState() => _ChatPopupState(); } class _ChatPopupState extends State with SingleTickerProviderStateMixin { Offset _iconPosition = const Offset(10, 120); final double _iconSizeDefault = 80.0; bool _isShowingPopup = false; late AnimationController _partScreenAnimationController; StreamSubscription? _wakeWordSubscription; @override void initState() { super.initState(); _partScreenAnimationController = AnimationController( duration: const Duration(milliseconds: 250), vsync: this, ); // 订阅唤醒词事件 _wakeWordSubscription = AIChatAssistantManager.instance.onWakeWordDetected.listen((_) async { // 在这里处理唤醒事件,例如显示聊天窗口 debugPrint("唤醒词被检测到,执行UI操作!"); final messageService = MessageService.instance; _insertOverlay(); await messageService.startVoiceInput(); }); } @override void dispose() { _partScreenAnimationController.dispose(); _wakeWordSubscription?.cancel(); super.dispose(); } void _insertOverlay() { if (_isShowingPopup) return; setState(() { _isShowingPopup = true; }); _partScreenAnimationController.forward(); } void _removeOverlay() { if (!_isShowingPopup) return; _partScreenAnimationController.reverse().then((_) { if (mounted) { setState(() { _isShowingPopup = false; }); } }); } _openFullScreenPage() async { final messageService = MessageService.instance; _removeOverlay(); await Navigator.of(context).push( MaterialPageRoute( builder: (context) => ChangeNotifierProvider.value( value: messageService, // 传递同一个单例实例 child: const FullScreenPage(), ), ), ); } @override Widget build(BuildContext context) { return ChangeNotifierProvider.value( value: MessageService.instance, child: LayoutBuilder( builder: (context, constraints) { return Stack( children: [ // 1. 底层:弹窗内容(只有在显示时才渲染) if (_isShowingPopup) ...[ _buildPopupBackground(), _buildPopupContent(constraints), ], // 2. 顶层:FloatingIcon(始终在最上层,事件不被遮挡) _buildFloatingIcon(), ], ); }, ), ); } Widget _buildPopupBackground() { return Positioned.fill( child: GestureDetector( onTap: () { final messageService = MessageService.instance; _removeOverlay(); messageService.abortReply(); messageService.initializeEmpty(); }, child: SizedBox.expand( child: Container( color: Colors.black45.withValues(alpha: 0.1), )))); } Widget _buildPopupContent(BoxConstraints constraints) { final position = _calculatePosition(constraints); final double minHeight = constraints.maxHeight * 0.16; final double maxHeight = constraints.maxHeight * 0.4; final double chatWidth = constraints.maxWidth * 0.94; return Positioned( left: position.left, right: position.right, bottom: position.bottom, child: ChatWindowContent( onCloseWindow: _removeOverlay, animationController: _partScreenAnimationController, minHeight: minHeight, maxHeight: maxHeight, chatWidth: chatWidth, ), ); } Widget _buildFloatingIcon() { // 如果外部传入了 child,优先使用它的属性 if (widget.child != null) { return ChatFloatingIcon( iconSize: widget.child!.iconSize ?? widget.iconSize ?? _iconSizeDefault, icon: widget.child!.icon ?? widget.icon, onTap: () async { debugPrint('🖱️ FloatingIcon onTap triggered! (using child properties)'); // 先执行外部传入的 onTap(如果有) if (widget.child!.onTap != null) { widget.child!.onTap!(); } // 默认行为:关闭弹窗并打开全屏 await _openFullScreenPage(); }, onLongPress: () async { debugPrint('⏳ FloatingIcon onLongPress triggered! (using child properties)'); // 先执行外部传入的 onLongPress(如果有) if (widget.child!.onLongPress != null) { widget.child!.onLongPress!(); } // 执行内部业务逻辑 final messageService = MessageService.instance; _insertOverlay(); await messageService.startVoiceInput(); }, onLongPressUp: () async { debugPrint('⏫ FloatingIcon onLongPressUp triggered! (using child properties)'); // 先执行外部传入的 onLongPressUp(如果有) if (widget.child!.onLongPressUp != null) { widget.child!.onLongPressUp!(); } // 执行内部业务逻辑 final messageService = MessageService.instance; await messageService.stopAndProcessVoiceInput(); final hasMessage = messageService.messages.isNotEmpty; if (!hasMessage) { _removeOverlay(); } }, onPositionChanged: (position) { debugPrint('📍 FloatingIcon position changed: $position (using child properties)'); // 先执行外部传入的 onPositionChanged(如果有) if (widget.child!.onPositionChanged != null) { widget.child!.onPositionChanged!(position); } // 执行内部业务逻辑 setState(() { _iconPosition = position; }); }, // 传递其他可能的属性 onLongPressEnd: widget.child!.onLongPressEnd, onDragStart: widget.child!.onDragStart, onDragEnd: widget.child!.onDragEnd, ); } // 如果没有传入 child,使用原来的逻辑 return ChatFloatingIcon( iconSize: widget.iconSize ?? _iconSizeDefault, icon: widget.icon, onTap: () async { debugPrint('🖱️ FloatingIcon onTap triggered!'); await _openFullScreenPage(); }, onLongPress: () async { debugPrint('⏳ FloatingIcon onLongPress triggered!'); final messageService = MessageService.instance; _insertOverlay(); await messageService.startVoiceInput(); }, onLongPressUp: () async { debugPrint('⏫ FloatingIcon onLongPressUp triggered!'); final messageService = MessageService.instance; await messageService.stopAndProcessVoiceInput(); final hasMessage = messageService.messages.isNotEmpty; if (!hasMessage) { _removeOverlay(); } }, onPositionChanged: (position) { debugPrint('📍 FloatingIcon position changed: $position'); setState(() { _iconPosition = position; }); }, ); } // 计算PartScreen的位置,使其显示在FloatingIcon附近 EdgeInsets _calculatePosition(BoxConstraints constraints) { final screenWidth = constraints.maxWidth; final screenHeight = constraints.maxHeight; final chatWidth = screenWidth * 0.94; final chatHeight = screenHeight * 0.4; // 1. 先将icon的right/bottom转为屏幕坐标(icon左下角) final iconLeft = screenWidth - _iconPosition.dx - (widget.iconSize ?? _iconSizeDefault); final iconBottom = _iconPosition.dy; final iconTop = iconBottom + (widget.iconSize ?? _iconSizeDefault); final iconCenterX = iconLeft + (widget.iconSize ?? _iconSizeDefault) / 2; // 判断FloatingIcon在屏幕的哪一边 final isOnRightSide = iconCenterX > screenWidth / 2; // 计算水平位置 double leftPadding; if (isOnRightSide) { // 图标在右边,聊天框显示在左边 leftPadding = (screenWidth - chatWidth) * 0.1; } else { // 图标在左边,聊天框显示在右边 leftPadding = screenWidth - chatWidth - (screenWidth - chatWidth) * 0.1; } // 优先尝试放在icon上方 double chatBottom = iconTop + 12; // 聊天框底部距离icon顶部12px double chatTop = screenHeight - chatBottom - chatHeight; // 如果上方空间不足,则放在icon下方 if (chatTop < 0) { chatBottom = iconBottom - 12 - chatHeight / 2; // 聊天框顶部距离icon底部12px // 如果下方也不足,则贴底 if (chatBottom < 0) { chatBottom = 20; // 距离底部20px } } return EdgeInsets.only( left: leftPadding, right: screenWidth - leftPadding - chatWidth, bottom: chatBottom, ); } }