import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/design/tokens.dart'; import '../../../core/design/tripz_colors.dart'; import '../../../core/di.dart'; import '../../../core/l10n/l10n.dart'; import '../../../core/ui/app_drawer.dart'; import '../../../core/ui/status_banner.dart'; import '../../../core/ui/tripz_button.dart'; import '../cubit/ride_cubit.dart'; import '../cubit/ride_state.dart'; import '../cubit/trip_simulator.dart'; import '../data/models/geo_point.dart'; import '../data/models/route_info.dart'; import 'widgets/active_trip_sheet.dart'; import 'widgets/center_pin.dart'; import 'widgets/confirm_sheet.dart'; import 'widgets/place_search_dialog.dart'; import 'widgets/planner_sheet.dart'; import 'widgets/ride_map.dart'; import 'widgets/searching_sheet.dart'; /// شاشة الراكب: خريطة ثابتة + ورقة سفلية تتبدّل بالمرحلة (docs/39 §3). /// /// الخريطة **لا تُعاد بناؤها** عند تبدّل الورقة: هي خارج الـ`BlocBuilder` /// الذي يبني الورقة، ولها `BlocSelector` خاص بما تحتاجه وحدها. هذا هو الفرق /// بين خريطة سلسة وأخرى تهتزّ عند كل تحديث حالة. class RidePage extends StatefulWidget { const RidePage({super.key}); @override State createState() => _RidePageState(); } /// نصيب الورقة السفلية من ارتفاع الشاشة — الخريطة تأخذ ما تبقّى (~67%). /// /// قرار المالك 2026-08-05: الخريطة تُرفع عن القاع بالثلث كي تبقى مساحة /// الورقة خالصة لها. سيرو يرفعها بـ20%، وتريبز بـ33% لأن ورقته تعرض /// خطوطاً أكثر (الخط الزمني + الأنواع + السعر). const double _sheetFraction = 0.33; class _RidePageState extends State with WidgetsBindingObserver { late final RideCubit _cubit = sl()..init(); late final TripSimulator _simulator = TripSimulator(_cubit); GeoPoint? _mapCenter; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); _simulator.stop(); _cubit.close(); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState lifecycle) { // الإذن قد يُمنح من إعدادات النظام والتطبيق في الخلفية — نلتقطه عند // العودة بدل أن تبقى الخريطة على الموقع الاحتياطي. if (lifecycle == AppLifecycleState.resumed && _cubit.state.origin == null) { _cubit.locateMe(); } } @override Widget build(BuildContext context) { return BlocProvider.value( value: _cubit, child: Scaffold( drawer: const AppDrawer(), body: Stack( children: [ // ── الخريطة ──────────────────────────────────────────────── // // **مرفوعة عن القاع بمقدار [_sheetFraction]** لا مالئةً الشاشة: // الثلث السفلي مساحة الورقة وحدها، فلا تغطّي الورقةُ الخريطةَ // (نمط سيرو — `Positioned(bottom: Get.height * .2, top: 0)` في // `google_map_passenger_widget.dart`). // // والدبّوس **داخل نفس المنطقة** لا في الـ`Stack` الخارجي: مركز // الكاميرا (`onCameraIdle`) هو مركز ودجت الخريطة، فلو بقي // الدبّوس متمركزاً على الشاشة لأشار إلى نقطة غير التي تُؤكَّد. Positioned( top: 0, left: 0, right: 0, bottom: MediaQuery.sizeOf(context).height * _sheetFraction, child: Stack( children: [ BlocSelector( selector: (s) => _MapData( origin: s.origin, destination: s.destination, driver: s.driverLocation, route: s.route, pickupRoute: s.pickupRoute, camera: s.cameraTarget, originLabel: s.originLabel, destinationLabel: s.destinationLabel, ), builder: (context, data) => RideMap( origin: data.origin, destination: data.destination, driver: data.driver, route: data.route, pickupRoute: data.pickupRoute, cameraTarget: data.camera, onCameraIdle: (p) => _mapCenter = p, // الورقة لم تعد تغطّي الخريطة — تبقى حشوة تنفّس فقط // كي لا يلتصق المسار بحافّة الخريطة السفلية. bottomInset: Space.xxl, originLabel: data.originLabel, destinationLabel: data.destinationLabel, ), ), // ── دبّوس الاختيار ───────────────────────────────────── BlocSelector( selector: (s) => s.phase == RidePhase.pickingOnMap, builder: (context, picking) => picking ? const CenterPin() : const SizedBox.shrink(), ), ], ), ), // زر القائمة — يطفو فوق الخريطة، ويحترم المنطقة الآمنة. SafeArea( child: Padding( padding: Space.page, child: Row( children: [ Builder( builder: (context) => _MapFab( icon: Icons.menu_rounded, onTap: Scaffold.of(context).openDrawer, ), ), // زرّ المحاكاة — يظهر فقط مع // `--dart-define=SIMULATE_TRIP=true`. الشرط `const` فيُحذف // هو وما تحته من بناء الإنتاج. if (RideCubit.simulationEnabled) ...[ const SizedBox(width: Space.xs), _MapFab( icon: Icons.play_arrow_rounded, onTap: () => _simulator.start(), ), ], ], ), ), ), // ── الورقة السفلية ───────────────────────────────────────── Align( alignment: Alignment.bottomCenter, child: BlocBuilder( builder: (context, state) => _Sheet( state: state, onConfirmPick: () { final center = _mapCenter; if (center != null) _cubit.confirmMapPick(center); }, ), ), ), ], ), ), ); } } /// ما تحتاجه الخريطة وحدها — تغيّرُ أي شيء آخر في الحالة لا يعيد بناءها. class _MapData { const _MapData({ this.origin, this.destination, this.driver, this.route, this.pickupRoute, this.camera, this.originLabel = '', this.destinationLabel = '', }); final GeoPoint? origin; final GeoPoint? destination; final GeoPoint? driver; final RouteInfo? route; final RouteInfo? pickupRoute; final GeoPoint? camera; final String originLabel; final String destinationLabel; @override bool operator ==(Object other) => other is _MapData && other.origin == origin && other.destination == destination && other.driver == driver && other.route == route && other.pickupRoute == pickupRoute && other.camera == camera && other.originLabel == originLabel && other.destinationLabel == destinationLabel; @override int get hashCode => Object.hash( origin, destination, driver, route, pickupRoute, camera, originLabel, destinationLabel, ); } class _Sheet extends StatelessWidget { const _Sheet({required this.state, required this.onConfirmPick}); final RideState state; final VoidCallback onConfirmPick; @override Widget build(BuildContext context) { final child = switch (state.phase) { RidePhase.idle => _Collapsed( noLocation: state.error == RideError.noLocation, originLabel: state.originLabel, ), RidePhase.planning => PlannerSheet(state: state), RidePhase.pickingOnMap => _PickConfirm(onConfirm: onConfirmPick), RidePhase.confirming => ConfirmSheet(state: state), RidePhase.searching => const SearchingSheet(), RidePhase.active => ActiveTripSheet( trip: state.trip!, pickupRoute: state.pickupRoute, ), }; return Container( width: double.infinity, // الحدّ الأدنى = المساحة المحجوزة للورقة تحت الخريطة، وإلا ظهر فراغُ // الـ`Scaffold` بينهما في المراحل قصيرة المحتوى (`idle` مثلاً). // وهو حدّ أدنى لا ثابت: `active` و`confirming` يكبران فوقه بحرّية. constraints: BoxConstraints( minHeight: MediaQuery.sizeOf(context).height * _sheetFraction, ), decoration: BoxDecoration( color: context.colors.surface, borderRadius: Radii.sheetTop, ), child: SafeArea( top: false, child: Padding( padding: Space.page, child: AnimatedSize( duration: Motion.inScreen, curve: Motion.curve, alignment: Alignment.bottomCenter, child: child, ), ), ), ); } } /// زر دائري فوق الخريطة — سطح مرتفع كي يُقرأ فوق أي لون بلاطة. class _MapFab extends StatelessWidget { const _MapFab({required this.icon, required this.onTap}); final IconData icon; final VoidCallback onTap; @override Widget build(BuildContext context) { return Align( alignment: AlignmentDirectional.topStart, child: Material( color: context.tripzColors.surfaceRaised, shape: const CircleBorder(), clipBehavior: Clip.antiAlias, child: InkWell( onTap: onTap, child: Padding( padding: EdgeInsets.all(Space.sm), child: Icon(icon, size: Sizes.icon), ), ), ), ); } } /// البطاقة المطوية — واجهة «إلى أين؟». /// /// حقل بحث حقيقي لا زرّ يفتح شاشة: الراكب يبدأ الكتابة فوراً. وموقعه الحالي /// معروض فوقه كي يتأكّد أن نقطة البداية صحيحة قبل أن يفكّر في الوجهة. class _Collapsed extends StatelessWidget { const _Collapsed({required this.noLocation, required this.originLabel}); final bool noLocation; final String originLabel; @override Widget build(BuildContext context) { final cubit = context.read(); return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // بلا موقع لا خريطة مفيدة — السبب يُعرض مع مخرجه بدل خريطة صامتة // على موقع خاطئ. if (noLocation) ...[ StatusBanner( message: context.l10n.rideNoLocation, tone: BannerTone.warning, icon: Icons.location_off_outlined, trailing: TripzButton.text( label: context.l10n.actionRetry, onPressed: cubit.locateMe, ), ), const SizedBox(height: Space.sm), ], if (originLabel.isNotEmpty) ...[ Row( children: [ Container( width: 10, height: 10, decoration: BoxDecoration( color: context.tripzColors.mapOrigin, shape: BoxShape.circle, ), ), const SizedBox(width: Space.xs), Expanded( child: Text( originLabel, maxLines: 1, overflow: TextOverflow.ellipsis, style: context.texts.bodySmall?.copyWith( color: context.colors.onSurfaceVariant, ), ), ), ], ), const SizedBox(height: Space.sm), ], // حقل بحث لا زرّ: الضغط عليه يفتح بحث الأماكن بملء الشاشة مباشرةً // (docs/40 §2) — لا ورقةً وسيطة ثم بحثاً داخلها. // // الحشوة سخيّة عمداً: الثلث السفلي صار مساحة الورقة وحدها بعد رفع // الخريطة، وحقلٌ نحيف وسط فراغ يبدو مقصوصاً لا أنيقاً. Material( color: context.colors.surfaceContainerHighest, borderRadius: Radii.card_, child: InkWell( onTap: () => PlaceSearchDialog.show( context, target: PickTarget.destination, ), borderRadius: Radii.card_, child: Padding( padding: const EdgeInsets.symmetric( horizontal: Space.md, vertical: Space.md, ), child: Row( children: [ Icon( Icons.search_rounded, size: Sizes.icon, color: context.colors.primary, ), const SizedBox(width: Space.sm), Expanded( child: Text( context.l10n.rideWhereTo, style: context.texts.titleMedium?.copyWith( color: context.colors.onSurfaceVariant, ), ), ), Icon( Icons.chevron_left_rounded, color: context.colors.onSurfaceVariant, ), ], ), ), ), ), // الوجهات الأخيرة — نمط سيرو (`rp_favorites.dart: RpRecentsRow`). // أكثر الرحلات تتكرّر، وبلا هذا يعيد الراكب كتابة الاسم نفسه كل مرة. const _RecentsRow(), ], ); } } /// صفّ أفقي للوجهات الأخيرة — يختفي كلياً حين لا توجد، فلا يترك فراغاً. class _RecentsRow extends StatelessWidget { const _RecentsRow(); @override Widget build(BuildContext context) { final cubit = context.read(); final places = cubit.recentPlaces; if (places.isEmpty) return const SizedBox.shrink(); return Padding( padding: const EdgeInsets.only(top: Space.sm), child: SizedBox( height: Sizes.touchTarget, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: places.length, separatorBuilder: (context, i) => const SizedBox(width: Space.xs), itemBuilder: (context, i) { final place = places[i]; return ActionChip( avatar: Icon( Icons.history_rounded, size: Sizes.iconSm, color: context.colors.onSurfaceVariant, ), label: Text( place.label, maxLines: 1, overflow: TextOverflow.ellipsis, ), onPressed: () => cubit.pickPlace(place, PickTarget.destination), ); }, ), ), ); } } class _PickConfirm extends StatelessWidget { const _PickConfirm({required this.onConfirm}); final VoidCallback onConfirm; @override Widget build(BuildContext context) { return TripzButton.primary( label: context.l10n.rideConfirmPoint, onPressed: onConfirm, ); } }