288 lines
9.2 KiB
Dart
288 lines
9.2 KiB
Dart
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<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;
|
||
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,
|
||
);
|
||
}
|
||
}
|