import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:image_picker/image_picker.dart'; import 'package:intl/intl.dart'; import 'package:lba/gen/assets.gen.dart'; import 'package:lba/res/colors.dart'; import 'package:lba/widgets/chat_message_audio_player.dart'; import 'package:lba/widgets/chat_text_input_field.dart'; import 'package:lba/widgets/profile_app_bar.dart'; import 'package:lba/widgets/recorded_audio_preview.dart'; import 'package:lba/widgets/recording_indicator.dart'; import 'package:flutter_sound/flutter_sound.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:vibration/vibration.dart'; class ChatMessage { final String? text; final String? imagePath; final String? audioPath; final Duration? audioDuration; final String time; final bool isUser; ChatMessage({ this.text, this.imagePath, this.audioPath, this.audioDuration, required this.time, required this.isUser, }); } class HelpAndSupportPage extends StatefulWidget { const HelpAndSupportPage({super.key}); @override State createState() => _HelpAndSupportPageState(); } class _HelpAndSupportPageState extends State with TickerProviderStateMixin { final TextEditingController _textController = TextEditingController(); final List _messages = []; final GlobalKey _listKey = GlobalKey(); late AnimationController _pageEnterAnimationController; final ImagePicker _picker = ImagePicker(); final GlobalKey _attachIconKey = GlobalKey(); XFile? _attachedImage; FlutterSoundRecorder? _audioRecorder; bool _isRecording = false; String? _recordedAudioPath; Duration _recordDuration = Duration.zero; Timer? _recordingTimer; late AnimationController _micPulseController; late Animation _micPulseAnimation; @override void initState() { super.initState(); _pageEnterAnimationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 800), ); _micPulseController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1000), )..repeat(reverse: true); _micPulseAnimation = Tween(begin: 1.0, end: 1.4).animate(_micPulseController); _initRecorder(); _loadInitialMessages(); } Future _initRecorder() async { try { _audioRecorder = FlutterSoundRecorder(); await _audioRecorder!.openRecorder(); } catch (e) { debugPrint('Error initializing recorder: $e'); } } void _loadInitialMessages() { Future.delayed(const Duration(milliseconds: 500), () { _addMessage(ChatMessage( text: "Hello! How can I assist you today?", time: DateFormat('HH:mm').format(DateTime.now()), isUser: false, )); _pageEnterAnimationController.forward(); }); } @override void dispose() { _textController.dispose(); _pageEnterAnimationController.dispose(); _audioRecorder?.closeRecorder(); _recordingTimer?.cancel(); _micPulseController.dispose(); super.dispose(); } void _sendMessage() { final text = _textController.text.trim(); final imageFile = _attachedImage; final audioPath = _recordedAudioPath; if (text.isNotEmpty || imageFile != null || audioPath != null) { final message = ChatMessage( text: text.isNotEmpty ? text : null, imagePath: imageFile?.path, audioPath: audioPath, audioDuration: audioPath != null ? _recordDuration : null, time: DateFormat('HH:mm').format(DateTime.now()), isUser: true, ); _addMessage(message); _textController.clear(); setState(() { _attachedImage = null; _recordedAudioPath = null; _recordDuration = Duration.zero; }); Future.delayed(const Duration(seconds: 2), () { _addMessage(ChatMessage( text: "Thanks for reaching out! A support agent will review your message and get back to you shortly.", time: DateFormat('HH:mm').format(DateTime.now()), isUser: false, )); }); } } void _addMessage(ChatMessage message) { _messages.add(message); _listKey.currentState?.insertItem(_messages.length - 1, duration: const Duration(milliseconds: 400)); } void _showAttachmentOptions() { showModalBottomSheet( context: context, backgroundColor: Colors.transparent, builder: (context) => Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: SvgPicture.asset(Assets.icons.camera2.path, height: 20, color: AppColors.textSecondary), title: Text('Take a photo', style: TextStyle(color: AppColors.textPrimary)), onTap: () { Navigator.of(context).pop(); _pickImage(ImageSource.camera); }, ), Divider(color: AppColors.divider), ListTile( leading: SvgPicture.asset(Assets.icons.galleryAdd.path, height: 19, color: AppColors.textSecondary), title: Text('Choose from gallery', style: TextStyle(color: AppColors.textPrimary)), onTap: () { Navigator.of(context).pop(); _pickImage(ImageSource.gallery); }, ), ], ), ), ); } Future _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); Future _startRecording() async { final permission = await Permission.microphone.request(); if (permission != PermissionStatus.granted) return; try { if (await Vibration.hasVibrator() == true) { Vibration.vibrate(duration: 100); } final Directory tempDir = await getTemporaryDirectory(); final String filePath = '${tempDir.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 _stopRecording() async { if (!_isRecording) return; try { _recordingTimer?.cancel(); final path = await _audioRecorder!.stopRecorder(); setState(() { _isRecording = false; _recordedAudioPath = path; }); } catch (e) { debugPrint('Error stopping recording: $e'); } } void _startRecordTimer() { _recordingTimer = Timer.periodic(const Duration(seconds: 1), (timer) { setState(() => _recordDuration += const Duration(seconds: 1)); }); } void _deleteRecording() => setState(() { _recordedAudioPath = null; _recordDuration = Duration.zero; }); 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"; } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.scaffoldBackground, appBar: const ProfileAppBar( title: 'Help & Support', showBackButton: true, ), body: Container( decoration: BoxDecoration( color: AppColors.scaffoldBackground, image: DecorationImage( image: AssetImage(Assets.images.seamlessPatternWithShoppingCartsGiftSalevectorBackground6780995561.path), fit: BoxFit.cover, ), ), child: Column( children: [ Expanded( child: AnimatedList( key: _listKey, initialItemCount: _messages.length, padding: const EdgeInsets.all(16.0), itemBuilder: (context, index, animation) { return _buildMessageItem(_messages[index], animation); }, ), ), if (_attachedImage != null) _buildAttachedImagePreview(), if (_recordedAudioPath != null) RecordedAudioPreview( audioPath: _recordedAudioPath!, totalDuration: _recordDuration, onDelete: _deleteRecording, formatDuration: _formatDuration, ), if (_isRecording) RecordingIndicator( formattedDuration: _formatDuration(_recordDuration), ), SlideTransition( position: Tween( begin: const Offset(0, 1), end: Offset.zero, ).animate(CurvedAnimation( parent: _pageEnterAnimationController, curve: Curves.easeOutCubic, )), child: ChatTextInputField( textController: _textController, onSendMessage: _sendMessage, onShowAttachmentOptions: _showAttachmentOptions, onStartRecording: _startRecording, onStopRecording: _stopRecording, isRecording: _isRecording, micPulseAnimation: _micPulseAnimation, attachIconKey: _attachIconKey, hintText: "Type your message...", ), ), ], ), ), ); } Widget _buildMessageItem(ChatMessage message, Animation animation) { final isUser = message.isUser; final slideAnimation = Tween( begin: isUser ? const Offset(1, 0) : const Offset(-1, 0), end: Offset.zero, ).animate(CurvedAnimation(parent: animation, curve: Curves.easeOutCubic)); return FadeTransition( opacity: animation, child: SlideTransition( position: slideAnimation, child: Align( alignment: isUser ? Alignment.centerRight : Alignment.centerLeft, child: Column( crossAxisAlignment: isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ Container( margin: const EdgeInsets.symmetric(vertical: 6.0), padding: const EdgeInsets.symmetric( horizontal: 16.0, vertical: 10.0), constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.75, ), decoration: BoxDecoration( color: isUser ? AppColors.confirmPopup : AppColors.nearbyPopup, borderRadius: BorderRadius.circular(20.0).subtract( isUser ? const BorderRadius.only(bottomRight: Radius.circular(16)) : const BorderRadius.only(bottomLeft: Radius.circular(16)), ), boxShadow: [ BoxShadow( color: AppColors.shadowColor, blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ if (message.text != null) Text( message.text!, style: TextStyle( color: isUser ? Colors.white : AppColors.textPrimary, fontSize: 16.0), ), if (message.imagePath != null) Padding( padding: const EdgeInsets.only(top: 4.0, bottom: 4.0), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.file( File(message.imagePath!), width: 200, fit: BoxFit.cover, ), ), ), if (message.audioPath != null) SizedBox( width: MediaQuery.of(context).size.width * 0.5, child: ChatMessageAudioPlayer( audioPath: message.audioPath!, isUser: message.isUser, audioDuration: message.audioDuration, ), ), ], ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0), child: Text( message.time, style: TextStyle(color: AppColors.textSecondary, fontSize: 12.0), ), ), ], ), ), ), ); } Widget _buildAttachedImagePreview() { return Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), child: Align( alignment: Alignment.centerLeft, child: Stack( children: [ ClipRRect( borderRadius: BorderRadius.circular(12.0), child: Image.file( File(_attachedImage!.path), height: 100, width: 100, fit: BoxFit.cover, ), ), Positioned( top: 4, right: 4, child: GestureDetector( onTap: _removeAttachedImage, child: Container( decoration: BoxDecoration(color: AppColors.textPrimary.withOpacity(0.7), shape: BoxShape.circle), child: const Icon(Icons.close, color: Colors.white, size: 18), ), ), ), ], ), ), ); } }