feat: implement splash and onboarding flows, update app assets, and add project design documentation

This commit is contained in:
Hamza-Ayed
2026-07-24 04:15:07 +03:00
parent 9f7bc31408
commit 213b459912
208 changed files with 23801 additions and 1066 deletions
@@ -20,7 +20,14 @@ class TripRepository {
if (items.isEmpty) return null;
final trip = TripModel.fromJson(items.first as Map<String, dynamic>);
final active = ['searching', 'assigned', 'driver_arriving', 'driver_arrived', 'in_progress'];
return active.contains(trip.status) ? trip : null;
if (!active.contains(trip.status)) return null;
// تصفية: إذا مرت أكثر من 60 دقيقة على إنشاء الرحلة، لا تُعتبر نشطة
if (trip.createdAt != null) {
final age = DateTime.now().difference(trip.createdAt!);
if (age.inMinutes >= 60) return null;
}
return trip;
} catch (_) {
return null;
}
@@ -4,6 +4,7 @@ import '../../../core/l10n/app_localizations.dart';
import 'package:go_router/go_router.dart';
import '../../../core/design/tokens.dart';
import '../../../core/ui/tripz_button.dart';
import '../../../core/ui/tripz_scaffold.dart';
import '../bloc/trip_bloc.dart';
import '../bloc/trip_state.dart';
import '../bloc/trip_event.dart';
@@ -42,72 +43,74 @@ class _RatingScreenState extends State<RatingScreen> {
context.go('/home');
}
},
child: Scaffold(
appBar: AppBar(title: Text(l10n.rateYourTrip)),
body: Padding(
padding: const EdgeInsets.all(T.pagePadding),
child: Column(
children: [
const Spacer(flex: 2),
child: TripzScaffold(
title: l10n.rateYourTrip,
showBackButton: true,
onBack: () {
context.read<TripBloc>().add(const TripClear());
context.go('/home');
},
child: Column(
children: [
const Spacer(flex: 2),
// نجوم التقييم
Text(
l10n.rateDriver,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
// نجوم التقييم
Text(
l10n.rateDriver,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: T.s24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(5, (i) {
final index = i + 1;
return GestureDetector(
onTap: () => setState(() => _stars = index),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: Icon(
index <= _stars ? Icons.star_rounded : Icons.star_outline_rounded,
size: 48,
color: index <= _stars ? Colors.amber : cs.onSurfaceVariant,
),
),
const SizedBox(height: T.s24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(5, (i) {
final index = i + 1;
return GestureDetector(
onTap: () => setState(() => _stars = index),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: Icon(
index <= _stars ? Icons.star_rounded : Icons.star_outline_rounded,
size: 48,
color: index <= _stars ? Colors.amber : cs.onSurfaceVariant,
),
),
);
}),
),
),
);
}),
),
const SizedBox(height: T.s24),
const SizedBox(height: T.s24),
// حقل التعليق
TextField(
controller: _commentController,
maxLines: 3,
decoration: InputDecoration(
hintText: l10n.ratingComment,
border: const OutlineInputBorder(),
),
// حقل التعليق
TextField(
controller: _commentController,
maxLines: 3,
decoration: InputDecoration(
hintText: l10n.ratingComment,
border: const OutlineInputBorder(),
),
),
const Spacer(flex: 3),
const Spacer(flex: 3),
// زر الإرسال
SizedBox(
width: double.infinity,
child: TripzButton(
label: l10n.submitRating,
onPressed: _stars > 0
? () {
context.read<TripBloc>().add(TripRate(
stars: _stars,
comment: _commentController.text,
));
}
: null,
),
// زر الإرسال
SizedBox(
width: double.infinity,
child: TripzButton(
label: l10n.submitRating,
onPressed: _stars > 0
? () {
context.read<TripBloc>().add(TripRate(
stars: _stars,
comment: _commentController.text,
));
}
: null,
),
const SizedBox(height: T.s8),
],
),
),
const SizedBox(height: T.s8),
],
),
),
);
@@ -4,6 +4,7 @@ import '../../../core/l10n/app_localizations.dart';
import 'package:go_router/go_router.dart';
import '../../../core/design/tokens.dart';
import '../../../core/ui/tripz_button.dart';
import '../../../core/ui/tripz_scaffold.dart';
import '../bloc/trip_bloc.dart';
import '../bloc/trip_state.dart';
import '../bloc/trip_event.dart';
@@ -15,94 +16,90 @@ class TripInvoiceScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final cs = Theme.of(context).colorScheme;
return BlocBuilder<TripBloc, TripState>(
builder: (context, state) {
final trip = state.trip;
if (trip == null) {
return Scaffold(
appBar: AppBar(title: Text(l10n.rideCompleted)),
body: const Center(child: CircularProgressIndicator()),
return TripzScaffold(
title: l10n.rideCompleted,
showBackButton: true,
onBack: () => context.go('/home'),
child: const Center(child: CircularProgressIndicator()),
);
}
final fare = trip.fare ?? 0;
final currency = trip.currency ?? 'JOD';
return Scaffold(
appBar: AppBar(
title: Text(l10n.rideCompleted),
automaticallyImplyLeading: false,
),
body: Padding(
padding: const EdgeInsets.all(T.pagePadding),
child: Column(
children: [
// أيقونة النجاح
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Colors.green.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: const Icon(Icons.check_circle_rounded, size: 48, color: Colors.green),
return TripzScaffold(
title: l10n.rideCompleted,
showBackButton: false,
child: Column(
children: [
// أيقونة النجاح
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Colors.green.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
const SizedBox(height: T.s16),
Text(
l10n.rideCompleted,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: T.s24),
// تفاصيل الأجرة
Card(
child: Padding(
padding: const EdgeInsets.all(T.pagePadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(l10n.fareBreakdown,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
)),
const SizedBox(height: T.s12),
_FareRow(label: l10n.tripFare, value: '$fare $currency'),
if (trip.paymentMethod == 'wallet')
_FareRow(label: l10n.walletPayment, value: l10n.youPaid),
if (trip.paymentMethod == 'cash')
_FareRow(label: l10n.cash, value: l10n.payCash),
const Divider(height: T.s16),
_FareRow(
label: l10n.total,
value: '$fare $currency',
bold: true,
),
],
child: const Icon(Icons.check_circle_rounded, size: 48, color: Colors.green),
),
const SizedBox(height: T.s16),
Text(
l10n.rideCompleted,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: T.s24),
// تفاصيل الأجرة
Card(
child: Padding(
padding: const EdgeInsets.all(T.pagePadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(l10n.fareBreakdown,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
)),
const SizedBox(height: T.s12),
_FareRow(label: l10n.tripFare, value: '$fare $currency'),
if (trip.paymentMethod == 'wallet')
_FareRow(label: l10n.walletPayment, value: l10n.youPaid),
if (trip.paymentMethod == 'cash')
_FareRow(label: l10n.cash, value: l10n.payCash),
const Divider(height: T.s16),
_FareRow(
label: l10n.total,
value: '$fare $currency',
bold: true,
),
],
),
),
),
const Spacer(),
const Spacer(),
// أزرار الإجراءات
TripzButton(
label: l10n.rateYourTrip,
onPressed: () => context.push('/trip-rating'),
),
const SizedBox(height: T.s8),
TextButton(
onPressed: () {
context.read<TripBloc>().add(const TripClear());
context.go('/home');
},
child: Text(l10n.skipRating),
),
],
),
// أزرار الإجراءات
TripzButton(
label: l10n.rateYourTrip,
onPressed: () => context.push('/trip-rating'),
),
const SizedBox(height: T.s8),
TextButton(
onPressed: () {
context.read<TripBloc>().add(const TripClear());
context.go('/home');
},
child: Text(l10n.skipRating),
),
],
),
);
},
@@ -5,6 +5,7 @@ import '../../../core/l10n/app_localizations.dart';
import 'package:go_router/go_router.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import '../../../core/design/tokens.dart';
import '../../../core/ui/tripz_scaffold.dart';
import '../bloc/trip_bloc.dart';
import '../bloc/trip_state.dart';
import '../bloc/trip_event.dart';
@@ -20,7 +21,6 @@ class TripTrackingScreen extends StatefulWidget {
}
class _TripTrackingScreenState extends State<TripTrackingScreen> {
IntaleqMapController? _mapController;
Timer? _cameraFollowTimer;
@override
@@ -39,7 +39,6 @@ class _TripTrackingScreenState extends State<TripTrackingScreen> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final cs = Theme.of(context).colorScheme;
return BlocConsumer<TripBloc, TripState>(
listenWhen: (prev, cur) => prev.trip?.status != cur.trip?.status,
@@ -52,7 +51,23 @@ class _TripTrackingScreenState extends State<TripTrackingScreen> {
builder: (context, state) {
final trip = state.trip;
if (trip == null) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
return TripzScaffold(
showBackButton: true,
onBack: () {
context.read<TripBloc>().add(const TripClear());
context.go('/home');
},
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
const SizedBox(height: T.s16),
Text(l10n.loading),
],
),
),
);
}
final pickup = LatLng(trip.pickupLat, trip.pickupLng);
@@ -71,10 +86,14 @@ class _TripTrackingScreenState extends State<TripTrackingScreen> {
target: driverPos ?? pickup,
zoom: 15,
),
myLocationEnabled: false,
mapType: Theme.of(context).brightness == Brightness.dark
? IntaleqMapType.normal
: IntaleqMapType.light,
myLocationEnabled: true,
myLocationButtonEnabled: false,
zoomControlsEnabled: false,
compassEnabled: false,
onMapCreated: (c) => _mapController = c,
onMapCreated: (_) {},
markers: _buildMarkers(pickup, dropoff, driverPos),
),
@@ -106,7 +125,9 @@ class _TripTrackingScreenState extends State<TripTrackingScreen> {
// TODO: Chat — Phase 3
},
onCancel: state.canCancel
? () => context.read<TripBloc>().add(const TripCancelRequested())
? () => context
.read<TripBloc>()
.add(const TripCancelRequested())
: null,
),
),
@@ -166,8 +187,9 @@ class _SearchingBodyState extends State<_SearchingBody>
@override
void initState() {
super.initState();
_pulse = AnimationController(vsync: this, duration: const Duration(seconds: 2))
..repeat(reverse: true);
_pulse =
AnimationController(vsync: this, duration: const Duration(seconds: 2))
..repeat(reverse: true);
_scale = Tween<double>(begin: 0.8, end: 1.2).animate(
CurvedAnimation(parent: _pulse, curve: Curves.easeInOut),
);
@@ -213,14 +235,20 @@ class _SearchingBodyState extends State<_SearchingBody>
),
const SizedBox(height: T.s16),
Text(l10n.searchingDriver,
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600),
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(fontWeight: FontWeight.w600),
textAlign: TextAlign.center),
const SizedBox(height: T.s20),
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: () => context.read<TripBloc>().add(const TripCancelRequested()),
style: OutlinedButton.styleFrom(foregroundColor: cs.error, side: BorderSide(color: cs.error)),
onPressed: () =>
context.read<TripBloc>().add(const TripCancelRequested()),
style: OutlinedButton.styleFrom(
foregroundColor: cs.error,
side: BorderSide(color: cs.error)),
child: Text(l10n.cancelSearch),
),
),
@@ -65,7 +65,7 @@ class DriverCard extends StatelessWidget {
children: [
Icon(Icons.star_rounded, size: 16, color: Colors.amber),
const SizedBox(width: 4),
Text('4.8', style: Theme.of(context).textTheme.bodySmall),
Text(l10n.driverRatingValue, style: Theme.of(context).textTheme.bodySmall),
],
),
],