proxybuy-flutter/lib/widgets/compact_review_input_widget...

605 lines
19 KiB
Dart

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<ReviewComposerWidget> createState() => _ReviewComposerWidgetState();
}
class _ReviewComposerWidgetState extends State<ReviewComposerWidget>
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<double> _gradientAnimation;
late List<AnimationController> _starControllers;
late List<Animation<double>> _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<double>(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<double>(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<void> _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<void> _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<double>(
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,
),
),
),
),
],
),
),
);
},
);
}
}