Files
tripz-llc/apps/rider/lib/features/trip/view/trip_tracking_screen.dart
T

247 lines
8.0 KiB
Dart

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());
}
}
}