feat: implement splash and onboarding flows, update app assets, and add project design documentation
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user