mirror of
https://github.com/koloideal/Casha.git
synced 2026-08-08 17:51:14 +03:00
step
This commit is contained in:
+2
-4
@@ -10,17 +10,15 @@ import '../features/settings/categories/category_manager_screen.dart';
|
|||||||
import '../features/onboarding/screen.dart';
|
import '../features/onboarding/screen.dart';
|
||||||
import '../shared/models/transaction.dart';
|
import '../shared/models/transaction.dart';
|
||||||
import '../shared/paywall/paywall_screen.dart';
|
import '../shared/paywall/paywall_screen.dart';
|
||||||
import '../shared/providers/onboarding_provider.dart';
|
import '../shared/services/onboarding_service.dart';
|
||||||
|
|
||||||
final _shellKey = GlobalKey<NavigatorState>();
|
final _shellKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
final appRouter = GoRouter(
|
final appRouter = GoRouter(
|
||||||
initialLocation: '/dashboard',
|
initialLocation: '/dashboard',
|
||||||
redirect: (context, state) {
|
redirect: (context, state) {
|
||||||
final service = ProviderScope.containerOf(context)
|
|
||||||
.read(onboardingServiceProvider);
|
|
||||||
final location = state.uri.toString();
|
final location = state.uri.toString();
|
||||||
if (service.shouldShowOnboarding && location != '/onboarding') {
|
if (OnboardingService.shouldShowOnboarding && location != '/onboarding') {
|
||||||
return '/onboarding';
|
return '/onboarding';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ class AppStrings {
|
|||||||
|
|
||||||
String get onboardingWelcome => _ru ? 'Добро пожаловать в' : 'Welcome to';
|
String get onboardingWelcome => _ru ? 'Добро пожаловать в' : 'Welcome to';
|
||||||
String get onboardingMultiCurrencyTitle =>
|
String get onboardingMultiCurrencyTitle =>
|
||||||
_ru ? 'Все деньги на одном экране' : 'All Your Money, One View';
|
_ru ? 'Все деньги\nна одном экране' : 'All Your Money,\nOne View';
|
||||||
String get onboardingMultiCurrencyBody => _ru
|
String get onboardingMultiCurrencyBody => _ru
|
||||||
? 'Счета в разных валютах с автоматической конвертацией по актуальным курсам'
|
? 'Счета в разных валютах с автоматической конвертацией по актуальным курсам'
|
||||||
: 'Accounts in different currencies, automatically converted at live exchange rates';
|
: 'Accounts in different currencies, automatically converted at live exchange rates';
|
||||||
@@ -273,5 +273,9 @@ class AppStrings {
|
|||||||
String get onboardingCardsBody => _ru
|
String get onboardingCardsBody => _ru
|
||||||
? 'Наклоните телефон — и карточка оживает. Градиенты, цвета и высота — всё настраивается под вас'
|
? 'Наклоните телефон — и карточка оживает. Градиенты, цвета и высота — всё настраивается под вас'
|
||||||
: 'Tilt your phone and watch them respond. Customize gradients, colors and height to make them yours';
|
: '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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import '../../core/l10n/locale_provider.dart';
|
import '../../core/l10n/locale_provider.dart';
|
||||||
|
import '../../core/services/haptic_service.dart';
|
||||||
import 'provider.dart';
|
import 'provider.dart';
|
||||||
import 'widgets/onboarding_page.dart';
|
import 'widgets/onboarding_page.dart';
|
||||||
import 'widgets/onboarding_page_indicator.dart';
|
import 'widgets/onboarding_page_indicator.dart';
|
||||||
@@ -22,9 +23,16 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _finish() {
|
void _onPageChanged(int page) {
|
||||||
|
ref.read(onboardingProvider.notifier).setPage(page);
|
||||||
|
HapticService.light();
|
||||||
|
if (page == 4) {
|
||||||
|
HapticService.medium();
|
||||||
ref.read(onboardingProvider.notifier).complete();
|
ref.read(onboardingProvider.notifier).complete();
|
||||||
context.go('/dashboard');
|
Future.delayed(const Duration(milliseconds: 400), () {
|
||||||
|
if (mounted) context.go('/dashboard');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -39,8 +47,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: PageView(
|
child: PageView(
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
onPageChanged: (page) =>
|
onPageChanged: _onPageChanged,
|
||||||
ref.read(onboardingProvider.notifier).setPage(page),
|
|
||||||
children: [
|
children: [
|
||||||
OnboardingPage.welcome(welcomeText: s.onboardingWelcome),
|
OnboardingPage.welcome(welcomeText: s.onboardingWelcome),
|
||||||
OnboardingPage.content(
|
OnboardingPage.content(
|
||||||
@@ -53,30 +60,65 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
|||||||
headline: s.onboardingCardsTitle,
|
headline: s.onboardingCardsTitle,
|
||||||
description: s.onboardingCardsBody,
|
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(
|
||||||
padding: const EdgeInsets.only(bottom: 48),
|
padding: const EdgeInsets.only(bottom: 48),
|
||||||
child: Column(
|
child: OnboardingPageIndicator(
|
||||||
children: [
|
|
||||||
OnboardingPageIndicator(
|
|
||||||
current: currentPage,
|
current: currentPage,
|
||||||
count: 3,
|
count: 5,
|
||||||
),
|
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:sensors_plus/sensors_plus.dart';
|
import 'package:sensors_plus/sensors_plus.dart';
|
||||||
|
|
||||||
@@ -19,8 +20,8 @@ class CashaShimmerText extends StatefulWidget {
|
|||||||
class _CashaShimmerTextState extends State<CashaShimmerText>
|
class _CashaShimmerTextState extends State<CashaShimmerText>
|
||||||
with SingleTickerProviderStateMixin {
|
with SingleTickerProviderStateMixin {
|
||||||
late final AnimationController _controller;
|
late final AnimationController _controller;
|
||||||
double _tilt = 0.0;
|
double _tiltX = 0.0, _tiltY = 0.0;
|
||||||
double _targetTilt = 0.0;
|
double _targetTiltX = 0.0, _targetTiltY = 0.0;
|
||||||
StreamSubscription<AccelerometerEvent>? _sub;
|
StreamSubscription<AccelerometerEvent>? _sub;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -28,13 +29,14 @@ class _CashaShimmerTextState extends State<CashaShimmerText>
|
|||||||
super.initState();
|
super.initState();
|
||||||
_controller = AnimationController(
|
_controller = AnimationController(
|
||||||
vsync: this,
|
vsync: this,
|
||||||
duration: const Duration(seconds: 4),
|
duration: const Duration(seconds: 5),
|
||||||
)..repeat();
|
)..repeat();
|
||||||
|
|
||||||
_sub = accelerometerEventStream(
|
_sub = accelerometerEventStream(
|
||||||
samplingPeriod: const Duration(milliseconds: 50),
|
samplingPeriod: const Duration(milliseconds: 50),
|
||||||
).listen((e) {
|
).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<CashaShimmerText>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final primary = Theme.of(context).colorScheme.primary;
|
final primary = Theme.of(context).colorScheme.primary;
|
||||||
|
final secondary = Theme.of(context).colorScheme.secondary;
|
||||||
|
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: _controller,
|
animation: _controller,
|
||||||
builder: (context, child) {
|
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(
|
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) {
|
shaderCallback: (bounds) {
|
||||||
return LinearGradient(
|
return LinearGradient(
|
||||||
begin: Alignment(sweep - 0.8, -0.3),
|
begin: Alignment(sweepX - 1.2, sweepY - 0.5),
|
||||||
end: Alignment(sweep + 0.8, 0.3),
|
end: Alignment(sweepX + 1.2, sweepY + 0.5),
|
||||||
colors: [
|
colors: [
|
||||||
primary.withValues(alpha: 0.7),
|
primary.withOpacity(0.7),
|
||||||
primary,
|
primary,
|
||||||
|
secondary,
|
||||||
Colors.white,
|
Colors.white,
|
||||||
primary.withValues(alpha: 0.9),
|
secondary,
|
||||||
Colors.white,
|
|
||||||
primary,
|
primary,
|
||||||
primary.withValues(alpha: 0.7),
|
primary.withOpacity(0.7),
|
||||||
],
|
],
|
||||||
stops: [0.0, 0.2, 0.35, 0.45, 0.55, 0.8, 1.0],
|
stops: [0.0, 0.15, 0.35, 0.5, 0.65, 0.85, 1.0],
|
||||||
).createShader(bounds);
|
).createShader(bounds);
|
||||||
},
|
},
|
||||||
blendMode: BlendMode.srcIn,
|
blendMode: BlendMode.srcIn,
|
||||||
child: child,
|
child: child,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ class OnboardingPageIndicator extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final color = Theme.of(context).colorScheme.primary;
|
final color = Theme.of(context).colorScheme.primary;
|
||||||
|
final outlineColor = Theme.of(context).colorScheme.secondary;
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: List.generate(count, (i) {
|
children: List.generate(count, (i) {
|
||||||
final isActive = i == current;
|
final isActive = i == current;
|
||||||
|
final isLast = i == count - 1;
|
||||||
return AnimatedContainer(
|
return AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
@@ -25,6 +27,9 @@ class OnboardingPageIndicator extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isActive ? color : color.withOpacity(0.3),
|
color: isActive ? color : color.withOpacity(0.3),
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
border: isLast && !isActive
|
||||||
|
? Border.all(color: outlineColor, width: 1.5)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'app/app.dart';
|
|||||||
import 'core/services/haptic_service.dart';
|
import 'core/services/haptic_service.dart';
|
||||||
import 'data/database/app_database.dart';
|
import 'data/database/app_database.dart';
|
||||||
import 'features/dashboard/provider.dart';
|
import 'features/dashboard/provider.dart';
|
||||||
|
import 'shared/services/onboarding_service.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
@@ -17,6 +18,7 @@ void main() async {
|
|||||||
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await HapticService.init();
|
await HapticService.init();
|
||||||
|
OnboardingService(prefs);
|
||||||
|
|
||||||
final database = AppDatabase();
|
final database = AppDatabase();
|
||||||
|
|
||||||
|
|||||||
@@ -4,18 +4,27 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||||||
class OnboardingService {
|
class OnboardingService {
|
||||||
static const _key = 'onboarding_completed';
|
static const _key = 'onboarding_completed';
|
||||||
static bool _shownInSession = false;
|
static bool _shownInSession = false;
|
||||||
|
static SharedPreferences? _staticPrefs;
|
||||||
|
|
||||||
final SharedPreferences _prefs;
|
final SharedPreferences _prefs;
|
||||||
|
|
||||||
OnboardingService(this._prefs);
|
OnboardingService(this._prefs) {
|
||||||
|
_staticPrefs = _prefs;
|
||||||
|
}
|
||||||
|
|
||||||
bool get shouldShowOnboarding {
|
static bool get shouldShowOnboarding {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
return !_shownInSession;
|
return !_shownInSession;
|
||||||
}
|
}
|
||||||
return !(_prefs.getBool(_key) ?? false);
|
return !(_staticPrefs?.getBool(_key) ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void markCompleted() {
|
||||||
|
_shownInSession = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get shouldShowOnboardingInstance => shouldShowOnboarding;
|
||||||
|
|
||||||
Future<void> completeOnboarding() async {
|
Future<void> completeOnboarding() async {
|
||||||
_shownInSession = true;
|
_shownInSession = true;
|
||||||
if (!kDebugMode) {
|
if (!kDebugMode) {
|
||||||
|
|||||||
Reference in New Issue
Block a user