Files
tripz-llc/apps/rider/lib/features/splash/view/splash_screen.dart
T

248 lines
7.3 KiB
Dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../core/app_info.dart';
import '../../../core/design/tokens.dart';
import '../../../core/di.dart';
import '../../../core/api/api_client.dart';
import '../../auth/cubit/auth_cubit.dart';
import '../../auth/cubit/auth_state.dart';
import '../../../core/ui/tripz_scaffold.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen>
with SingleTickerProviderStateMixin {
static const _kOnboardingSeen = 'onboarding_seen';
late final AnimationController _revealController;
late final Animation<double> _fade;
late final Animation<double> _scale;
@override
void initState() {
super.initState();
// حركة دخول واحدة هادئة: fade + scale ضمن أزمنة tokens (docs/29 §3.1)
_revealController = AnimationController(vsync: this, duration: T.slow);
final curved = CurvedAnimation(
parent: _revealController,
curve: Curves.easeOutCubic,
);
_fade = curved;
_scale = Tween<double>(begin: 0.85, end: 1.0).animate(curved);
_revealController.forward();
WidgetsBinding.instance.addPostFrameCallback((_) => _route());
}
@override
void dispose() {
_revealController.dispose();
super.dispose();
}
Future<void> _route() async {
FlutterNativeSplash.remove();
// Minimum 800ms for the native splash to fade out gracefully
await Future.delayed(const Duration(milliseconds: 800));
if (!mounted) return;
final authStatus = context.read<AuthCubit>().state.status;
if (authStatus != AuthStatus.authenticated) {
final prefs = await SharedPreferences.getInstance();
if (!mounted) return;
final seen = prefs.getBool(_kOnboardingSeen) ?? false;
if (!seen) {
await prefs.setBool(_kOnboardingSeen, true);
if (!mounted) return;
context.go('/onboarding');
} else {
context.go('/phone');
}
return;
}
final api = getIt<ApiClient>();
Future<Map<String, dynamic>?> pendingRating() async {
try {
final r = await api.dio
.get('/trips/rating/pending')
.timeout(const Duration(seconds: 3));
final data = r.data;
if (data is Map<String, dynamic> && data['pending'] != null) {
return data;
}
return null;
} catch (_) {
return null;
}
}
Future<bool> hasActiveTrip() async {
try {
final r = await api.dio
.get('/trips/mine')
.timeout(const Duration(seconds: 3));
final data = r.data;
if (data == null) return false;
final items = (data['items'] ?? data) as List;
if (items.isEmpty) return false;
final status = items.first['status'] as String? ?? '';
return [
'searching',
'assigned',
'driver_arriving',
'driver_arrived',
'in_progress',
].contains(status);
} catch (_) {
return false;
}
}
final results = await Future.wait([pendingRating(), hasActiveTrip()]);
final rating = results[0] as Map<String, dynamic>?;
final activeTrip = results[1] as bool;
if (!mounted) return;
if (rating != null) {
context.go('/trip-rating', extra: rating);
return;
}
if (activeTrip) {
context.go('/trip-tracking');
return;
}
context.go('/home');
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
// Just show the logo briefly — native splash handles the branded experience
return TripzScaffold(
backgroundColor: cs.surface,
padding: false,
safeAreaTop: false,
safeAreaBottom: false,
child: Stack(
alignment: Alignment.center,
children: [
Center(
child: AnimatedBuilder(
animation: _revealController,
builder: (context, child) => Opacity(
opacity: _fade.value,
child: Transform.scale(scale: _scale.value, child: child),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: T.s48),
child: Image.asset(
'assets/images/logo.png',
width: 240,
fit: BoxFit.contain,
errorBuilder: (_, __, ___) => Icon(
Icons.directions_car_filled_rounded,
size: 120,
color: cs.primary,
),
),
),
),
),
// مؤشر تحميل خفيف ورقم الإصدار أسفل الشاشة — يظهر بعد اكتمال حركة الشعار
Positioned(
bottom: T.s32,
child: FadeTransition(
opacity: _revealController,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(3, (i) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: T.s4),
child: _PulsingDot(color: cs.primary, delay: i * 150),
);
}),
),
const SizedBox(height: T.s16),
Text(
AppInfo.formattedVersion,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: cs.onSurfaceVariant.withValues(alpha: 0.5),
letterSpacing: 1.2,
),
),
],
),
),
),
],
),
);
}
}
/// نقطة نابضة صغيرة — مؤشر تحميل زخرفي بلا شبكة، `CustomPaint`-free ويكفي هنا Container بسيط.
class _PulsingDot extends StatefulWidget {
const _PulsingDot({required this.color, required this.delay});
final Color color;
final int delay;
@override
State<_PulsingDot> createState() => _PulsingDotState();
}
class _PulsingDotState extends State<_PulsingDot>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: T.slow * 3)
..repeat(reverse: true);
Future.delayed(Duration(milliseconds: widget.delay), () {
if (mounted) _controller.forward();
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, _) => Opacity(
opacity: 0.2 + (_controller.value * 0.6),
child: Container(
width: T.s8,
height: T.s8,
decoration: BoxDecoration(color: widget.color, shape: BoxShape.circle),
),
),
);
}
}