proxybuy-flutter/lib/screens/mains/planner/planner.dart

1283 lines
38 KiB
Dart
Raw 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:async';
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:image_picker/image_picker.dart';
import 'package:lba/gen/assets.gen.dart';
import 'package:lba/res/colors.dart';
import 'package:flutter_sound/flutter_sound.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
class Planner extends StatefulWidget {
const Planner({super.key});
@override
State<Planner> createState() => _PlannerState();
}
class _PlannerState extends State<Planner> with TickerProviderStateMixin {
final ImagePicker _picker = ImagePicker();
final GlobalKey _attachIconKey = GlobalKey();
final TextEditingController _textController = TextEditingController();
bool _isLoading = false;
String _sentText = "";
XFile? _sentImage;
String? _sentAudioPath;
XFile? _attachedImage;
FlutterSoundRecorder? _audioRecorder;
final AudioPlayer _audioPlayer = AudioPlayer();
bool _isRecording = false;
bool _isPlaying = false;
String? _recordedAudioPath;
Duration _recordDuration = Duration.zero;
Duration _playDuration = Duration.zero;
Duration _totalDuration = Duration.zero;
Timer? _recordingTimer;
late AnimationController _micPulseController;
late Animation<double> _micPulseAnimation;
late AnimationController _pageEnterController;
late Animation<double> _staggeredAnimation;
late Animation<Offset> _textInputAnimation;
final List<String> suggestions = [
'running shoes',
'hotel',
'power tools',
'food',
'cafe',
'fashion',
'gift',
'indoor plants',
];
@override
void initState() {
super.initState();
_initRecorder();
_micPulseController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1000),
)..repeat(reverse: true);
_micPulseAnimation =
Tween<double>(begin: 1.0, end: 1.4).animate(_micPulseController);
_pageEnterController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
);
_staggeredAnimation = CurvedAnimation(
parent: _pageEnterController,
curve: Curves.easeOutCubic,
);
_textInputAnimation = Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _pageEnterController,
curve: const Interval(0.4, 1.0, curve: Curves.easeOutCubic),
));
_pageEnterController.forward();
}
Future<void> _initRecorder() async {
try {
_audioRecorder = FlutterSoundRecorder();
await _audioRecorder!.openRecorder();
} catch (e) {
debugPrint('Error initializing recorder: $e');
}
}
@override
void dispose() {
_textController.dispose();
_audioRecorder?.closeRecorder();
_audioPlayer.dispose();
_recordingTimer?.cancel();
_micPulseController.dispose();
_pageEnterController.dispose();
super.dispose();
}
void _showAttachmentOptions() {
final RenderBox renderBox =
_attachIconKey.currentContext!.findRenderObject() as RenderBox;
final position = renderBox.localToGlobal(Offset.zero);
const double menuWidth = 220.0;
const double menuHeight = 120.0;
showGeneralDialog(
context: context,
barrierDismissible: true,
barrierLabel: 'Attachment Options',
barrierColor: Colors.black.withOpacity(0.1),
transitionDuration: const Duration(milliseconds: 200),
pageBuilder: (context, anim1, anim2) {
return GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Material(
color: Colors.transparent,
child: Stack(
children: [
Positioned(
left: position.dx + 30,
top: position.dy - menuHeight + 5,
child: GestureDetector(
onTap: () {},
child: PopupMenuWithoutTail(
child: SizedBox(
width: menuWidth,
height: menuHeight,
child: _buildAttachmentMenuContent(),
),
),
),
),
],
),
),
);
},
transitionBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut),
child: child,
);
},
);
}
Widget _buildAttachmentMenuContent() {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ListTile(
leading: SvgPicture.asset(Assets.icons.camera2.path, height: 20),
title: const Text('Snap a photo', style: TextStyle(fontSize: 16)),
onTap: () {
Navigator.of(context).pop();
_pickImage(ImageSource.camera);
},
dense: true,
),
const Divider(thickness: 0.5, height: 1, indent: 55, endIndent: 55),
ListTile(
leading: SvgPicture.asset(Assets.icons.galleryAdd.path, height: 19),
title: const Text('Upload a photo', style: TextStyle(fontSize: 16)),
onTap: () {
Navigator.of(context).pop();
_pickImage(ImageSource.gallery);
},
dense: true,
),
],
);
}
Future<void> _pickImage(ImageSource source) async {
try {
final XFile? pickedFile = await _picker.pickImage(source: source);
if (pickedFile != null) {
setState(() {
_attachedImage = pickedFile;
});
}
} catch (e) {
debugPrint("Error picking image: $e");
}
}
void _removeAttachedImage() {
setState(() {
_attachedImage = null;
});
}
void _sendMessage() {
final text = _textController.text.trim();
final imageFile = _attachedImage;
final audioPath = _recordedAudioPath;
if (text.isNotEmpty || imageFile != null || audioPath != null) {
setState(() {
_isLoading = true;
_sentText = text;
_sentImage = imageFile;
_sentAudioPath = audioPath;
_textController.clear();
_attachedImage = null;
_recordedAudioPath = null;
_recordDuration = Duration.zero;
});
Future.delayed(const Duration(seconds: 3), () {
if (mounted) {
setState(() {
_isLoading = false;
});
}
});
}
}
Future<void> _startRecording() async {
try {
HapticFeedback.mediumImpact();
final permission = await Permission.microphone.request();
if (permission != PermissionStatus.granted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Microphone permission is required to record audio.')),
);
return;
}
if (_audioRecorder == null) {
await _initRecorder();
}
final Directory appDocumentsDir =
await getApplicationDocumentsDirectory();
final String filePath =
'${appDocumentsDir.path}/recording_${DateTime.now().millisecondsSinceEpoch}.aac';
await _audioRecorder!.startRecorder(
toFile: filePath,
codec: Codec.aacADTS,
);
setState(() {
_isRecording = true;
_recordDuration = Duration.zero;
});
_startRecordTimer();
} catch (e) {
debugPrint('Error starting recording: $e');
}
}
Future<void> _stopRecording() async {
try {
HapticFeedback.lightImpact();
_recordingTimer?.cancel();
final path = await _audioRecorder!.stopRecorder();
setState(() {
_isRecording = false;
_recordedAudioPath = path;
});
} catch (e) {
debugPrint('Error stopping recording: $e');
}
}
void _startRecordTimer() {
_recordingTimer?.cancel();
_recordingTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_isRecording && mounted) {
setState(() {
_recordDuration += const Duration(seconds: 1);
});
} else {
timer.cancel();
}
});
}
Future<void> _playRecording() async {
if (_recordedAudioPath == null) return;
try {
if (_isPlaying) {
await _audioPlayer.pause();
setState(() {
_isPlaying = false;
});
} else {
await _audioPlayer.play(DeviceFileSource(_recordedAudioPath!));
setState(() {
_isPlaying = true;
});
_audioPlayer.onDurationChanged.listen((duration) {
if (mounted) {
setState(() {
_totalDuration = duration;
});
}
});
_audioPlayer.onPositionChanged.listen((position) {
if (mounted) {
setState(() {
_playDuration = position;
});
}
});
_audioPlayer.onPlayerComplete.listen((_) {
if (mounted) {
setState(() {
_isPlaying = false;
_playDuration = Duration.zero;
});
}
});
}
} catch (e) {
debugPrint('Error playing recording: $e');
}
}
void _deleteRecording() {
setState(() {
_recordedAudioPath = null;
_recordDuration = Duration.zero;
_playDuration = Duration.zero;
_totalDuration = Duration.zero;
_isPlaying = false;
});
}
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, "0");
String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
return "$twoDigitMinutes:$twoDigitSeconds";
}
Widget _buildAnimatedSection(Widget child,
{required double begin, required double end}) {
return FadeTransition(
opacity: CurvedAnimation(
parent: _staggeredAnimation,
curve: Interval(begin, end),
),
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.0, 0.5),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _staggeredAnimation,
curve: Interval(begin, end),
)),
child: child,
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 1,
shadowColor: Colors.grey.withOpacity(0.2),
surfaceTintColor: Colors.transparent,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black54),
onPressed: () {
if (_isLoading) {
setState(() {
_isLoading = false;
});
} else {
Navigator.pop(context);
}
},
),
title: const Text(
'Smart Planner',
style: TextStyle(
color: Colors.black,
fontSize: 18,
fontWeight: FontWeight.normal),
),
actions: [
IconButton(
icon: SvgPicture.asset(Assets.icons.mDSPublicTWButton1.path),
onPressed: () {},
),
],
),
body: Column(
children: [
Expanded(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
transitionBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
child: ScaleTransition(
scale: Tween<double>(begin: 0.9, end: 1.0).animate(animation),
child: child,
),
);
},
child: _isLoading ? _buildLoadingView() : _buildDefaultView(),
),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) {
return SizeTransition(
sizeFactor: animation,
child: FadeTransition(opacity: animation, child: child),
);
},
child: _attachedImage != null
? _buildAttachedImagePreview()
: const SizedBox.shrink(),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) {
return SizeTransition(
sizeFactor: animation,
child: FadeTransition(opacity: animation, child: child),
);
},
child: _recordedAudioPath != null
? _buildRecordedAudioPreview()
: const SizedBox.shrink(),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) {
return SizeTransition(
sizeFactor: animation,
child: FadeTransition(opacity: animation, child: child),
);
},
child: _isRecording
? _buildRecordingIndicator()
: const SizedBox.shrink(),
),
SlideTransition(
position: _textInputAnimation,
child: _buildTextInputArea(),
),
],
),
);
}
Widget _buildDefaultView() {
return ListView(
key: const ValueKey('default'),
padding: const EdgeInsets.all(16.0),
children: [
_buildAnimatedSection(
_buildChatBubble(
icon: Assets.icons.flatColorIconsPlanner.path,
text:
"\"Hi there! Tell me what you need I'm here to plan it for you!\" ",
backgroundColor: const Color(0xFFEFF7FE),
iconBackgroundColor: Colors.transparent,
),
begin: 0.0,
end: 0.5,
),
const SizedBox(height: 16),
_buildAnimatedSection(
_buildSuggestionArea(suggestions),
begin: 0.2,
end: 1.0,
),
],
);
}
Widget _buildLoadingView() {
return Padding(
key: const ValueKey('loading'),
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Column(
children: [
_buildUserRequestBubble(),
const Padding(
padding: EdgeInsets.symmetric(vertical: 15.0),
child: Divider(color: Color.fromARGB(255, 224, 224, 224)),
),
const Text(
"\"Scanning the best deals just for you...\"",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
color: Color(0xFF37474F),
height: 1.4,
),
),
const SizedBox(height: 12),
const SlantedLinesAnimation(isLoading: true),
],
),
);
}
Widget _buildUserRequestBubble() {
bool hasImage = _sentImage != null;
bool hasText = _sentText.isNotEmpty;
bool hasAudio = _sentAudioPath != null;
if (hasImage || hasAudio) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (hasImage)
Center(
child: Padding(
padding: const EdgeInsets.only(top: 0.0, right: 12.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(12.0),
child: Image.file(
File(_sentImage!.path),
width: 75,
height: 75,
fit: BoxFit.cover,
),
),
),
),
if (hasAudio && !hasImage)
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFEFF7FE),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: LightAppColors.primary.withOpacity(0.3)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 32,
height: 32,
decoration: const BoxDecoration(
color: LightAppColors.primary,
shape: BoxShape.circle,
),
child: const Icon(
Icons.volume_up,
color: Colors.white,
size: 16,
),
),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Voice Message',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.grey[700],
),
),
Text(
_formatDuration(_recordDuration),
style: TextStyle(
fontSize: 10,
color: Colors.grey[600],
),
),
],
),
],
),
),
if (hasText)
Flexible(
child: Container(
padding:
const EdgeInsets.symmetric(vertical: 6, horizontal: 16),
decoration: BoxDecoration(
color: const Color(0xFFEFF7FE),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade300),
),
child: Text(
"\"$_sentText\"",
style: const TextStyle(
fontSize: 15,
color: Color(0xFF37474F),
height: 1.4,
),
),
),
),
],
);
} else {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
child: IntrinsicHeight(
child: Container(
decoration: BoxDecoration(
color: const Color(0xFFEFF7FE),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade300),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 6, horizontal: 16),
child: Text(
"\"$_sentText\"",
style: const TextStyle(
fontSize: 15,
color: Color(0xFF37474F),
height: 1.4,
),
),
),
),
Container(
width: 10,
decoration: const BoxDecoration(
color: Color(0xFF189CFF),
borderRadius: BorderRadius.only(
topRight: Radius.circular(16),
bottomRight: Radius.circular(16),
),
),
),
],
),
),
),
),
],
);
}
}
Widget _buildAttachedImagePreview() {
return Padding(
key: const ValueKey('image_preview'),
padding: const EdgeInsets.fromLTRB(35, 8, 16, 0),
child: Align(
alignment: Alignment.centerLeft,
child: SizedBox(
height: 120,
width: 120,
child: Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(16.0),
child: Image.file(
File(_attachedImage!.path),
height: 120,
width: 120,
fit: BoxFit.cover,
),
),
Positioned(
top: 6,
left: 6,
child: GestureDetector(
onTap: _removeAttachedImage,
child: Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.6),
shape: BoxShape.circle,
),
padding: const EdgeInsets.all(4),
child: const Icon(Icons.close,
color: Colors.black, size: 18),
),
),
),
],
),
),
),
);
}
Widget _buildRecordedAudioPreview() {
return Padding(
key: const ValueKey('audio_preview'),
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF8F9FA),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: LightAppColors.primary.withOpacity(0.3)),
),
child: Row(
children: [
GestureDetector(
onTap: _playRecording,
child: Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
color: LightAppColors.primary,
shape: BoxShape.circle,
),
child: Icon(
_isPlaying ? Icons.pause : Icons.play_arrow,
color: Colors.white,
size: 20,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.mic,
size: 16, color: LightAppColors.primary),
const SizedBox(width: 4),
Text(
'Voice Message',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.grey[700],
),
),
],
),
const SizedBox(height: 4),
Row(
children: [
Expanded(
child: LinearProgressIndicator(
value: _totalDuration.inMilliseconds > 0
? _playDuration.inMilliseconds /
_totalDuration.inMilliseconds
: 0.0,
backgroundColor: Colors.grey[300],
valueColor: const AlwaysStoppedAnimation<Color>(
LightAppColors.primary),
minHeight: 2,
),
),
const SizedBox(width: 8),
Text(
_formatDuration(_playDuration),
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
Text(
' / ${_formatDuration(_recordDuration)}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
],
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: _deleteRecording,
child: Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
shape: BoxShape.circle,
),
child: const Icon(
Icons.delete_outline,
color: Colors.red,
size: 18,
),
),
),
],
),
),
);
}
Widget _buildRecordingIndicator() {
return Padding(
key: const ValueKey('recording_indicator'),
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.red.withOpacity(0.3)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 500),
width: 12,
height: 12,
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.red.withOpacity(0.5),
blurRadius: 8,
spreadRadius: 2,
),
],
),
),
const SizedBox(width: 12),
const Icon(
Icons.mic,
color: Colors.red,
size: 20,
),
const SizedBox(width: 8),
Text(
'Recording... ${_formatDuration(_recordDuration)}',
style: const TextStyle(
color: Colors.red,
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
const SizedBox(width: 12),
const RecordingPulseAnimation(),
],
),
),
);
}
Widget _buildChatBubble({
required String icon,
required String text,
required Color backgroundColor,
required Color iconBackgroundColor,
}) {
return Container(
padding: const EdgeInsets.all(12.0),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(16),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: iconBackgroundColor,
borderRadius: BorderRadius.circular(10),
),
child: SvgPicture.asset(icon, height: 24),
),
const SizedBox(width: 12),
Expanded(
child: Padding(
padding: const EdgeInsets.only(top: 2.0),
child: Text(
text,
style: const TextStyle(
fontSize: 15,
color: Color(0xFF37474F),
height: 1.4,
),
),
),
),
],
),
);
}
Widget _buildSuggestionArea(List<String> suggestions) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 16.0),
decoration: BoxDecoration(
color: const Color(0xFFF7F7F7),
borderRadius: BorderRadius.circular(16),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 2.0),
child: SvgPicture.asset(Assets.icons.flatColorIconsIdea.path),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"\"Need help? Here are some ideas you can ask me about!\"",
style: TextStyle(
fontSize: 15,
color: Color(0xFF37474F),
height: 1.4,
),
),
const SizedBox(height: 16),
Wrap(
spacing: 10.0,
runSpacing: 10.0,
alignment: WrapAlignment.start,
children: suggestions.asMap().entries.map((entry) {
final index = entry.key;
final suggestion = entry.value;
return FadeTransition(
opacity: CurvedAnimation(
parent: _staggeredAnimation,
curve: Interval(0.4 + (index * 0.05), 1.0),
),
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.2, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _staggeredAnimation,
curve: Interval(0.4 + (index * 0.05), 1.0),
)),
child: _buildSuggestionChip(suggestion),
),
);
}).toList(),
),
],
),
),
],
),
);
}
Widget _buildSuggestionChip(String text) {
return GestureDetector(
onTap: () {
_textController.text = text;
_textController.selection = TextSelection.fromPosition(
TextPosition(offset: _textController.text.length));
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
border: Border.all(color: LightAppColors.primary, width: 1),
),
child: Text(
text,
style: const TextStyle(
color: Color(0xFF546E7A),
fontWeight: FontWeight.w500,
fontSize: 14),
),
),
);
}
Widget _buildTextInputArea() {
return SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
color: LightAppColors.nearbyPopup,
borderRadius: BorderRadius.circular(35),
border: Border.all(color: Colors.grey.shade300),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
key: _attachIconKey,
onPressed: _showAttachmentOptions,
icon: SvgPicture.asset(Assets.icons.link2.path),
),
Expanded(
child: TextField(
controller: _textController,
decoration: const InputDecoration(
hintText: "Type your request here and let's plan it!",
hintStyle: TextStyle(color: Colors.grey, fontSize: 14),
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
contentPadding:
EdgeInsets.symmetric(vertical: 0, horizontal: 3),
),
onSubmitted: (_) => _sendMessage(),
),
),
GestureDetector(
onLongPressStart: (_) => _startRecording(),
onLongPressEnd: (_) => _stopRecording(),
child: AnimatedBuilder(
animation: _micPulseAnimation,
builder: (context, child) {
return Transform.scale(
scale: _isRecording ? _micPulseAnimation.value : 1.0,
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: _isRecording
? Colors.red
: Colors.transparent,
shape: BoxShape.circle,
boxShadow: _isRecording
? [
BoxShadow(
color: Colors.red.withOpacity(0.3),
blurRadius: 8,
spreadRadius: 2,
),
]
: null,
),
child: SvgPicture.asset(
Assets.icons.microphone2.path,
colorFilter: _isRecording
? const ColorFilter.mode(
Colors.white, BlendMode.srcIn)
: null,
),
),
);
}),
),
IconButton(
icon: SvgPicture.asset(Assets.icons.arrowUp.path),
onPressed: _sendMessage,
),
],
),
),
),
);
}
}
class SlantedLinesAnimation extends StatefulWidget {
final bool isLoading;
const SlantedLinesAnimation({super.key, this.isLoading = false});
@override
_SlantedLinesAnimationState createState() => _SlantedLinesAnimationState();
}
class _SlantedLinesAnimationState extends State<SlantedLinesAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return ClipRRect(
borderRadius: BorderRadius.circular(4),
child: CustomPaint(
painter: _SlantedLinesPainter(_controller.value),
child: const SizedBox(height: 8, width: 180),
),
);
},
);
}
}
class _SlantedLinesPainter extends CustomPainter {
final double progress;
_SlantedLinesPainter(this.progress);
@override
void paint(Canvas canvas, Size size) {
const double stripeWidth = 5.0;
const double angle = -0.5;
final Paint paint = Paint()
..shader = LinearGradient(
colors: [const Color(0xFF189CFF), LightAppColors.loadingBorder],
stops: const [0.5, 0.5],
tileMode: TileMode.repeated,
transform: GradientRotation(angle),
).createShader(
Rect.fromLTWH(
-progress * stripeWidth * 4,
0,
stripeWidth * 4,
size.height,
),
);
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), paint);
}
@override
bool shouldRepaint(covariant _SlantedLinesPainter oldDelegate) {
return oldDelegate.progress != progress;
}
}
class PopupMenuWithoutTail extends StatelessWidget {
final Widget child;
const PopupMenuWithoutTail({super.key, required this.child});
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _MenuPainter(
color: Colors.white,
shadowColor: Colors.black.withOpacity(0.15),
),
child: child,
);
}
}
class _MenuPainter extends CustomPainter {
final Color color;
final Color shadowColor;
final double radius = 20.0;
_MenuPainter({required this.color, required this.shadowColor});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..style = PaintingStyle.fill;
final path = Path()
..moveTo(radius, 0)
..lineTo(size.width - radius, 0)
..arcToPoint(Offset(size.width, radius),
radius: Radius.circular(radius))
..lineTo(size.width, size.height - radius)
..arcToPoint(Offset(size.width - radius, size.height),
radius: Radius.circular(radius))
..lineTo(0, size.height)
..lineTo(0, radius)
..arcToPoint(Offset(radius, 0), radius: Radius.circular(radius))
..close();
canvas.drawShadow(path, shadowColor, 10.0, true);
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
class RecordingPulseAnimation extends StatefulWidget {
const RecordingPulseAnimation({super.key});
@override
_RecordingPulseAnimationState createState() =>
_RecordingPulseAnimationState();
}
class _RecordingPulseAnimationState extends State<RecordingPulseAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
)..repeat(reverse: true);
_animation = Tween<double>(
begin: 0.5,
end: 1.0,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Row(
children: List.generate(3, (index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 1),
child: AnimatedContainer(
duration: Duration(milliseconds: 200 + (index * 100)),
height: 16 * _animation.value,
width: 3,
decoration: BoxDecoration(
color: Colors.red.withOpacity(_animation.value),
borderRadius: BorderRadius.circular(2),
),
),
);
}),
);
},
);
}
}