feat: implement core architecture, design system, localization, and trip features for both driver and rider apps.
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
import 'dart:async';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../data/offers_repository.dart';
|
||||
import 'package:driver/features/home/data/models/trip_offer_model.dart';
|
||||
import '../../trip/data/driver_socket_service.dart';
|
||||
import '../../trip/data/trip_event_merger.dart';
|
||||
|
||||
// ─── أحداث ───
|
||||
|
||||
sealed class OffersEvent extends Equatable {
|
||||
const OffersEvent();
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// بدء الاستماع للعروض (بعد الاتصال بالـWebSocket).
|
||||
class OffersStarted extends OffersEvent {
|
||||
const OffersStarted();
|
||||
}
|
||||
|
||||
/// عرض رحلة وارد (من WebSocket).
|
||||
class OfferReceived extends OffersEvent {
|
||||
final TripOfferModel offer;
|
||||
const OfferReceived(this.offer);
|
||||
@override
|
||||
List<Object?> get props => [offer.tripId];
|
||||
}
|
||||
|
||||
/// العرض تم قبوله من سائق آخر.
|
||||
class OfferTaken extends OffersEvent {
|
||||
final String tripId;
|
||||
const OfferTaken(this.tripId);
|
||||
@override
|
||||
List<Object?> get props => [tripId];
|
||||
}
|
||||
|
||||
/// العرض انتهت مهلته (من المؤقت).
|
||||
class OfferExpired extends OffersEvent {
|
||||
final String tripId;
|
||||
const OfferExpired(this.tripId);
|
||||
@override
|
||||
List<Object?> get props => [tripId];
|
||||
}
|
||||
|
||||
/// قبول العرض.
|
||||
class OfferAccepted extends OffersEvent {
|
||||
final String tripId;
|
||||
const OfferAccepted(this.tripId);
|
||||
@override
|
||||
List<Object?> get props => [tripId];
|
||||
}
|
||||
|
||||
/// رفض العرض يدوياً.
|
||||
class OfferRejected extends OffersEvent {
|
||||
final String tripId;
|
||||
const OfferRejected(this.tripId);
|
||||
@override
|
||||
List<Object?> get props => [tripId];
|
||||
}
|
||||
|
||||
/// مسح كل العروض (عند بدء رحلة نشطة).
|
||||
class OffersClear extends OffersEvent {
|
||||
const OffersClear();
|
||||
}
|
||||
|
||||
// ─── حالة ───
|
||||
|
||||
class OffersState extends Equatable {
|
||||
final List<TripOfferModel> offers;
|
||||
final String? acceptedTripId;
|
||||
final bool accepting;
|
||||
final String? error;
|
||||
|
||||
const OffersState({
|
||||
this.offers = const [],
|
||||
this.acceptedTripId,
|
||||
this.accepting = false,
|
||||
this.error,
|
||||
});
|
||||
|
||||
OffersState copyWith({
|
||||
List<TripOfferModel>? offers,
|
||||
String? acceptedTripId,
|
||||
bool? accepting,
|
||||
String? error,
|
||||
bool clearError = false,
|
||||
bool clearAccepted = false,
|
||||
}) {
|
||||
return OffersState(
|
||||
offers: offers ?? this.offers,
|
||||
acceptedTripId: clearAccepted ? null : (acceptedTripId ?? this.acceptedTripId),
|
||||
accepting: accepting ?? this.accepting,
|
||||
error: clearError ? null : (error ?? this.error),
|
||||
);
|
||||
}
|
||||
|
||||
/// هل هناك عرض واحد على الأقل؟
|
||||
bool get hasOffers => offers.isNotEmpty;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [offers, acceptedTripId, accepting, error];
|
||||
}
|
||||
|
||||
// ─── Bloc ───
|
||||
|
||||
/// OffersBloc — Bloc الثاني للسائق (docs/03 §14).
|
||||
///
|
||||
/// يستقبل عروض من WebSocket ويدير القبول/الرفض والمؤقت.
|
||||
/// العروض تُعرض كـstack: الأحدث أولاً.
|
||||
class OffersBloc extends Bloc<OffersEvent, OffersState> {
|
||||
final OffersRepository _offersRepo;
|
||||
final DriverSocketService _socket;
|
||||
final DriverTripEventMerger _merger;
|
||||
|
||||
/// مؤقتات المهلة لكل عرض ( tripId → Timer ).
|
||||
final Map<String, Timer> _expiryTimers = {};
|
||||
|
||||
OffersBloc({
|
||||
required OffersRepository offersRepo,
|
||||
required DriverSocketService socket,
|
||||
DriverTripEventMerger? merger,
|
||||
}) : _offersRepo = offersRepo,
|
||||
_socket = socket,
|
||||
_merger = merger ?? DriverTripEventMerger(),
|
||||
super(const OffersState()) {
|
||||
on<OffersStarted>(_onStarted);
|
||||
on<OfferReceived>(_onOfferReceived);
|
||||
on<OfferTaken>(_onOfferTaken);
|
||||
on<OfferExpired>(_onOfferExpired);
|
||||
on<OfferAccepted>(_onOfferAccepted);
|
||||
on<OfferRejected>(_onOfferRejected);
|
||||
on<OffersClear>(_onClear);
|
||||
|
||||
// استمع لتحديثات Merger.
|
||||
_merger.offerStream.listen((offer) {
|
||||
add(OfferReceived(offer));
|
||||
});
|
||||
|
||||
_merger.offerTakenStream.listen((tripId) {
|
||||
add(OfferTaken(tripId));
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onStarted(OffersStarted event, Emitter<OffersState> emit) async {
|
||||
_socket.connect();
|
||||
}
|
||||
|
||||
void _onOfferReceived(OfferReceived event, Emitter<OffersState> emit) {
|
||||
// لا تضف عروضاً مكررة.
|
||||
final exists = state.offers.any((o) => o.tripId == event.offer.tripId);
|
||||
if (exists) return;
|
||||
|
||||
final updated = [event.offer, ...state.offers];
|
||||
emit(state.copyWith(offers: updated, clearError: true));
|
||||
|
||||
// ابدأ مؤقت المهلة.
|
||||
_startExpiryTimer(event.offer);
|
||||
}
|
||||
|
||||
void _onOfferTaken(OfferTaken event, Emitter<OffersState> emit) {
|
||||
final updated = state.offers.where((o) => o.tripId != event.tripId).toList();
|
||||
_cancelExpiryTimer(event.tripId);
|
||||
emit(state.copyWith(offers: updated));
|
||||
}
|
||||
|
||||
void _onOfferExpired(OfferExpired event, Emitter<OffersState> emit) {
|
||||
final updated = state.offers.where((o) => o.tripId != event.tripId).toList();
|
||||
_cancelExpiryTimer(event.tripId);
|
||||
emit(state.copyWith(offers: updated));
|
||||
}
|
||||
|
||||
Future<void> _onOfferAccepted(OfferAccepted event, Emitter<OffersState> emit) async {
|
||||
emit(state.copyWith(accepting: true, clearError: true));
|
||||
|
||||
final trip = await _offersRepo.acceptTrip(event.tripId);
|
||||
if (trip == null) {
|
||||
emit(state.copyWith(accepting: false, error: 'Failed to accept trip'));
|
||||
return;
|
||||
}
|
||||
|
||||
// أزل كل العروض المتبقية (تم اختيار سائق).
|
||||
_cancelAllTimers();
|
||||
emit(state.copyWith(
|
||||
offers: [],
|
||||
accepting: false,
|
||||
acceptedTripId: event.tripId,
|
||||
));
|
||||
}
|
||||
|
||||
void _onOfferRejected(OfferRejected event, Emitter<OffersState> emit) {
|
||||
final updated = state.offers.where((o) => o.tripId != event.tripId).toList();
|
||||
_cancelExpiryTimer(event.tripId);
|
||||
emit(state.copyWith(offers: updated));
|
||||
}
|
||||
|
||||
void _onClear(OffersClear event, Emitter<OffersState> emit) {
|
||||
_cancelAllTimers();
|
||||
emit(const OffersState());
|
||||
}
|
||||
|
||||
// ─── مساعدات المؤقتات ───
|
||||
|
||||
void _startExpiryTimer(TripOfferModel offer) {
|
||||
final remaining = offer.expiresAt.difference(DateTime.now());
|
||||
if (remaining.isNegative) {
|
||||
add(OfferExpired(offer.tripId));
|
||||
return;
|
||||
}
|
||||
|
||||
_expiryTimers[offer.tripId]?.cancel();
|
||||
_expiryTimers[offer.tripId] = Timer(remaining, () {
|
||||
add(OfferExpired(offer.tripId));
|
||||
});
|
||||
}
|
||||
|
||||
void _cancelExpiryTimer(String tripId) {
|
||||
_expiryTimers[tripId]?.cancel();
|
||||
_expiryTimers.remove(tripId);
|
||||
}
|
||||
|
||||
void _cancelAllTimers() {
|
||||
for (final timer in _expiryTimers.values) {
|
||||
timer.cancel();
|
||||
}
|
||||
_expiryTimers.clear();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_cancelAllTimers();
|
||||
_merger.dispose();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user