feat: implement core architecture, design system, localization, and trip features for both driver and rider apps.
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
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 '../bloc/trip_bloc.dart';
|
||||
import '../bloc/trip_state.dart';
|
||||
import '../bloc/trip_event.dart';
|
||||
|
||||
/// شاشة تقييم الرحلة.
|
||||
class RatingScreen extends StatefulWidget {
|
||||
const RatingScreen({super.key});
|
||||
|
||||
@override
|
||||
State<RatingScreen> createState() => _RatingScreenState();
|
||||
}
|
||||
|
||||
class _RatingScreenState extends State<RatingScreen> {
|
||||
int _stars = 0;
|
||||
final _commentController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_commentController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return BlocListener<TripBloc, TripState>(
|
||||
listenWhen: (prev, cur) => prev.rated != cur.rated,
|
||||
listener: (context, state) {
|
||||
if (state.rated) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.thankYou)),
|
||||
);
|
||||
context.read<TripBloc>().add(const TripClear());
|
||||
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),
|
||||
|
||||
// نجوم التقييم
|
||||
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),
|
||||
|
||||
// حقل التعليق
|
||||
TextField(
|
||||
controller: _commentController,
|
||||
maxLines: 3,
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.ratingComment,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: T.s8),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
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 '../bloc/trip_bloc.dart';
|
||||
import '../bloc/trip_state.dart';
|
||||
import '../bloc/trip_event.dart';
|
||||
|
||||
/// شاشة فاتورة الرحلة بعد الانتهاء.
|
||||
class TripInvoiceScreen extends StatelessWidget {
|
||||
const TripInvoiceScreen({super.key});
|
||||
|
||||
@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()),
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
),
|
||||
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(),
|
||||
|
||||
// أزرار الإجراءات
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FareRow extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final bool bold;
|
||||
|
||||
const _FareRow({required this.label, required this.value, this.bold = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final style = bold
|
||||
? Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700)
|
||||
: Theme.of(context).textTheme.bodyMedium;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label, style: style),
|
||||
Text(value, style: style),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
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 '../bloc/trip_bloc.dart';
|
||||
import '../bloc/trip_state.dart';
|
||||
import '../bloc/trip_event.dart';
|
||||
import 'widgets/driver_card.dart';
|
||||
import 'widgets/trip_status_banner.dart';
|
||||
|
||||
/// شاشة تتبع الرحلة — من searching إلى completed.
|
||||
class TripTrackingScreen extends StatefulWidget {
|
||||
const TripTrackingScreen({super.key});
|
||||
|
||||
@override
|
||||
State<TripTrackingScreen> createState() => _TripTrackingScreenState();
|
||||
}
|
||||
|
||||
class _TripTrackingScreenState extends State<TripTrackingScreen> {
|
||||
IntaleqMapController? _mapController;
|
||||
Timer? _cameraFollowTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// مراقبة حالة التطبيق للتحقق من الرحلة النشطة عند العودة.
|
||||
WidgetsBinding.instance.addObserver(_AppLifecycleObserver(context));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cameraFollowTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@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,
|
||||
listener: (context, state) {
|
||||
if (state.isCompleted) {
|
||||
// الرحلة اكتملت — انتقل لشاشة الفاتورة.
|
||||
context.pushReplacement('/trip-invoice');
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
final trip = state.trip;
|
||||
if (trip == null) {
|
||||
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
||||
}
|
||||
|
||||
final pickup = LatLng(trip.pickupLat, trip.pickupLng);
|
||||
final dropoff = LatLng(trip.dropoffLat, trip.dropoffLng);
|
||||
final driverPos = state.driverLat != null && state.driverLng != null
|
||||
? LatLng(state.driverLat!, state.driverLng!)
|
||||
: null;
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
// الخريطة
|
||||
IntaleqMap(
|
||||
apiKey: '',
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: driverPos ?? pickup,
|
||||
zoom: 15,
|
||||
),
|
||||
myLocationEnabled: false,
|
||||
zoomControlsEnabled: false,
|
||||
compassEnabled: false,
|
||||
onMapCreated: (c) => _mapController = c,
|
||||
markers: _buildMarkers(pickup, dropoff, driverPos),
|
||||
),
|
||||
|
||||
// شريط الحالة العلوي
|
||||
Positioned(
|
||||
top: MediaQuery.of(context).padding.top + T.s8,
|
||||
left: T.pagePadding,
|
||||
right: T.pagePadding,
|
||||
child: TripStatusBanner(status: trip.status),
|
||||
),
|
||||
|
||||
// بطاقة السائق (إذا تم تعيينه)
|
||||
if (state.requestStatus == TripRequestStatus.active)
|
||||
Positioned(
|
||||
left: T.pagePadding,
|
||||
right: T.pagePadding,
|
||||
bottom: T.pagePadding,
|
||||
child: DriverCard(
|
||||
driverName: state.driverName ?? '',
|
||||
vehicleMake: state.vehicleMake ?? '',
|
||||
vehicleModel: state.vehicleModel ?? '',
|
||||
vehiclePlate: state.vehiclePlate ?? '',
|
||||
vehicleColor: state.vehicleColor ?? '',
|
||||
status: trip.status,
|
||||
onCall: () {
|
||||
// TODO: WebRTC calling — Phase 3
|
||||
},
|
||||
onChat: () {
|
||||
// TODO: Chat — Phase 3
|
||||
},
|
||||
onCancel: state.canCancel
|
||||
? () => context.read<TripBloc>().add(const TripCancelRequested())
|
||||
: null,
|
||||
),
|
||||
),
|
||||
|
||||
// شاشة البحث (قبل تعيين السائق)
|
||||
if (state.requestStatus == TripRequestStatus.searching)
|
||||
_SearchingBody(elapsed: 0),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Set<Marker> _buildMarkers(LatLng pickup, LatLng dropoff, LatLng? driverPos) {
|
||||
final markers = <Marker>{
|
||||
Marker(
|
||||
markerId: const MarkerId('pickup'),
|
||||
position: pickup,
|
||||
icon: InlqBitmap.defaultMarkerWithHue(210),
|
||||
anchor: const Offset(0.5, 1.0),
|
||||
),
|
||||
Marker(
|
||||
markerId: const MarkerId('dropoff'),
|
||||
position: dropoff,
|
||||
icon: InlqBitmap.defaultMarkerWithHue(0),
|
||||
anchor: const Offset(0.5, 1.0),
|
||||
),
|
||||
};
|
||||
if (driverPos != null) {
|
||||
markers.add(Marker(
|
||||
markerId: const MarkerId('driver'),
|
||||
position: driverPos,
|
||||
icon: InlqBitmap.defaultMarkerWithHue(140),
|
||||
anchor: const Offset(0.5, 0.5),
|
||||
zIndex: 3,
|
||||
));
|
||||
}
|
||||
return markers;
|
||||
}
|
||||
}
|
||||
|
||||
/// شاشة البحث عن سائق مع مؤقت.
|
||||
class _SearchingBody extends StatefulWidget {
|
||||
final int elapsed;
|
||||
const _SearchingBody({required this.elapsed});
|
||||
|
||||
@override
|
||||
State<_SearchingBody> createState() => _SearchingBodyState();
|
||||
}
|
||||
|
||||
class _SearchingBodyState extends State<_SearchingBody>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _pulse;
|
||||
late Animation<double> _scale;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_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),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pulse.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return Material(
|
||||
color: Colors.black54,
|
||||
child: Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(T.pagePadding * 2),
|
||||
padding: const EdgeInsets.all(T.pagePadding * 1.5),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surface,
|
||||
borderRadius: BorderRadius.circular(T.r16),
|
||||
boxShadow: [T.shadowLg],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ScaleTransition(
|
||||
scale: _scale,
|
||||
child: Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.primaryContainer,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.directions_car_filled_rounded,
|
||||
size: 40, color: cs.onPrimaryContainer),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: T.s16),
|
||||
Text(l10n.searchingDriver,
|
||||
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)),
|
||||
child: Text(l10n.cancelSearch),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// مراقب دورة حياة التطبيق — يتحقق من الرحلة عند العودة.
|
||||
class _AppLifecycleObserver extends WidgetsBindingObserver {
|
||||
final BuildContext context;
|
||||
_AppLifecycleObserver(this.context);
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
context.read<TripBloc>().add(const TripCheckActive());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/l10n/app_localizations.dart';
|
||||
import '../../../../core/design/tokens.dart';
|
||||
|
||||
/// بطاقة السائق في شاشة التتبع.
|
||||
class DriverCard extends StatelessWidget {
|
||||
final String driverName;
|
||||
final String vehicleMake;
|
||||
final String vehicleModel;
|
||||
final String vehiclePlate;
|
||||
final String vehicleColor;
|
||||
final String status;
|
||||
final VoidCallback? onCall;
|
||||
final VoidCallback? onChat;
|
||||
final VoidCallback? onCancel;
|
||||
|
||||
const DriverCard({
|
||||
super.key,
|
||||
required this.driverName,
|
||||
required this.vehicleMake,
|
||||
required this.vehicleModel,
|
||||
required this.vehiclePlate,
|
||||
required this.vehicleColor,
|
||||
required this.status,
|
||||
this.onCall,
|
||||
this.onChat,
|
||||
this.onCancel,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return Card(
|
||||
elevation: 4,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(T.pagePadding),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// صف الصورة + المعلومات
|
||||
Row(
|
||||
children: [
|
||||
// صورة السائق ( Placeholder )
|
||||
CircleAvatar(
|
||||
radius: 28,
|
||||
backgroundColor: cs.primaryContainer,
|
||||
child: Icon(Icons.person, color: cs.onPrimaryContainer),
|
||||
),
|
||||
const SizedBox(width: T.s12),
|
||||
// اسم السائق + التقييم
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
driverName.isNotEmpty ? driverName : l10n.loading,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.star_rounded, size: 16, color: Colors.amber),
|
||||
const SizedBox(width: 4),
|
||||
Text('4.8', style: Theme.of(context).textTheme.bodySmall),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: T.s12),
|
||||
const Divider(height: 1),
|
||||
const SizedBox(height: T.s12),
|
||||
|
||||
// معلومات المركبة
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.directions_car_filled_rounded, size: 20, color: cs.onSurfaceVariant),
|
||||
const SizedBox(width: T.s8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'$vehicleColor $vehicleMake $vehicleModel',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
// رقم اللوحة — بارز بوضوح
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: T.s8, vertical: T.s4),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(T.r8),
|
||||
),
|
||||
child: Text(
|
||||
vehiclePlate,
|
||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: T.s12),
|
||||
|
||||
// أزرار الإجراءات
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: onCall,
|
||||
icon: const Icon(Icons.call_rounded, size: 18),
|
||||
label: Text(l10n.callDriver),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: T.s8),
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: onChat,
|
||||
icon: const Icon(Icons.chat_rounded, size: 18),
|
||||
label: Text(l10n.chatWithDriver),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
if (onCancel != null) ...[
|
||||
const SizedBox(height: T.s8),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: TextButton(
|
||||
onPressed: onCancel,
|
||||
child: Text(l10n.cancel, style: TextStyle(color: cs.error)),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/l10n/app_localizations.dart';
|
||||
import '../../../../core/design/tokens.dart';
|
||||
|
||||
/// شريط الحالة العلوي في شاشة التتبع.
|
||||
class TripStatusBanner extends StatelessWidget {
|
||||
final String status;
|
||||
|
||||
const TripStatusBanner({super.key, required this.status});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
final (text, icon, color) = switch (status) {
|
||||
'searching' => (l10n.searchingDriver, Icons.search_rounded, cs.primary),
|
||||
'assigned' || 'driver_arriving' => (l10n.driverArriving, Icons.directions_car_rounded, cs.tertiary),
|
||||
'driver_arrived' => (l10n.driverArrived, Icons.location_on_rounded, Colors.green),
|
||||
'in_progress' => (l10n.rideInProgress, Icons.play_arrow_rounded, cs.primary),
|
||||
'completed' => (l10n.rideCompleted, Icons.check_circle_rounded, Colors.green),
|
||||
'cancelled' => (l10n.tripCancelled, Icons.cancel_rounded, cs.error),
|
||||
'no_drivers' => (l10n.tripNoDrivers, Icons.error_outline_rounded, cs.error),
|
||||
_ => (l10n.loading, Icons.hourglass_empty_rounded, cs.onSurfaceVariant),
|
||||
};
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: T.pagePadding, vertical: T.s12),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(T.r12),
|
||||
border: Border.all(color: color.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: color, size: 20),
|
||||
const SizedBox(width: T.s8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
color: color,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user