Files
ai_chat_assistant/lib/widgets/chat_popup.dart

280 lines
8.8 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:ui';
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<ChatPopup> createState() => _ChatPopupState();
}
class _ChatPopupState extends State<ChatPopup> with SingleTickerProviderStateMixin {
Offset _iconPosition = const Offset(10, 120);
final double _iconSizeDefault = 80.0;
bool _isShowingPopup = false;
late AnimationController _partScreenAnimationController;
@override
void initState() {
super.initState();
_partScreenAnimationController = AnimationController(
duration: const Duration(milliseconds: 250),
vsync: this,
);
}
@override
void dispose() {
_partScreenAnimationController.dispose();
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;
});
}
});
}
@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()
),
);
}
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(
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!();
} else {
// 默认行为:关闭弹窗并打开全屏
final messageService = context.read<MessageService>();
_removeOverlay();
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ChangeNotifierProvider.value(
value: messageService,
child: const FullScreenPage(),
),
),
);
}
},
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!');
final messageService = context.read<MessageService>();
_removeOverlay();
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ChangeNotifierProvider.value(
value: messageService, // 传递同一个单例实例
child: const FullScreenPage(),
),
),
);
},
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,
);
}
}