248 lines
7.3 KiB
Dart
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),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|