247 lines
8.0 KiB
Dart
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());
|
|
}
|
|
}
|
|
}
|