diff --git a/lib/app/router.dart b/lib/app/router.dart index 87f4e85..de175f8 100644 --- a/lib/app/router.dart +++ b/lib/app/router.dart @@ -10,17 +10,15 @@ import '../features/settings/categories/category_manager_screen.dart'; import '../features/onboarding/screen.dart'; import '../shared/models/transaction.dart'; import '../shared/paywall/paywall_screen.dart'; -import '../shared/providers/onboarding_provider.dart'; +import '../shared/services/onboarding_service.dart'; final _shellKey = GlobalKey(); final appRouter = GoRouter( initialLocation: '/dashboard', redirect: (context, state) { - final service = ProviderScope.containerOf(context) - .read(onboardingServiceProvider); final location = state.uri.toString(); - if (service.shouldShowOnboarding && location != '/onboarding') { + if (OnboardingService.shouldShowOnboarding && location != '/onboarding') { return '/onboarding'; } return null; diff --git a/lib/core/l10n/app_strings.dart b/lib/core/l10n/app_strings.dart index 60bbbe6..b4e5d8c 100644 --- a/lib/core/l10n/app_strings.dart +++ b/lib/core/l10n/app_strings.dart @@ -264,7 +264,7 @@ class AppStrings { String get onboardingWelcome => _ru ? 'Добро пожаловать в' : 'Welcome to'; String get onboardingMultiCurrencyTitle => - _ru ? 'Все деньги на одном экране' : 'All Your Money, One View'; + _ru ? 'Все деньги\nна одном экране' : 'All Your Money,\nOne View'; String get onboardingMultiCurrencyBody => _ru ? 'Счета в разных валютах с автоматической конвертацией по актуальным курсам' : 'Accounts in different currencies, automatically converted at live exchange rates'; @@ -273,5 +273,9 @@ class AppStrings { String get onboardingCardsBody => _ru ? 'Наклоните телефон — и карточка оживает. Градиенты, цвета и высота — всё настраивается под вас' : 'Tilt your phone and watch them respond. Customize gradients, colors and height to make them yours'; - String get onboardingGetStarted => _ru ? 'Начать' : 'Get Started'; + String get onboardingReadyTitle => _ru ? 'Всё готово' : "You're All Set"; + String get onboardingReadyBody => _ru + ? 'Если готовы — свайпните вправо, чтобы открыть приложение' + : "If you're ready — swipe right to open the app"; + String get onboardingSwipeRight => _ru ? 'Свайп вправо' : 'Swipe Right'; } diff --git a/lib/features/onboarding/screen.dart b/lib/features/onboarding/screen.dart index c5d6d51..94e4435 100644 --- a/lib/features/onboarding/screen.dart +++ b/lib/features/onboarding/screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/l10n/locale_provider.dart'; +import '../../core/services/haptic_service.dart'; import 'provider.dart'; import 'widgets/onboarding_page.dart'; import 'widgets/onboarding_page_indicator.dart'; @@ -22,9 +23,16 @@ class _OnboardingScreenState extends ConsumerState { super.dispose(); } - void _finish() { - ref.read(onboardingProvider.notifier).complete(); - context.go('/dashboard'); + void _onPageChanged(int page) { + ref.read(onboardingProvider.notifier).setPage(page); + HapticService.light(); + if (page == 4) { + HapticService.medium(); + ref.read(onboardingProvider.notifier).complete(); + Future.delayed(const Duration(milliseconds: 400), () { + if (mounted) context.go('/dashboard'); + }); + } } @override @@ -39,8 +47,7 @@ class _OnboardingScreenState extends ConsumerState { Expanded( child: PageView( controller: _controller, - onPageChanged: (page) => - ref.read(onboardingProvider.notifier).setPage(page), + onPageChanged: _onPageChanged, children: [ OnboardingPage.welcome(welcomeText: s.onboardingWelcome), OnboardingPage.content( @@ -53,30 +60,65 @@ class _OnboardingScreenState extends ConsumerState { headline: s.onboardingCardsTitle, description: s.onboardingCardsBody, ), + 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, + ), + ], + ), + ], + ), + ), + ), + const SizedBox.shrink(), ], ), ), Padding( padding: const EdgeInsets.only(bottom: 48), - child: Column( - children: [ - OnboardingPageIndicator( - current: currentPage, - count: 3, - ), - const SizedBox(height: 32), - if (currentPage == 2) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: _finish, - child: Text(s.onboardingGetStarted), - ), - ), - ), - ], + child: OnboardingPageIndicator( + current: currentPage, + count: 5, ), ), ], diff --git a/lib/features/onboarding/widgets/casha_shimmer_text.dart b/lib/features/onboarding/widgets/casha_shimmer_text.dart index 207d9b2..bf968ca 100644 --- a/lib/features/onboarding/widgets/casha_shimmer_text.dart +++ b/lib/features/onboarding/widgets/casha_shimmer_text.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:sensors_plus/sensors_plus.dart'; @@ -19,8 +20,8 @@ class CashaShimmerText extends StatefulWidget { class _CashaShimmerTextState extends State with SingleTickerProviderStateMixin { late final AnimationController _controller; - double _tilt = 0.0; - double _targetTilt = 0.0; + double _tiltX = 0.0, _tiltY = 0.0; + double _targetTiltX = 0.0, _targetTiltY = 0.0; StreamSubscription? _sub; @override @@ -28,13 +29,14 @@ class _CashaShimmerTextState extends State super.initState(); _controller = AnimationController( vsync: this, - duration: const Duration(seconds: 4), + duration: const Duration(seconds: 5), )..repeat(); _sub = accelerometerEventStream( samplingPeriod: const Duration(milliseconds: 50), ).listen((e) { - _targetTilt = (e.x / 9.8).clamp(-1.0, 1.0); + _targetTiltY = (e.x / 9.8).clamp(-1.0, 1.0); + _targetTiltX = ((e.y / 9.8) - 1.0).clamp(-1.0, 1.0); }); } @@ -48,33 +50,44 @@ class _CashaShimmerTextState extends State @override Widget build(BuildContext context) { final primary = Theme.of(context).colorScheme.primary; + final secondary = Theme.of(context).colorScheme.secondary; return AnimatedBuilder( animation: _controller, builder: (context, child) { - _tilt += (_targetTilt - _tilt) * 0.12; + _tiltX += (_targetTiltX - _tiltX) * 0.15; + _tiltY += (_targetTiltY - _tiltY) * 0.15; - final sweep = (_controller.value * 2.0 - 1.0) + _tilt * 0.6; + final t = _controller.value; + final sweepX = sin(t * 2 * pi); + final sweepY = cos(t * 2 * pi); - return ShaderMask( - shaderCallback: (bounds) { - return LinearGradient( - begin: Alignment(sweep - 0.8, -0.3), - end: Alignment(sweep + 0.8, 0.3), - colors: [ - primary.withValues(alpha: 0.7), - primary, - Colors.white, - primary.withValues(alpha: 0.9), - Colors.white, - primary, - primary.withValues(alpha: 0.7), - ], - stops: [0.0, 0.2, 0.35, 0.45, 0.55, 0.8, 1.0], - ).createShader(bounds); - }, - blendMode: BlendMode.srcIn, - child: child, + return Transform( + alignment: Alignment.center, + transform: Matrix4.identity() + ..setEntry(3, 2, 0.001) + ..rotateX(_tiltX * 0.85) + ..rotateY(_tiltY * 0.85), + child: ShaderMask( + shaderCallback: (bounds) { + return LinearGradient( + begin: Alignment(sweepX - 1.2, sweepY - 0.5), + end: Alignment(sweepX + 1.2, sweepY + 0.5), + colors: [ + primary.withOpacity(0.7), + primary, + secondary, + Colors.white, + secondary, + primary, + primary.withOpacity(0.7), + ], + stops: [0.0, 0.15, 0.35, 0.5, 0.65, 0.85, 1.0], + ).createShader(bounds); + }, + blendMode: BlendMode.srcIn, + child: child, + ), ); }, child: Text( diff --git a/lib/features/onboarding/widgets/onboarding_page_indicator.dart b/lib/features/onboarding/widgets/onboarding_page_indicator.dart index 49f885f..795ebc0 100644 --- a/lib/features/onboarding/widgets/onboarding_page_indicator.dart +++ b/lib/features/onboarding/widgets/onboarding_page_indicator.dart @@ -13,10 +13,12 @@ class OnboardingPageIndicator extends StatelessWidget { @override Widget build(BuildContext context) { final color = Theme.of(context).colorScheme.primary; + final outlineColor = Theme.of(context).colorScheme.secondary; return Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(count, (i) { final isActive = i == current; + final isLast = i == count - 1; return AnimatedContainer( duration: const Duration(milliseconds: 200), margin: const EdgeInsets.symmetric(horizontal: 4), @@ -25,6 +27,9 @@ class OnboardingPageIndicator extends StatelessWidget { decoration: BoxDecoration( color: isActive ? color : color.withOpacity(0.3), borderRadius: BorderRadius.circular(4), + border: isLast && !isActive + ? Border.all(color: outlineColor, width: 1.5) + : null, ), ); }), diff --git a/lib/main.dart b/lib/main.dart index a278d03..770253b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'app/app.dart'; import 'core/services/haptic_service.dart'; import 'data/database/app_database.dart'; import 'features/dashboard/provider.dart'; +import 'shared/services/onboarding_service.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -17,6 +18,7 @@ void main() async { final prefs = await SharedPreferences.getInstance(); await HapticService.init(); + OnboardingService(prefs); final database = AppDatabase(); diff --git a/lib/shared/services/onboarding_service.dart b/lib/shared/services/onboarding_service.dart index 908c6ff..29ddc24 100644 --- a/lib/shared/services/onboarding_service.dart +++ b/lib/shared/services/onboarding_service.dart @@ -4,18 +4,27 @@ import 'package:shared_preferences/shared_preferences.dart'; class OnboardingService { static const _key = 'onboarding_completed'; static bool _shownInSession = false; + static SharedPreferences? _staticPrefs; final SharedPreferences _prefs; - OnboardingService(this._prefs); + OnboardingService(this._prefs) { + _staticPrefs = _prefs; + } - bool get shouldShowOnboarding { + static bool get shouldShowOnboarding { if (kDebugMode) { return !_shownInSession; } - return !(_prefs.getBool(_key) ?? false); + return !(_staticPrefs?.getBool(_key) ?? false); } + static void markCompleted() { + _shownInSession = true; + } + + bool get shouldShowOnboardingInstance => shouldShowOnboarding; + Future completeOnboarding() async { _shownInSession = true; if (!kDebugMode) {