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 createState() => _SplashScreenState(); } class _SplashScreenState extends State with SingleTickerProviderStateMixin { static const _kOnboardingSeen = 'onboarding_seen'; late final AnimationController _revealController; late final Animation _fade; late final Animation _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(begin: 0.85, end: 1.0).animate(curved); _revealController.forward(); WidgetsBinding.instance.addPostFrameCallback((_) => _route()); } @override void dispose() { _revealController.dispose(); super.dispose(); } Future _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().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(); Future?> 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 && data['pending'] != null) { return data; } return null; } catch (_) { return null; } } Future 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?; 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), ), ), ); } }