feat: implement core architecture, design system, localization, and trip features for both driver and rider apps.
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../data/models/trip_model.dart';
|
||||
import '../data/trip_repository.dart';
|
||||
import '../data/trip_socket_service.dart';
|
||||
import '../data/trip_event_merger.dart';
|
||||
import 'trip_event.dart';
|
||||
import 'trip_state.dart';
|
||||
|
||||
/// TripBloc — Bloc الوحيد للراكب (docs/03 §14).
|
||||
///
|
||||
/// يتعامل مع Lifecycle كامل: إنشاء → تتبّع → إتمام → تقييم.
|
||||
/// المصادر الثلاث: أحداث المستخدم، WebSocket، FCM — تمر عبر TripEventMerger.
|
||||
class TripBloc extends Bloc<TripEvent, TripState> {
|
||||
final TripRepository _tripRepo;
|
||||
final TripSocketService _socket;
|
||||
final TripEventMerger _merger;
|
||||
Timer? _reconciliationTimer;
|
||||
|
||||
TripBloc({
|
||||
required TripRepository tripRepo,
|
||||
required TripSocketService socket,
|
||||
TripEventMerger? merger,
|
||||
}) : _tripRepo = tripRepo,
|
||||
_socket = socket,
|
||||
_merger = merger ?? TripEventMerger(),
|
||||
super(const TripState()) {
|
||||
on<TripCheckActive>(_onCheckActive);
|
||||
on<TripRequested>(_onRequested);
|
||||
on<TripUpdateReceived>(_onUpdateReceived);
|
||||
on<TripDriverLocation>(_onDriverLocation);
|
||||
on<TripCancelRequested>(_onCancel);
|
||||
on<TripRetry>(_onRetry);
|
||||
on<TripRate>(_onRate);
|
||||
on<TripClear>(_onClear);
|
||||
|
||||
// استمع لتحديثات TripEventMerger.
|
||||
_merger.updateStream.listen((update) {
|
||||
add(TripUpdateReceived(
|
||||
tripId: update.tripId,
|
||||
status: update.status,
|
||||
driverId: update.driverId,
|
||||
extra: update.extra,
|
||||
));
|
||||
});
|
||||
|
||||
_merger.locationStream.listen((loc) {
|
||||
add(TripDriverLocation(
|
||||
tripId: loc.tripId,
|
||||
lat: loc.lat,
|
||||
lng: loc.lng,
|
||||
heading: loc.heading,
|
||||
speed: loc.speed,
|
||||
));
|
||||
});
|
||||
|
||||
// اتصل بالـWebSocket.
|
||||
_socket.connect();
|
||||
}
|
||||
|
||||
/// فحص رحلة نشطة عند بدء التشغيل أو العودة من الخلفية.
|
||||
Future<void> _onCheckActive(TripCheckActive event, Emitter<TripState> emit) async {
|
||||
try {
|
||||
final trip = await _tripRepo.getActiveTrip();
|
||||
if (trip != null) {
|
||||
emit(state.copyWith(
|
||||
requestStatus: _statusFromTrip(trip.status),
|
||||
trip: trip,
|
||||
));
|
||||
_joinTripRoom(trip.id);
|
||||
_startReconciliation(trip.id);
|
||||
}
|
||||
} catch (_) {
|
||||
// خطأ صامت — سنحاول مرة أخرى عند إعادة الاتصال.
|
||||
}
|
||||
}
|
||||
|
||||
/// طلب رحلة جديدة.
|
||||
Future<void> _onRequested(TripRequested event, Emitter<TripState> emit) async {
|
||||
emit(state.copyWith(requestStatus: TripRequestStatus.loading, clearError: true));
|
||||
try {
|
||||
final trip = await _tripRepo.createTrip(
|
||||
pickupLat: event.pickupLat,
|
||||
pickupLng: event.pickupLng,
|
||||
pickupAddress: event.pickupAddress,
|
||||
dropoffLat: event.dropoffLat,
|
||||
dropoffLng: event.dropoffLng,
|
||||
dropoffAddress: event.dropoffAddress,
|
||||
vehicleClass: event.vehicleClass,
|
||||
paymentMethod: event.paymentMethod,
|
||||
couponCode: event.couponCode,
|
||||
);
|
||||
emit(state.copyWith(
|
||||
requestStatus: TripRequestStatus.searching,
|
||||
trip: trip,
|
||||
));
|
||||
_joinTripRoom(trip.id);
|
||||
_startReconciliation(trip.id);
|
||||
} catch (e) {
|
||||
emit(state.copyWith(
|
||||
requestStatus: TripRequestStatus.error,
|
||||
error: e.toString(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// تحديث حالة الرحلة — من WebSocket أو FCM عبر TripEventMerger.
|
||||
void _onUpdateReceived(TripUpdateReceived event, Emitter<TripState> emit) {
|
||||
final currentTrip = state.trip;
|
||||
if (currentTrip == null || currentTrip.id != event.tripId) return;
|
||||
|
||||
// التصفية الصامتة: تجاهل الأحداث القديمة.
|
||||
final currentOrder = statusOrder(currentTrip.status);
|
||||
final newOrder = statusOrder(event.status);
|
||||
if (newOrder >= 0 && newOrder <= currentOrder) return;
|
||||
|
||||
// تحديث النموذج محلياً.
|
||||
final updatedTrip = TripModel(
|
||||
id: currentTrip.id,
|
||||
status: event.status,
|
||||
clientId: currentTrip.clientId,
|
||||
pickupLat: currentTrip.pickupLat,
|
||||
pickupLng: currentTrip.pickupLng,
|
||||
pickupAddress: currentTrip.pickupAddress,
|
||||
dropoffLat: currentTrip.dropoffLat,
|
||||
dropoffLng: currentTrip.dropoffLng,
|
||||
dropoffAddress: currentTrip.dropoffAddress,
|
||||
vehicleClass: currentTrip.vehicleClass,
|
||||
fare: (event.extra['fare'] as num?) ?? currentTrip.fare,
|
||||
currency: event.extra['currency'] as String? ?? currentTrip.currency,
|
||||
paymentMethod: currentTrip.paymentMethod,
|
||||
driverId: event.driverId ?? currentTrip.driverId,
|
||||
createdAt: currentTrip.createdAt,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
final newStatus = _statusFromTrip(event.status);
|
||||
|
||||
// استخراج معلومات السائق من extra.
|
||||
final vehicle = event.extra['vehicle'] as Map<String, dynamic>?;
|
||||
|
||||
emit(state.copyWith(
|
||||
requestStatus: newStatus,
|
||||
trip: updatedTrip,
|
||||
driverName: event.extra['driverName'] as String?,
|
||||
vehicleMake: vehicle?['make'] as String?,
|
||||
vehicleModel: vehicle?['model'] as String?,
|
||||
vehiclePlate: vehicle?['plate'] as String?,
|
||||
vehicleColor: vehicle?['color'] as String?,
|
||||
));
|
||||
|
||||
// حالة نهائية: أوقف مؤقت الم reconciliation.
|
||||
if (isTerminalStatus(event.status)) {
|
||||
_stopReconciliation();
|
||||
_socket.leaveTrip(event.tripId);
|
||||
}
|
||||
}
|
||||
|
||||
/// موقع السائق الحي.
|
||||
void _onDriverLocation(TripDriverLocation event, Emitter<TripState> emit) {
|
||||
if (state.trip?.id != event.tripId) return;
|
||||
emit(state.copyWith(
|
||||
driverLat: event.lat,
|
||||
driverLng: event.lng,
|
||||
driverHeading: event.heading,
|
||||
));
|
||||
}
|
||||
|
||||
/// إلغاء الرحلة.
|
||||
Future<void> _onCancel(TripCancelRequested event, Emitter<TripState> emit) async {
|
||||
if (state.trip == null) return;
|
||||
try {
|
||||
final trip = await _tripRepo.cancelTrip(state.trip!.id);
|
||||
_stopReconciliation();
|
||||
_socket.leaveTrip(state.trip!.id);
|
||||
emit(state.copyWith(
|
||||
requestStatus: TripRequestStatus.completed,
|
||||
trip: trip,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(error: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
/// إعادة المحاولة بعد خطأ.
|
||||
void _onRetry(TripRetry event, Emitter<TripState> emit) {
|
||||
emit(state.copyWith(requestStatus: TripRequestStatus.idle, clearError: true));
|
||||
}
|
||||
|
||||
/// تقييم الرحلة.
|
||||
Future<void> _onRate(TripRate event, Emitter<TripState> emit) async {
|
||||
if (state.trip == null) return;
|
||||
try {
|
||||
await _tripRepo.rateTrip(state.trip!.id, stars: event.stars, comment: event.comment);
|
||||
emit(state.copyWith(rated: true));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(error: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
/// مسح الرحلة من الذاكرة.
|
||||
void _onClear(TripClear event, Emitter<TripState> emit) {
|
||||
_stopReconciliation();
|
||||
emit(const TripState());
|
||||
}
|
||||
|
||||
// --- مساعدات ---
|
||||
|
||||
void _joinTripRoom(String tripId) {
|
||||
_socket.joinTrip(tripId);
|
||||
}
|
||||
|
||||
void _startReconciliation(String tripId) {
|
||||
_stopReconciliation();
|
||||
_reconciliationTimer = Timer.periodic(const Duration(seconds: 25), (_) async {
|
||||
try {
|
||||
final trip = await _tripRepo.getTrip(tripId);
|
||||
if (trip != null) {
|
||||
add(TripUpdateReceived(
|
||||
tripId: trip.id,
|
||||
status: trip.status,
|
||||
driverId: trip.driverId,
|
||||
extra: {
|
||||
if (trip.fare != null) 'fare': trip.fare,
|
||||
if (trip.currency != null) 'currency': trip.currency,
|
||||
},
|
||||
));
|
||||
}
|
||||
} catch (_) {
|
||||
// خطأ صامت — سيحاول مرة أخرى.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _stopReconciliation() {
|
||||
_reconciliationTimer?.cancel();
|
||||
_reconciliationTimer = null;
|
||||
}
|
||||
|
||||
TripRequestStatus _statusFromTrip(String status) {
|
||||
switch (status) {
|
||||
case 'searching':
|
||||
return TripRequestStatus.searching;
|
||||
case 'assigned':
|
||||
case 'driver_arriving':
|
||||
case 'driver_arrived':
|
||||
case 'in_progress':
|
||||
return TripRequestStatus.active;
|
||||
case 'completed':
|
||||
case 'paid':
|
||||
return TripRequestStatus.completed;
|
||||
case 'cancelled':
|
||||
case 'no_drivers':
|
||||
case 'expired':
|
||||
return TripRequestStatus.completed;
|
||||
default:
|
||||
return TripRequestStatus.searching;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_stopReconciliation();
|
||||
_merger.dispose();
|
||||
_socket.disconnect();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user