232 lines
7.2 KiB
Dart
232 lines
7.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import '../../../core/design/tokens.dart';
|
|
import '../../../core/l10n/app_localizations.dart';
|
|
import '../../../core/ui/tripz_scaffold.dart';
|
|
import '../../../core/ui/tripz_button.dart';
|
|
|
|
class OnboardingScreen extends StatefulWidget {
|
|
const OnboardingScreen({super.key});
|
|
|
|
@override
|
|
State<OnboardingScreen> createState() => _OnboardingScreenState();
|
|
}
|
|
|
|
class _OnboardingScreenState extends State<OnboardingScreen>
|
|
with SingleTickerProviderStateMixin {
|
|
final PageController _pageController = PageController();
|
|
int _currentIndex = 0;
|
|
|
|
late final AnimationController _pulseController;
|
|
late final Animation<double> _pulseAnimation;
|
|
|
|
static const _onboardingKey = 'onboarding_seen';
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_pulseController = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 1400),
|
|
)..repeat(reverse: true);
|
|
_pulseAnimation = Tween<double>(begin: 1.0, end: 1.08).animate(
|
|
CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_pulseController.dispose();
|
|
_pageController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _markSeen() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setBool(_onboardingKey, true);
|
|
}
|
|
|
|
void _nextPage(int pageCount) {
|
|
if (_currentIndex < pageCount - 1) {
|
|
_pageController.nextPage(
|
|
duration: T.slow,
|
|
curve: Curves.easeInOut,
|
|
);
|
|
} else {
|
|
_markSeen();
|
|
context.go('/phone');
|
|
}
|
|
}
|
|
|
|
void _skip() {
|
|
_markSeen();
|
|
context.go('/phone');
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final cs = theme.colorScheme;
|
|
final l10n = AppLocalizations.of(context)!;
|
|
|
|
final pages = [
|
|
_OnboardingPageData(
|
|
title: l10n.onboardingTitle1,
|
|
subtitle: l10n.onboardingSubtitle1,
|
|
image: 'assets/images/onboarding_1.png',
|
|
fallbackIcon: Icons.touch_app_rounded,
|
|
),
|
|
_OnboardingPageData(
|
|
title: l10n.onboardingTitle2,
|
|
subtitle: l10n.onboardingSubtitle2,
|
|
image: 'assets/images/onboarding_2.png',
|
|
fallbackIcon: Icons.location_on_rounded,
|
|
),
|
|
_OnboardingPageData(
|
|
title: l10n.onboardingTitle3,
|
|
subtitle: l10n.onboardingSubtitle3,
|
|
image: 'assets/images/onboarding_3.png',
|
|
fallbackIcon: Icons.payment_rounded,
|
|
),
|
|
];
|
|
|
|
return TripzScaffold(
|
|
backgroundColor: cs.surface,
|
|
padding: false,
|
|
bottomAction: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// ── Page Indicators ──
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: List.generate(pages.length, (index) {
|
|
final isActive = index == _currentIndex;
|
|
return AnimatedContainer(
|
|
duration: T.medium,
|
|
margin: const EdgeInsets.symmetric(horizontal: T.s4),
|
|
height: T.s8,
|
|
width: isActive ? T.s24 : T.s8,
|
|
decoration: BoxDecoration(
|
|
color: isActive
|
|
? cs.primary
|
|
: cs.surfaceContainerHighest,
|
|
borderRadius: BorderRadius.circular(T.rCircle),
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
const SizedBox(height: T.s24),
|
|
|
|
// ── Primary Button ──
|
|
TripzButton(
|
|
onPressed: () => _nextPage(pages.length),
|
|
label: _currentIndex == pages.length - 1 ? l10n.getStarted : l10n.next,
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
// ── Skip Button ──
|
|
Align(
|
|
alignment: AlignmentDirectional.centerEnd,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: T.pagePadding,
|
|
vertical: T.s8,
|
|
),
|
|
child: TripzButton(
|
|
onPressed: _skip,
|
|
style: TripzButtonStyle.text,
|
|
label: l10n.skip,
|
|
fullWidth: false,
|
|
),
|
|
),
|
|
),
|
|
|
|
// ── PageView ──
|
|
Expanded(
|
|
child: PageView.builder(
|
|
controller: _pageController,
|
|
itemCount: pages.length,
|
|
onPageChanged: (index) {
|
|
setState(() => _currentIndex = index);
|
|
},
|
|
itemBuilder: (context, index) {
|
|
final data = pages[index];
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: T.s24),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
// ── الرسم التوضيحي (أصل مسجّل) مع نبض خفيف ──
|
|
AnimatedScale(
|
|
scale: _pulseAnimation.value,
|
|
duration: const Duration(milliseconds: 100),
|
|
child: SizedBox(
|
|
width: 240,
|
|
height: 240,
|
|
child: Image.asset(
|
|
data.image,
|
|
fit: BoxFit.contain,
|
|
errorBuilder: (_, __, ___) => Container(
|
|
decoration: BoxDecoration(
|
|
color: cs.primaryContainer,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
data.fallbackIcon,
|
|
size: 88,
|
|
color: cs.onPrimaryContainer,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: T.s48),
|
|
|
|
// ── Title ──
|
|
Text(
|
|
data.title,
|
|
style: theme.textTheme.headlineMedium?.copyWith(
|
|
color: cs.onSurface,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: T.s12),
|
|
|
|
// ── Subtitle ──
|
|
Text(
|
|
data.subtitle,
|
|
style: theme.textTheme.bodyLarge?.copyWith(
|
|
color: cs.onSurfaceVariant,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _OnboardingPageData {
|
|
final String title;
|
|
final String subtitle;
|
|
final String image;
|
|
final IconData fallbackIcon;
|
|
|
|
const _OnboardingPageData({
|
|
required this.title,
|
|
required this.subtitle,
|
|
required this.image,
|
|
required this.fallbackIcon,
|
|
});
|
|
}
|