Files
ai_chat_assistant/lib/screens/part_screen.dart

255 lines
9.0 KiB
Dart
Raw Permalink Normal View History

2025-08-12 13:36:42 +08:00
import 'package:flutter/material.dart';
import 'dart:ui';
import '../widgets/chat_box.dart';
import 'package:provider/provider.dart';
import '../services/message_service.dart';
import '../widgets/gradient_background.dart';
2025-09-19 11:40:38 +08:00
import '../utils/assets_util.dart';
import '../pages/full_screen.dart';
2025-08-12 13:36:42 +08:00
class PartScreen extends StatefulWidget {
final VoidCallback? onHide;
final Offset floatingIconPosition;
final double iconSize;
2025-08-12 13:36:42 +08:00
const PartScreen({
super.key,
this.onHide,
required this.floatingIconPosition,
required this.iconSize,
});
2025-08-12 13:36:42 +08:00
@override
State<PartScreen> createState() => _PartScreenState();
}
class _PartScreenState extends State<PartScreen> {
final ScrollController _scrollController = ScrollController();
2025-08-13 15:17:13 +08:00
bool _isInitialized = false;
2025-08-22 17:35:02 +08:00
int _lastMessageCount = 0;
2025-08-12 13:36:42 +08:00
@override
void initState() {
super.initState();
2025-08-13 15:17:13 +08:00
WidgetsBinding.instance.addPostFrameCallback((_) {
2025-08-22 16:32:13 +08:00
final messageService =
Provider.of<MessageService>(context, listen: false);
messageService.removeNonListeningMessages();
2025-08-13 15:17:13 +08:00
setState(() {
_isInitialized = true;
});
});
2025-08-12 13:36:42 +08:00
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
void _scrollToBottom() {
if (_scrollController.hasClients) {
_scrollController.animateTo(
0.0,
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
);
}
}
void _openFullScreen() async {
final messageService = context.read<MessageService>();
2025-08-12 13:36:42 +08:00
widget.onHide?.call();
2025-08-12 13:36:42 +08:00
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ChangeNotifierProvider.value(
value: messageService, // 传递同一个单例实例
child: const FullScreenPage(),
),
2025-08-12 13:36:42 +08:00
),
);
}
// 计算PartScreen的位置使其显示在FloatingIcon附近
EdgeInsets _calculatePosition(BoxConstraints constraints) {
final screenWidth = constraints.maxWidth;
final screenHeight = constraints.maxHeight;
// 聊天框的尺寸
final chatWidth = screenWidth * 0.94;
final maxChatHeight = screenHeight * 0.4;
final iconX = screenWidth - widget.floatingIconPosition.dx;
final iconY = screenHeight - widget.floatingIconPosition.dy;
// 1. 先将icon的right/bottom转为屏幕坐标icon左下角
var iconPosition = widget.floatingIconPosition;
final iconLeft = screenWidth - iconPosition.dx - widget.iconSize;
final iconBottom = iconPosition.dy;
final iconTop = iconBottom + widget.iconSize;
final iconCenterX = iconLeft + widget.iconSize / 2;
// 判断FloatingIcon在屏幕的哪一边
final isOnRightSide = iconX < 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 - maxChatHeight;
// 如果上方空间不足则放在icon下方
if (chatTop < 0) {
chatBottom = iconBottom - 12 - maxChatHeight / 2; // 聊天框顶部距离icon底部12px
// 如果下方也不足,则贴底
if (chatBottom < 0) {
chatBottom = 20; // 距离底部20px
}
}
// // 计算垂直位置,确保聊天框在图标上方
// double bottomPadding = iconY + widget.iconSize + 20;
//
// // 确保聊天框不会超出屏幕
// final minBottomPadding = screenHeight * 0.15;
// final maxBottomPadding = screenHeight - maxChatHeight - 50;
// bottomPadding = bottomPadding.clamp(minBottomPadding, maxBottomPadding);
return EdgeInsets.only(
left: leftPadding,
right: screenWidth - leftPadding - chatWidth,
bottom: chatBottom,
);
}
2025-08-12 13:36:42 +08:00
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
body: LayoutBuilder(
builder: (context, constraints) {
2025-08-13 15:17:13 +08:00
if (!_isInitialized) {
return const SizedBox.shrink();
}
final position = _calculatePosition(constraints);
2025-08-22 16:32:13 +08:00
final double minHeight = constraints.maxHeight * 0.16;
2025-08-12 13:36:42 +08:00
final double maxHeight = constraints.maxHeight * 0.4;
final double chatWidth = constraints.maxWidth * 0.94;
2025-08-12 13:36:42 +08:00
return Stack(
children: [
Positioned.fill(
child: IgnorePointer(
ignoring: false,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
2025-08-13 15:17:13 +08:00
final messageService = context.read<MessageService>();
2025-08-12 13:36:42 +08:00
if (widget.onHide != null) widget.onHide!();
2025-08-13 15:17:13 +08:00
setState(() {
_isInitialized = true;
});
messageService.abortReply();
messageService.initializeEmpty();
2025-08-12 13:36:42 +08:00
},
child: Container(
color: Colors.transparent,
),
),
),
),
Positioned(
left: position.left,
right: position.right,
bottom: position.bottom,
child: Consumer<MessageService>(
builder: (context, messageService, child) {
final messageCount = messageService.messages.length;
if (messageCount > _lastMessageCount) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
}
_lastMessageCount = messageCount;
return TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0.8, end: 1.0),
duration: const Duration(milliseconds: 200),
curve: Curves.easeOutBack,
builder: (context, scale, child) {
return Transform.scale(
scale: scale,
child: Container(
width: chatWidth,
constraints: BoxConstraints(
minHeight: minHeight,
maxHeight: maxHeight,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(24),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
child: GradientBackground(
colors: const [
Color(0XBF3B0A3F),
Color(0xBF0E0E24),
Color(0xBF0C0B33),
],
borderRadius: BorderRadius.circular(6),
child: Stack(
children: [
Padding(
padding: const EdgeInsets.only(
top: 50, left: 6, right: 6, bottom: 30),
child: LayoutBuilder(
builder: (context, boxConstraints) {
return ChatBox(
scrollController: _scrollController,
messages: messageService.messages,
);
}),
),
Positioned(
top: 6,
right: 6,
child: IconButton(
2025-09-19 11:40:38 +08:00
icon: AssetsUtil.getImageWidget(
'open_in_full.png',
width: 24,
height: 24,
),
onPressed: _openFullScreen,
padding: EdgeInsets.zero,
),
2025-08-12 13:36:42 +08:00
),
],
2025-08-12 13:36:42 +08:00
),
),
2025-08-12 13:36:42 +08:00
),
),
2025-08-22 16:32:13 +08:00
),
);
},
);
},
2025-08-12 13:36:42 +08:00
),
),
],
);
},
),
);
}
}