diff --git a/lib/features/onboarding/screen.dart b/lib/features/onboarding/screen.dart index 94e4435..8638e07 100644 --- a/lib/features/onboarding/screen.dart +++ b/lib/features/onboarding/screen.dart @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; import '../../core/l10n/locale_provider.dart'; import '../../core/services/haptic_service.dart'; import 'provider.dart'; +import 'widgets/fade_slide_in.dart'; import 'widgets/onboarding_page.dart'; import 'widgets/onboarding_page_indicator.dart'; @@ -49,67 +50,23 @@ class _OnboardingScreenState extends ConsumerState { controller: _controller, onPageChanged: _onPageChanged, children: [ - OnboardingPage.welcome(welcomeText: s.onboardingWelcome), + OnboardingPage.welcome( + welcomeText: s.onboardingWelcome, + isActive: currentPage == 0, + ), OnboardingPage.content( icon: Icons.currency_exchange_rounded, headline: s.onboardingMultiCurrencyTitle, description: s.onboardingMultiCurrencyBody, + isActive: currentPage == 1, ), OnboardingPage.content( icon: Icons.credit_card_rounded, headline: s.onboardingCardsTitle, description: s.onboardingCardsBody, + isActive: currentPage == 2, ), - Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.waving_hand_rounded, - size: 72, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(height: 28), - Text( - s.onboardingReadyTitle, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - const SizedBox(height: 12), - Text( - s.onboardingReadyBody, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5), - ), - ), - const SizedBox(height: 24), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - s.onboardingSwipeRight, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(width: 8), - Icon( - Icons.arrow_forward_rounded, - color: Theme.of(context).colorScheme.primary, - ), - ], - ), - ], - ), - ), - ), + _ReadyPage(isActive: currentPage == 3), const SizedBox.shrink(), ], ), @@ -127,3 +84,81 @@ class _OnboardingScreenState extends ConsumerState { ); } } + +class _ReadyPage extends ConsumerWidget { + final bool isActive; + + const _ReadyPage({required this.isActive}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final s = ref.watch(stringsProvider); + final colorScheme = Theme.of(context).colorScheme; + + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + FadeSlideIn( + active: isActive, + child: Icon( + Icons.waving_hand_rounded, + size: 72, + color: colorScheme.primary, + ), + ), + const SizedBox(height: 28), + FadeSlideIn( + active: isActive, + delay: const Duration(milliseconds: 150), + child: Text( + s.onboardingReadyTitle, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + ), + const SizedBox(height: 12), + FadeSlideIn( + active: isActive, + delay: const Duration(milliseconds: 300), + child: Text( + s.onboardingReadyBody, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: colorScheme.onSurface.withOpacity(0.5), + ), + ), + ), + const SizedBox(height: 24), + FadeSlideIn( + active: isActive, + delay: const Duration(milliseconds: 450), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + s.onboardingSwipeRight, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: colorScheme.primary, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 8), + Icon( + Icons.arrow_forward_rounded, + color: colorScheme.primary, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/onboarding/widgets/fade_slide_in.dart b/lib/features/onboarding/widgets/fade_slide_in.dart new file mode 100644 index 0000000..ff9416c --- /dev/null +++ b/lib/features/onboarding/widgets/fade_slide_in.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; + +class FadeSlideIn extends StatefulWidget { + final bool active; + final Duration delay; + final Duration duration; + final Widget child; + + const FadeSlideIn({ + required this.active, + required this.child, + this.delay = Duration.zero, + this.duration = const Duration(milliseconds: 500), + super.key, + }); + + @override + State createState() => _FadeSlideInState(); +} + +class _FadeSlideInState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + late final Animation _opacity; + late final Animation _offset; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: widget.duration, + ); + _opacity = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeOut), + ); + _offset = Tween( + begin: const Offset(0, 0.15), + end: Offset.zero, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut)); + } + + @override + void didUpdateWidget(covariant FadeSlideIn oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.active && !oldWidget.active) { + _controller.reset(); + Future.delayed(widget.delay, () { + if (mounted) _controller.forward(); + }); + } else if (!widget.active && oldWidget.active) { + _controller.reset(); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Opacity( + opacity: _opacity.value, + child: FractionalTranslation( + translation: _offset.value, + child: child, + ), + ); + }, + child: widget.child, + ); + } +} diff --git a/lib/features/onboarding/widgets/onboarding_page.dart b/lib/features/onboarding/widgets/onboarding_page.dart index 60a33a5..ee529d7 100644 --- a/lib/features/onboarding/widgets/onboarding_page.dart +++ b/lib/features/onboarding/widgets/onboarding_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'casha_shimmer_text.dart'; +import 'fade_slide_in.dart'; class OnboardingPage extends StatelessWidget { final IconData? icon; @@ -7,9 +8,13 @@ class OnboardingPage extends StatelessWidget { final String? description; final String? welcomeText; final bool isWelcomePage; + final bool isActive; - const OnboardingPage.welcome({required this.welcomeText, super.key}) - : icon = null, + const OnboardingPage.welcome({ + required this.welcomeText, + this.isActive = false, + super.key, + }) : icon = null, headline = null, description = null, isWelcomePage = true; @@ -18,6 +23,7 @@ class OnboardingPage extends StatelessWidget { required this.icon, required this.headline, required this.description, + this.isActive = false, super.key, }) : welcomeText = null, isWelcomePage = false; @@ -59,34 +65,45 @@ class OnboardingPage extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Container( - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: colorScheme.primary.withOpacity(0.12), - shape: BoxShape.circle, - ), - child: Icon( - icon, - size: 64, - color: colorScheme.primary, + FadeSlideIn( + active: isActive, + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: colorScheme.primary.withOpacity(0.12), + shape: BoxShape.circle, + ), + child: Icon( + icon, + size: 64, + color: colorScheme.primary, + ), ), ), const SizedBox(height: 32), - Text( - headline!, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - ), + FadeSlideIn( + active: isActive, + delay: const Duration(milliseconds: 150), + child: Text( + headline!, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), ), const SizedBox(height: 12), - Text( - description!, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurface.withOpacity(0.6), - ), + FadeSlideIn( + active: isActive, + delay: const Duration(milliseconds: 300), + child: Text( + description!, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurface.withOpacity(0.6), + ), + ), ), ], ), diff --git a/lib/shared/widgets/pro_screen.dart b/lib/shared/widgets/pro_screen.dart index e2553b3..ab105d5 100644 --- a/lib/shared/widgets/pro_screen.dart +++ b/lib/shared/widgets/pro_screen.dart @@ -27,9 +27,9 @@ class _ProScreenState extends ConsumerState late final Animation _successScale; static const _gradientColors = [ - Color(0xFF1A237E), - Color(0xFF6A1B9A), - Color(0xFFE65100), + Color(0xFF283593), + Color(0xFF5E35B1), + Color(0xFFD81B60), ]; @override diff --git a/lib/shared/widgets/pro_subscription_card.dart b/lib/shared/widgets/pro_subscription_card.dart index 6a2dceb..fadb031 100644 --- a/lib/shared/widgets/pro_subscription_card.dart +++ b/lib/shared/widgets/pro_subscription_card.dart @@ -10,9 +10,9 @@ class ProSubscriptionCard extends ConsumerWidget { const ProSubscriptionCard({super.key}); static const _gradientColors = [ - Color(0xFF1A237E), - Color(0xFF6A1B9A), - Color(0xFFE65100), + Color(0xFF283593), + Color(0xFF5E35B1), + Color(0xFFD81B60), ]; @override @@ -24,7 +24,7 @@ class ProSubscriptionCard extends ConsumerWidget { final googleUser = googleUserAsync.value; return Container( - padding: const EdgeInsets.all(20), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24), decoration: BoxDecoration( gradient: const LinearGradient( begin: Alignment.topLeft, @@ -39,7 +39,7 @@ class ProSubscriptionCard extends ConsumerWidget { Row( children: [ Container( - padding: const EdgeInsets.all(10), + padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white.withOpacity(0.15), borderRadius: BorderRadius.circular(12), @@ -47,10 +47,10 @@ class ProSubscriptionCard extends ConsumerWidget { child: Icon( isVip ? Icons.verified_rounded : Icons.workspace_premium_rounded, color: Colors.white, - size: 24, + size: 28, ), ), - const SizedBox(width: 12), + const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -63,42 +63,55 @@ class ProSubscriptionCard extends ConsumerWidget { ), ), if (isVip) ...[ - const SizedBox(height: 2), + const SizedBox(height: 4), Text( - googleUser != null - ? '${s.proActive} \u2705' - : s.proActive, - style: Theme.of(context).textTheme.bodySmall?.copyWith( + s.proActive, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Colors.white.withOpacity(0.9), fontWeight: FontWeight.w600, ), ), + ] else ...[ + const SizedBox(height: 4), + Text( + s.proSubtitle, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.white.withOpacity(0.7), + ), + ), ], ], ), ), - OutlinedButton( - onPressed: () { - HapticService.light(); - context.push('/pro'); - }, - style: OutlinedButton.styleFrom( - foregroundColor: Colors.white, - side: BorderSide(color: Colors.white.withOpacity(0.4), width: 1.5), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () { + HapticService.light(); + context.push('/pro'); + }, + style: OutlinedButton.styleFrom( + foregroundColor: Colors.white, + side: BorderSide(color: Colors.white.withOpacity(0.4), width: 1.5), + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text( + s.proAboutPro, + style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 15), ), - ), - child: Text( - s.proAboutPro, - style: const TextStyle(fontWeight: FontWeight.w700), ), ), ], ), if (isVip && googleUser == null) ...[ - const SizedBox(height: 14), + const SizedBox(height: 10), SizedBox( width: double.infinity, child: OutlinedButton.icon(