diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 574cc9c..9356571 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -9,7 +9,7 @@ plugins { android { namespace = "com.example.lba" compileSdk = flutter.compileSdkVersion - ndkVersion = flutter.ndkVersion + ndkVersion = "27.0.12077973" compileOptions { // استفاده از جاوا ۸ برای سازگاری بهتر diff --git a/android/build.gradle.kts b/android/build.gradle.kts index a32cae5..f2deac5 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -28,5 +28,5 @@ plugins { // The Kotlin plugin version is also managed. id("org.jetbrains.kotlin.android") apply false // اضافه کردن این خط برای تعریف پلاگین گوگل سرویسز - id("com.google.gms.google-services") version "4.4.1" apply false + id("com.google.gms.google-services") version "4.3.15" apply false } diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index 5fc28b5..280ea5b 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -19,6 +19,9 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("com.android.application") version "8.7.0" apply false + // START: FlutterFire Configuration + id("com.google.gms.google-services") version("4.3.15") apply false + // END: FlutterFire Configuration // Updated Kotlin version to the latest stable release id("org.jetbrains.kotlin.android") version "2.0.0" apply false } diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..a8ba5ec --- /dev/null +++ b/firebase.json @@ -0,0 +1 @@ +{"flutter":{"platforms":{"android":{"default":{"projectId":"lba-app-c4a7e","appId":"1:81202355575:android:6d4c7db49b6120f8239572","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"lba-app-c4a7e","configurations":{"android":"1:81202355575:android:6d4c7db49b6120f8239572","ios":"1:81202355575:ios:d40682afd403fc5b239572","macos":"1:81202355575:ios:d40682afd403fc5b239572","web":"1:81202355575:web:0cf25bf19cb4a8a1239572","windows":"1:81202355575:web:03aeac4a6dbda4d3239572"}}}}}} \ No newline at end of file diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart index e69de29..6fea7e2 100644 --- a/lib/firebase_options.dart +++ b/lib/firebase_options.dart @@ -0,0 +1,93 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + return macos; + case TargetPlatform.windows: + return windows; + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions web = FirebaseOptions( + apiKey: 'AIzaSyCeRwWv-Pep_624hoUVIw-SSlicDVRHGiY', + appId: '1:81202355575:web:0cf25bf19cb4a8a1239572', + messagingSenderId: '81202355575', + projectId: 'lba-app-c4a7e', + authDomain: 'lba-app-c4a7e.firebaseapp.com', + storageBucket: 'lba-app-c4a7e.firebasestorage.app', + measurementId: 'G-8D14ETTE9T', + ); + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyB22NqmwB_PpI1s37xbc2ABJ5_COEHeC8g', + appId: '1:81202355575:android:6d4c7db49b6120f8239572', + messagingSenderId: '81202355575', + projectId: 'lba-app-c4a7e', + storageBucket: 'lba-app-c4a7e.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyCJsr8Baaw0CbucB0zrB_kWv_7fcBNRwIk', + appId: '1:81202355575:ios:d40682afd403fc5b239572', + messagingSenderId: '81202355575', + projectId: 'lba-app-c4a7e', + storageBucket: 'lba-app-c4a7e.firebasestorage.app', + androidClientId: '81202355575-fchlrrp4fu3irskh6co1ep8i04oi3adr.apps.googleusercontent.com', + iosClientId: '81202355575-hcgc97f7glhbpa8h7iqq4d57efcsrtqo.apps.googleusercontent.com', + iosBundleId: 'com.example.lba', + ); + + static const FirebaseOptions macos = FirebaseOptions( + apiKey: 'AIzaSyCJsr8Baaw0CbucB0zrB_kWv_7fcBNRwIk', + appId: '1:81202355575:ios:d40682afd403fc5b239572', + messagingSenderId: '81202355575', + projectId: 'lba-app-c4a7e', + storageBucket: 'lba-app-c4a7e.firebasestorage.app', + androidClientId: '81202355575-fchlrrp4fu3irskh6co1ep8i04oi3adr.apps.googleusercontent.com', + iosClientId: '81202355575-hcgc97f7glhbpa8h7iqq4d57efcsrtqo.apps.googleusercontent.com', + iosBundleId: 'com.example.lba', + ); + + static const FirebaseOptions windows = FirebaseOptions( + apiKey: 'AIzaSyCeRwWv-Pep_624hoUVIw-SSlicDVRHGiY', + appId: '1:81202355575:web:03aeac4a6dbda4d3239572', + messagingSenderId: '81202355575', + projectId: 'lba-app-c4a7e', + authDomain: 'lba-app-c4a7e.firebaseapp.com', + storageBucket: 'lba-app-c4a7e.firebasestorage.app', + measurementId: 'G-2EZ542PHK0', + ); + +} \ No newline at end of file diff --git a/lib/screens/product/item.dart b/lib/screens/product/item.dart index dd85d7d..9c58757 100644 --- a/lib/screens/product/item.dart +++ b/lib/screens/product/item.dart @@ -48,6 +48,7 @@ class _ItemState extends State with TickerProviderStateMixin { "yesCount": 2, "noCount": 0, "date": "Jun 09, 2025", + "userLiked": null, }, { "name": "Khalid A", @@ -57,6 +58,7 @@ class _ItemState extends State with TickerProviderStateMixin { "yesCount": 5, "noCount": 10, "date": "Dec 26, 2024", + "userLiked": null, }, { "name": "Khalid A", @@ -66,6 +68,7 @@ class _ItemState extends State with TickerProviderStateMixin { "yesCount": 5, "noCount": 10, "date": "Dec 26, 2024", + "userLiked": null, }, { "name": "Sara M", @@ -75,6 +78,7 @@ class _ItemState extends State with TickerProviderStateMixin { "yesCount": 2, "noCount": 0, "date": "Jun 09, 2025", + "userLiked": null, }, ]; @@ -246,6 +250,32 @@ class _ItemState extends State with TickerProviderStateMixin { yesCount: review['yesCount'], noCount: review['noCount'], date: review['date'], + initialLikeState: review['userLiked'], + onLikeDislike: (isLike) { + setState(() { + if (isLike) { + if (review['userLiked'] == true) { + review['userLiked'] = null; + } else { + if (review['userLiked'] == false) { + review['noCount'] = (review['noCount'] as int) - 1; + } + review['userLiked'] = true; + review['yesCount'] = (review['yesCount'] as int) + 1; + } + } else { + if (review['userLiked'] == false) { + review['userLiked'] = null; + } else { + if (review['userLiked'] == true) { + review['yesCount'] = (review['yesCount'] as int) - 1; + } + review['userLiked'] = false; + review['noCount'] = (review['noCount'] as int) + 1; + } + } + }); + }, ), ), ); @@ -279,7 +309,7 @@ class _ItemState extends State with TickerProviderStateMixin { ), ], ), - const SizedBox(height: 20), + const SizedBox(height: 7), const PriceReserveWidget(), ], ), diff --git a/lib/utils/sharedPreferencesKey.dart b/lib/utils/sharedPreferencesKey.dart index 9bc478c..9ee058e 100644 --- a/lib/utils/sharedPreferencesKey.dart +++ b/lib/utils/sharedPreferencesKey.dart @@ -2,4 +2,7 @@ class SharedPreferencesKey { static const String token = 'token'; static const String isDarkMode = 'isDarkMode'; static const String hasManualThemeOverride = 'hasManualThemeOverride'; + static const String hasSeenOnboarding = 'hasSeenOnboarding'; + static const String selectedLanguage = 'selectedLanguage'; + static const String selectedFlag = 'selectedFlag'; } \ No newline at end of file diff --git a/lib/widgets/logout_popup.dart b/lib/widgets/logout_popup.dart index eee01c1..3f30498 100644 --- a/lib/widgets/logout_popup.dart +++ b/lib/widgets/logout_popup.dart @@ -4,6 +4,7 @@ import 'package:lba/gen/assets.gen.dart'; import 'package:lba/res/colors.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:google_sign_in/google_sign_in.dart'; +import 'package:lba/screens/auth/login_page.dart'; Future showLogoutDialog(BuildContext context) async { showDialog( @@ -121,11 +122,22 @@ class _AnimatedLogoutDialogState extends State<_AnimatedLogoutDialog> await GoogleSignIn().signOut(); await FirebaseAuth.instance.signOut(); print('✅ Logout successful'); + + if (context.mounted) { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (context) => const LoginPage(), + ), + (Route route) => false, + ); + } } catch (e) { print('❌ Logout error: $e'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('خطا در خروج: $e')), - ); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('خطا در خروج: $e')), + ); + } } }, child: Text( diff --git a/lib/widgets/reviews.dart b/lib/widgets/reviews.dart index 4aefb65..bf82de3 100644 --- a/lib/widgets/reviews.dart +++ b/lib/widgets/reviews.dart @@ -5,14 +5,17 @@ import 'package:lba/res/colors.dart'; import 'package:lba/widgets/buildWarpedInfo.dart'; import 'package:lba/widgets/rate.dart'; -class Reviews extends StatelessWidget { - +enum LikeState { none, liked, disliked } + +class Reviews extends StatefulWidget { final String name; final String comment; final double rate; final int yesCount; final int noCount; final String date; + final Function(bool isLike)? onLikeDislike; + final bool? initialLikeState; const Reviews({ super.key, @@ -22,7 +25,483 @@ class Reviews extends StatelessWidget { required this.yesCount, required this.noCount, required this.date, - }); + this.onLikeDislike, + this.initialLikeState, + }); + + @override + State createState() => _ReviewsState(); +} + +class _ReviewsState extends State with TickerProviderStateMixin { + LikeState _likeState = LikeState.none; + int _currentYesCount = 0; + int _currentNoCount = 0; + + late AnimationController _likeScaleController; + late AnimationController _dislikeScaleController; + late AnimationController _particleController; + late AnimationController _pulseController; + late AnimationController _countController; + late AnimationController _likeIconController; + late AnimationController _dislikeIconController; + + late Animation _likeScaleAnimation; + late Animation _dislikeScaleAnimation; + late Animation _particleAnimation; + late Animation _pulseAnimation; + late Animation _countAnimation; + late Animation _likeColorAnimation; + late Animation _dislikeColorAnimation; + late Animation _likeIconRotationAnimation; + late Animation _dislikeIconRotationAnimation; + late Animation _likeIconScaleAnimation; + late Animation _dislikeIconScaleAnimation; + + @override + void initState() { + super.initState(); + _currentYesCount = widget.yesCount; + _currentNoCount = widget.noCount; + + if (widget.initialLikeState == true) { + _likeState = LikeState.liked; + } else if (widget.initialLikeState == false) { + _likeState = LikeState.disliked; + } + + _likeScaleController = AnimationController( + duration: const Duration(milliseconds: 600), + vsync: this, + ); + _dislikeScaleController = AnimationController( + duration: const Duration(milliseconds: 600), + vsync: this, + ); + _particleController = AnimationController( + duration: const Duration(milliseconds: 1200), + vsync: this, + ); + _pulseController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); + _countController = AnimationController( + duration: const Duration(milliseconds: 400), + vsync: this, + ); + _likeIconController = AnimationController( + duration: const Duration(milliseconds: 500), + vsync: this, + ); + _dislikeIconController = AnimationController( + duration: const Duration(milliseconds: 500), + vsync: this, + ); + + _likeScaleAnimation = Tween(begin: 1.0, end: 1.08).animate( + CurvedAnimation(parent: _likeScaleController, curve: Curves.easeOutBack), + ); + + _dislikeScaleAnimation = Tween(begin: 1.0, end: 1.08).animate( + CurvedAnimation( + parent: _dislikeScaleController, + curve: Curves.easeOutBack, + ), + ); + + _particleAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _particleController, curve: Curves.easeOutCubic), + ); + + _pulseAnimation = Tween(begin: 1.0, end: 1.15).animate( + CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut), + ); + + _countAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _countController, curve: Curves.bounceOut), + ); + + _likeColorAnimation = ColorTween( + begin: AppColors.hint, + end: const Color(0xFF4CAF50), + ).animate(_likeScaleController); + + _dislikeColorAnimation = ColorTween( + begin: AppColors.hint, + end: const Color(0xFFF44336), + ).animate(_dislikeScaleController); + + _likeIconRotationAnimation = Tween(begin: 0.0, end: 0.1).animate( + CurvedAnimation(parent: _likeIconController, curve: Curves.elasticOut), + ); + + _dislikeIconRotationAnimation = Tween( + begin: 0.0, + end: -0.1, + ).animate( + CurvedAnimation(parent: _dislikeIconController, curve: Curves.elasticOut), + ); + + _likeIconScaleAnimation = Tween(begin: 1.0, end: 1.3).animate( + CurvedAnimation(parent: _likeIconController, curve: Curves.elasticOut), + ); + + _dislikeIconScaleAnimation = Tween(begin: 1.0, end: 1.3).animate( + CurvedAnimation(parent: _dislikeIconController, curve: Curves.elasticOut), + ); + } + + @override + void dispose() { + _likeScaleController.dispose(); + _dislikeScaleController.dispose(); + _particleController.dispose(); + _pulseController.dispose(); + _countController.dispose(); + _likeIconController.dispose(); + _dislikeIconController.dispose(); + super.dispose(); + } + + void _onLikeTap() async { + if (_likeState == LikeState.liked) { + setState(() { + _likeState = LikeState.none; + _currentYesCount--; + }); + _likeScaleController.reverse(); + } else { + if (_likeState == LikeState.disliked) { + _currentNoCount--; + _dislikeScaleController.reverse(); + } + + setState(() { + _likeState = LikeState.liked; + _currentYesCount++; + }); + + _likeScaleController.forward(); + _likeIconController.forward().then((_) => _likeIconController.reverse()); + _particleController.forward().then((_) => _particleController.reset()); + _pulseController.forward().then((_) => _pulseController.reverse()); + _countController.forward().then((_) => _countController.reset()); + } + + widget.onLikeDislike?.call(true); + } + + void _onDislikeTap() async { + if (_likeState == LikeState.disliked) { + setState(() { + _likeState = LikeState.none; + _currentNoCount--; + }); + _dislikeScaleController.reverse(); + } else { + if (_likeState == LikeState.liked) { + _currentYesCount--; + _likeScaleController.reverse(); + } + + setState(() { + _likeState = LikeState.disliked; + _currentNoCount++; + }); + + _dislikeScaleController.forward(); + _dislikeIconController.forward().then( + (_) => _dislikeIconController.reverse(), + ); + _particleController.forward().then((_) => _particleController.reset()); + _pulseController.forward().then((_) => _pulseController.reverse()); + _countController.forward().then((_) => _countController.reset()); + } + + widget.onLikeDislike?.call(false); + } + + Widget _buildParticleEffect({required bool isLike}) { + return AnimatedBuilder( + animation: _particleAnimation, + builder: (context, child) { + return CustomPaint( + painter: ParticleEffectPainter( + progress: _particleAnimation.value, + color: isLike ? const Color(0xFF4CAF50) : const Color(0xFFF44336), + ), + size: const Size(10, 10), + ); + }, + ); + } + + Widget _buildAnimatedLikeButton() { + return AnimatedBuilder( + animation: Listenable.merge([ + _likeScaleController, + _pulseController, + _countController, + ]), + builder: (context, child) { + final isLiked = _likeState == LikeState.liked; + return GestureDetector( + onTap: _onLikeTap, + child: Stack( + clipBehavior: Clip.none, + children: [ + if (isLiked) + Positioned( + child: Transform.scale( + scale: _pulseAnimation.value, + child: Container( + width: 30, + height: 25, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: _likeColorAnimation.value?.withOpacity(0.2), + ), + ), + ), + ), + + Transform.scale( + scale: _likeScaleAnimation.value, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: + isLiked + ? _likeColorAnimation.value?.withOpacity(0.1) + : null, + border: + isLiked + ? Border.all( + color: + _likeColorAnimation.value ?? + Colors.transparent, + width: 1.5, + ) + : null, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedBuilder( + animation: Listenable.merge([ + _likeColorAnimation, + _likeIconController, + ]), + builder: (context, child) { + return Transform.rotate( + angle: _likeIconRotationAnimation.value, + child: Transform.scale( + scale: + isLiked ? _likeIconScaleAnimation.value : 1.0, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: SvgPicture.asset( + isLiked + ? Assets.icons.like.path + : Assets.icons.like.path, + key: ValueKey(isLiked ? 'liked' : 'normal'), + color: + isLiked + ? _likeColorAnimation.value + : AppColors.hint, + width: 16, + height: 16, + ), + ), + ), + ); + }, + ), + const SizedBox(width: 4), + AnimatedBuilder( + animation: _countAnimation, + builder: (context, child) { + return Transform.scale( + scale: 1.0 + (_countAnimation.value * 0.1), + child: AnimatedBuilder( + animation: _likeColorAnimation, + builder: (context, child) { + return Text( + "Yes ($_currentYesCount)", + style: TextStyle( + color: + isLiked + ? _likeColorAnimation.value + : AppColors.hint, + fontWeight: + isLiked + ? FontWeight.w600 + : FontWeight.normal, + fontSize: 12 + (_countAnimation.value * 1), + ), + ); + }, + ), + ); + }, + ), + ], + ), + ), + ), + + if (isLiked) + Positioned( + top: -5, + right: -5, + child: _buildParticleEffect(isLike: true), + ), + ], + ), + ); + }, + ); + } + + Widget _buildAnimatedDislikeButton() { + return AnimatedBuilder( + animation: Listenable.merge([ + _dislikeScaleController, + _pulseController, + _countController, + ]), + builder: (context, child) { + final isDisliked = _likeState == LikeState.disliked; + return GestureDetector( + onTap: _onDislikeTap, + child: Stack( + clipBehavior: Clip.none, + children: [ + if (isDisliked) + Positioned( + child: Transform.scale( + scale: _pulseAnimation.value, + child: Container( + width: 30, + height: 25, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: _dislikeColorAnimation.value?.withOpacity(0.2), + ), + ), + ), + ), + + Transform.scale( + scale: _dislikeScaleAnimation.value, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: + isDisliked + ? _dislikeColorAnimation.value?.withOpacity(0.1) + : null, + border: + isDisliked + ? Border.all( + color: + _dislikeColorAnimation.value ?? + Colors.transparent, + width: 1.5, + ) + : null, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedBuilder( + animation: Listenable.merge([ + _dislikeColorAnimation, + _dislikeIconController, + ]), + builder: (context, child) { + return Transform.rotate( + angle: _dislikeIconRotationAnimation.value, + child: Transform.scale( + scale: + isDisliked + ? _dislikeIconScaleAnimation.value + : 1.0, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: SvgPicture.asset( + isDisliked + ? Assets.icons.dislike.path + : Assets.icons.dislike.path, + key: ValueKey( + isDisliked ? 'disliked' : 'normal', + ), + color: + isDisliked + ? _dislikeColorAnimation.value + : AppColors.hint, + width: 16, + height: 16, + ), + ), + ), + ); + }, + ), + const SizedBox(width: 4), + AnimatedBuilder( + animation: _countAnimation, + builder: (context, child) { + return Transform.scale( + scale: 1.0 + (_countAnimation.value * 0.1), + child: AnimatedBuilder( + animation: _dislikeColorAnimation, + builder: (context, child) { + return Text( + "No ($_currentNoCount)", + style: TextStyle( + color: + isDisliked + ? _dislikeColorAnimation.value + : AppColors.hint, + fontWeight: + isDisliked + ? FontWeight.w600 + : FontWeight.normal, + fontSize: 12 + (_countAnimation.value * 1), + ), + ); + }, + ), + ); + }, + ), + ], + ), + ), + ), + + if (isDisliked) + Positioned( + top: -5, + right: -5, + child: _buildParticleEffect(isLike: false), + ), + ], + ), + ); + }, + ); + } @override Widget build(BuildContext context) { @@ -38,8 +517,8 @@ class Reviews extends StatelessWidget { children: [ Row( children: [ - Text(name), - SizedBox(width: 2), + Text(widget.name), + const SizedBox(width: 2), Image.asset(Assets.images.usa.path), ], ), @@ -49,54 +528,68 @@ class Reviews extends StatelessWidget { ), ], ), - CustomStarRating(rating: rate), + CustomStarRating(rating: widget.rate), ], ), - SizedBox(height: 7), - buildWrappedInfo( - "", - comment, - ), - SizedBox(height: 10,), + const SizedBox(height: 7), + buildWrappedInfo("", widget.comment), + const SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ - InkWell( - child: Row( - children: [ - SvgPicture.asset(Assets.icons.like.path), - SizedBox(width: 1,), - Text("Yes (${yesCount.toString()})",style: TextStyle( - color: AppColors.hint, - ),), - ], - ), - ), - SizedBox(width: 10,), - InkWell( - child: Row( - children: [ - SvgPicture.asset(Assets.icons.dislike.path), - SizedBox(width: 1,), - Text("No (${noCount.toString()})",style: TextStyle( - color: AppColors.hint, - ),), - ], - ), - ), + _buildAnimatedLikeButton(), + const SizedBox(width: 7), + _buildAnimatedDislikeButton(), ], ), - Text(date,style: TextStyle( - color: AppColors.hint - ),) + Text(widget.date, style: TextStyle(color: AppColors.hint)), ], ), - SizedBox(height: 5,), - Divider(thickness: 1.2, height: 2), + const SizedBox(height: 5), + const Divider(thickness: 1.2, height: 2), ], ), ); } } + +class ParticleEffectPainter extends CustomPainter { + final double progress; + final Color color; + + ParticleEffectPainter({required this.progress, required this.color}); + + @override + void paint(Canvas canvas, Size size) { + if (progress == 0.0) return; + + final paint = + Paint() + ..color = color.withOpacity(1.0 - progress) + ..style = PaintingStyle.fill; + + final particles = [ + [0.2, -0.3, 3.0], + [0.5, -0.5, 2.5], + [0.8, -0.2, 2.0], + [0.1, -0.6, 1.5], + [0.9, -0.4, 2.0], + ]; + + for (final particle in particles) { + final x = + size.width * particle[0] + (progress * 20 * (particle[0] - 0.5)); + final y = size.height * particle[1] * progress; + final particleSize = particle[2] * (1.0 - progress * 0.5); + + canvas.drawCircle(Offset(x, y), particleSize, paint); + } + } + + @override + bool shouldRepaint(ParticleEffectPainter oldDelegate) { + return oldDelegate.progress != progress; + } +}