import 'dart:io'; import 'package:flutter/material.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'; class ReviewComposerWidget extends StatefulWidget { const ReviewComposerWidget({super.key}); @override State createState() => _ReviewComposerWidgetState(); } class _ReviewComposerWidgetState extends State with TickerProviderStateMixin { double _rating = 0; final TextEditingController _reviewController = TextEditingController(); XFile? _image; final FocusNode _focusNode = FocusNode(); bool _isFocused = false; late AnimationController _focusController; late AnimationController _buttonController; late AnimationController _starController; late AnimationController _gradientController; late Animation _gradientAnimation; late List _starControllers; late List> _starAnimations; @override void initState() { super.initState(); _focusController = AnimationController( vsync: this, duration: const Duration(milliseconds: 300), ); _buttonController = AnimationController( vsync: this, duration: const Duration(milliseconds: 400), ); _starController = AnimationController( vsync: this, duration: const Duration(milliseconds: 600), ); _gradientController = AnimationController( vsync: this, duration: const Duration(milliseconds: 800), ); _gradientAnimation = Tween(begin: 0, end: 1).animate( CurvedAnimation(parent: _gradientController, curve: Curves.easeInOut), ); _starControllers = List.generate( 5, (index) => AnimationController( vsync: this, duration: const Duration(milliseconds: 300), ), ); _starAnimations = _starControllers .map( (controller) => Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: controller, curve: Curves.elasticOut), ), ) .toList(); _focusNode.addListener(() { setState(() { _isFocused = _focusNode.hasFocus; if (_isFocused) { _focusController.forward(); } else { _focusController.reverse(); } }); }); _reviewController.addListener(() { if (_reviewController.text.isNotEmpty) { _triggerGradientAnimation(); } if (_reviewController.text.isNotEmpty || _image != null) { _buttonController.forward(); } else { _buttonController.reverse(); } }); } void _triggerGradientAnimation() { if (!_gradientController.isAnimating) { _gradientController.forward().then((_) { _gradientController.reverse(); }); } } Future _animateStarsSequentially(int targetRating) async { for (var controller in _starControllers) { controller.reset(); } for (int i = 0; i < targetRating; i++) { if (i > 0) { await Future.delayed(Duration(milliseconds: 120)); } _starControllers[i].forward().then((_) { Future.delayed(Duration(milliseconds: 100), () { if (mounted) { _starControllers[i].reverse().then((_) { _starControllers[i].forward(); }); } }); }); } } @override void dispose() { _reviewController.dispose(); _focusNode.dispose(); _focusController.dispose(); _buttonController.dispose(); _starController.dispose(); _gradientController.dispose(); for (var controller in _starControllers) { controller.dispose(); } super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _gradientAnimation, builder: (context, child) { return Transform.scale( scale: 1.0 + (_gradientAnimation.value * 0.005), child: AnimatedContainer( duration: const Duration(milliseconds: 300), margin: const EdgeInsets.symmetric(vertical: 12.0), padding: const EdgeInsets.fromLTRB(16, 12, 16, 10), decoration: BoxDecoration( gradient: LinearGradient( colors: [ AppColors.primary.withOpacity( 0.05 + (_gradientAnimation.value * 0.03), ), AppColors.borderPrimary.withOpacity( 0.03 + (_gradientAnimation.value * 0.02), ), AppColors.buttonPrimary.withOpacity( 0.04 + (_gradientAnimation.value * 0.02), ), AppColors.surface, ], begin: Alignment.topLeft, end: Alignment.bottomRight, stops: [ 0.0, 0.3 + (_gradientAnimation.value * 0.2), 0.7 + (_gradientAnimation.value * 0.1), 1.0, ], ), borderRadius: BorderRadius.circular(16), border: Border.all( color: _isFocused ? AppColors.primary.withOpacity( 0.6 + (_gradientAnimation.value * 0.2), ) : AppColors.divider.withOpacity( 0.3 + (_gradientAnimation.value * 0.1), ), width: _isFocused ? 1.5 : 1, ), boxShadow: [ if (_isFocused) BoxShadow( color: AppColors.primary.withOpacity( 0.15 + (_gradientAnimation.value * 0.05), ), blurRadius: 12 + (_gradientAnimation.value * 4), spreadRadius: 1 + (_gradientAnimation.value * 0.5), offset: const Offset(0, 4), ) else BoxShadow( color: Colors.black.withOpacity( 0.03 + (_gradientAnimation.value * 0.02), ), blurRadius: 4 + (_gradientAnimation.value * 2), spreadRadius: 0, offset: const Offset(0, 1), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildHeader(), const SizedBox(height: 10), if (_image != null) _buildImagePreview(), _buildInputSection(), const SizedBox(height: 8), _buildActionBar(), ], ), ), ); }, ); } Widget _buildHeader() { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( flex: 3, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _rating > 0 ? 'Thanks for rating!' : 'Rate Your Experience', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 15, color: _rating > 0 ? AppColors.primary : AppColors.textPrimary, ), ), if (_rating > 0) Text( _getRatingText(_rating), style: TextStyle( fontSize: 12, color: AppColors.textSecondary, fontWeight: FontWeight.w400, ), ), ], ), ), const SizedBox(width: 8), Expanded(flex: 2, child: _buildAnimatedStarRating()), ], ); } Widget _buildInputSection() { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: _isFocused ? AppColors.surface.withOpacity(0.7) : AppColors.surface.withOpacity(0.4), borderRadius: BorderRadius.circular(12), border: _isFocused ? Border.all(color: AppColors.primary.withOpacity(0.25)) : null, ), child: TextField( controller: _reviewController, focusNode: _focusNode, maxLines: null, minLines: 2, keyboardType: TextInputType.multiline, style: TextStyle( fontSize: 14, height: 1.4, color: AppColors.textPrimary, ), decoration: InputDecoration( hintText: _getHintText(), hintStyle: TextStyle( color: AppColors.textSecondary.withOpacity(0.6), fontSize: 14, height: 1.4, ), border: InputBorder.none, focusedBorder: InputBorder.none, enabledBorder: InputBorder.none, isDense: true, contentPadding: EdgeInsets.zero, ), ), ); } Widget _buildActionBar() { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [_buildImageButton(), _buildSubmitButton()], ); } Widget _buildImageButton() { return AnimatedContainer( duration: const Duration(milliseconds: 200), child: InkWell( onTap: _pickImage, borderRadius: BorderRadius.circular(10), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), decoration: BoxDecoration( color: AppColors.primary.withOpacity(0.08), borderRadius: BorderRadius.circular(10), border: Border.all(color: AppColors.primary.withOpacity(0.15)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ SvgPicture.asset( Assets.icons.galleryAdd.path, width: 16, height: 16, colorFilter: ColorFilter.mode( AppColors.primary, BlendMode.srcIn, ), ), const SizedBox(width: 6), Text( 'Photo', style: TextStyle( color: AppColors.primary, fontWeight: FontWeight.w500, fontSize: 12, ), ), ], ), ), ), ); } Future _pickImage() async { final ImagePicker picker = ImagePicker(); final XFile? image = await picker.pickImage(source: ImageSource.gallery); if (image != null) { setState(() => _image = image); _buttonController.forward(); } } void _submit() { if (_reviewController.text.isEmpty && _image == null) return; print( 'Rating: $_rating, Review: ${_reviewController.text}, Image: ${_image?.path}', ); setState(() { _rating = 0; _reviewController.clear(); _image = null; }); FocusScope.of(context).unfocus(); } Widget _buildSubmitButton() { return ScaleTransition( scale: CurvedAnimation( parent: _buttonController, curve: Curves.elasticOut, ), child: FadeTransition( opacity: _buttonController, child: Container( decoration: BoxDecoration( gradient: LinearGradient( colors: [AppColors.primary, AppColors.primary.withOpacity(0.8)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: AppColors.primary.withOpacity(0.25), blurRadius: 6, offset: const Offset(0, 3), ), ], ), child: Material( color: Colors.transparent, child: InkWell( onTap: _submit, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ SvgPicture.asset( Assets.icons.send.path, width: 16, height: 16, colorFilter: ColorFilter.mode( AppColors.surface, BlendMode.srcIn, ), ), const SizedBox(width: 6), Text( 'Post', style: TextStyle( color: AppColors.surface, fontWeight: FontWeight.w600, fontSize: 13, ), ), ], ), ), ), ), ), ), ); } String _getRatingText(double rating) { if (rating >= 4.5) return 'Excellent! 🎉'; if (rating >= 3.5) return 'Great! 👍'; if (rating >= 2.5) return 'Good 👌'; if (rating >= 1.5) return 'Fair 😐'; return 'Could be better 😔'; } String _getHintText() { if (_rating >= 4.5) { return 'Tell others what made this experience amazing...'; } else if (_rating >= 3.5) { return 'Share what you liked about this experience...'; } else if (_rating >= 2.5) { return 'Share your thoughts about this experience...'; } else if (_rating >= 1.5) { return 'Help us understand what could be improved...'; } else { return 'Share your honest feedback to help others...'; } } Widget _buildAnimatedStarRating() { return SizedBox( width: 120, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: List.generate(5, (index) { return AnimatedBuilder( animation: _starAnimations[index], builder: (context, child) { final isActive = index < _rating; final animationValue = _starAnimations[index].value; final scaleValue = isActive ? (1.0 + (animationValue * 0.1)) : 1.0; return Transform.scale( scale: scaleValue, child: InkWell( onTap: () { setState(() => _rating = index + 1.0); _animateStarsSequentially(index + 1); _starController.forward().then( (_) => _starController.reverse(), ); }, borderRadius: BorderRadius.circular(10), child: Container( width: 20, height: 20, child: Stack( alignment: Alignment.center, children: [ if (isActive && animationValue > 0.5) Container( width: 14, height: 14, decoration: BoxDecoration( shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.green.withOpacity(0.3), blurRadius: 3, spreadRadius: 1, ), ], ), ), SvgPicture.asset( isActive ? Assets.icons.starFill.path : Assets.icons.star.path, width: 13, height: 13, // colorFilter: ColorFilter.mode( // isActive // ? AppColors.primary // : AppColors.textSecondary.withOpacity(0.4), // BlendMode.srcIn, // ), ), ], ), ), ), ); }, ); }), ), ); } Widget _buildImagePreview() { return TweenAnimationBuilder( duration: const Duration(milliseconds: 400), tween: Tween(begin: 0.0, end: 1.0), curve: Curves.elasticOut, builder: (context, value, child) { return Transform.scale( scale: value, child: Container( margin: const EdgeInsets.only(bottom: 10.0), padding: const EdgeInsets.all(3), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(10), border: Border.all( color: AppColors.primary.withOpacity(0.15), width: 1, ), boxShadow: [ BoxShadow( color: AppColors.primary.withOpacity(0.08), blurRadius: 4, offset: const Offset(0, 1), ), ], ), child: Stack( children: [ ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.file( File(_image!.path), width: 60, height: 60, fit: BoxFit.cover, ), ), Positioned( top: -1, right: -1, child: GestureDetector( onTap: () => setState(() => _image = null), child: Container( padding: const EdgeInsets.all(3), decoration: BoxDecoration( color: Colors.red.withOpacity(0.85), shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.2), blurRadius: 2, offset: const Offset(0, 1), ), ], ), child: const Icon( Icons.close, color: Colors.white, size: 12, ), ), ), ), ], ), ), ); }, ); } }