import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:intaleq_maps/intaleq_maps.dart'; import '../../../core/l10n/app_localizations.dart'; import '../../../core/design/tokens.dart'; import '../../../core/design/tripz_colors.dart'; import '../../../core/di.dart'; import '../cubit/destination_cubit.dart'; import '../cubit/home_cubit.dart'; import '../data/maps_repository.dart'; import '../data/models/place_model.dart'; const int _maxStops = 2; class DestinationScreen extends StatefulWidget { final LatLng? currentLocation; const DestinationScreen({super.key, this.currentLocation}); @override State createState() => _DestinationScreenState(); } class _DestinationScreenState extends State { final _destController = TextEditingController(); final _destFocusNode = FocusNode(); final _stopControllers = []; final _stopFocusNodes = []; int _editingStopIndex = -1; String? _pickupAddressName; bool _forSelf = true; @override void initState() { super.initState(); _destFocusNode.requestFocus(); _loadReverseGeocodePickup(); } Future _loadReverseGeocodePickup() async { final loc = widget.currentLocation; if (loc != null) { try { final repo = getIt(); final place = await repo.reverseGeocode(loc.latitude, loc.longitude); if (mounted && place.name.isNotEmpty) { setState(() { _pickupAddressName = place.name; }); } } catch (_) {} } } void _swapLocations() { setState(() { final oldDest = _destController.text; if (_stopControllers.isNotEmpty) { _destController.text = _stopControllers.first.text; _stopControllers.first.text = oldDest; } else if (_pickupAddressName != null && _pickupAddressName!.isNotEmpty) { _destController.text = _pickupAddressName!; _pickupAddressName = oldDest.isNotEmpty ? oldDest : _pickupAddressName; } }); } void _showAiVoiceSearchDialog(BuildContext context) { final voiceController = TextEditingController(); final l10n = AppLocalizations.of(context)!; final cs = Theme.of(context).colorScheme; showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (modalCtx) { return Container( padding: EdgeInsets.only( bottom: MediaQuery.of(modalCtx).viewInsets.bottom + T.s24, top: T.s24, left: T.pagePadding, right: T.pagePadding, ), decoration: BoxDecoration( color: cs.surface, borderRadius: const BorderRadius.vertical(top: Radius.circular(T.r24)), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 64, height: 64, decoration: BoxDecoration( color: cs.primaryContainer, shape: BoxShape.circle, ), child: Icon(Icons.mic_rounded, color: cs.onPrimaryContainer, size: 36), ), const SizedBox(height: T.s16), Text( 'البحث الصوتي الذكي (AI Voice)', style: Theme.of(modalCtx).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: T.s8), Text( 'قل وجهتك بصوتك أو اكتب وصف المكان (مثل: مجمع الأبحاث، الشميساني)', textAlign: TextAlign.center, style: Theme.of(modalCtx).textTheme.bodyMedium?.copyWith( color: cs.onSurfaceVariant, ), ), const SizedBox(height: T.s16), TextField( controller: voiceController, autofocus: true, decoration: InputDecoration( hintText: 'تحدث أو اكتب المكان...', prefixIcon: const Icon(Icons.graphic_eq_rounded), border: OutlineInputBorder( borderRadius: BorderRadius.circular(T.r12), ), ), onSubmitted: (text) { Navigator.pop(modalCtx); if (text.trim().isNotEmpty) { _destController.text = text; final loc = widget.currentLocation; context.read().searchVoiceDirectly( text, lat: loc?.latitude, lng: loc?.longitude, ); } }, ), const SizedBox(height: T.s16), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: () => Navigator.pop(modalCtx), child: Text(l10n.cancel), ), const SizedBox(width: T.s8), ElevatedButton.icon( onPressed: () { final text = voiceController.text; Navigator.pop(modalCtx); if (text.trim().isNotEmpty) { _destController.text = text; final loc = widget.currentLocation; context.read().searchVoiceDirectly( text, lat: loc?.latitude, lng: loc?.longitude, ); } }, icon: const Icon(Icons.search_rounded), label: const Text('بحث بالذكاء الاصطناعي'), ), ], ), ], ), ); }, ); } @override void dispose() { _destController.dispose(); _destFocusNode.dispose(); for (final c in _stopControllers) { c.dispose(); } for (final f in _stopFocusNodes) { f.dispose(); } super.dispose(); } void _addStop() { if (_stopControllers.length >= _maxStops) return; setState(() { _stopControllers.add(TextEditingController()); _stopFocusNodes.add(FocusNode()); _editingStopIndex = _stopControllers.length - 1; }); _stopFocusNodes.last.requestFocus(); } void _removeStop(int index) { setState(() { _stopControllers[index].dispose(); _stopFocusNodes[index].dispose(); _stopControllers.removeAt(index); _stopFocusNodes.removeAt(index); if (_editingStopIndex == index) { _editingStopIndex = -1; } else if (_editingStopIndex > index) { _editingStopIndex--; } }); } void _selectPlace(dynamic result) { context.pop(result); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final theme = Theme.of(context); final cs = theme.colorScheme; final tripz = context.tripzColors; return Scaffold( backgroundColor: Colors.transparent, extendBodyBehindAppBar: true, appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, leading: IconButton( onPressed: () => context.pop(), icon: const Icon(Icons.arrow_forward), color: cs.onSurface, style: IconButton.styleFrom( backgroundColor: cs.surface, shadowColor: Colors.black12, elevation: 2, ), ), title: Text( l10n.selectDestination, style: theme.textTheme.titleMedium, ), centerTitle: true, ), body: Stack( children: [ // Map background Positioned.fill( child: Container( color: cs.surfaceContainerHighest, child: Opacity( opacity: 0.5, child: Image.asset( 'assets/images/destination_selection_img.png', fit: BoxFit.cover, errorBuilder: (_, __, ___) => const SizedBox.shrink(), ), ), ), ), // Map overlay gradient Positioned.fill( child: Container( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Colors.black12, Colors.black38], ), ), ), ), // Draggable sheet Positioned.fill( child: DraggableScrollableSheet( initialChildSize: 0.55, minChildSize: 0.15, maxChildSize: 0.88, snap: true, snapSizes: const [0.15, 0.55, 0.88], builder: (context, scrollController) { return Container( decoration: BoxDecoration( color: cs.surface, borderRadius: const BorderRadius.vertical( top: Radius.circular(T.r24)), boxShadow: const [T.shadowLg], ), child: Column( children: [ // Drag handle Center( child: Padding( padding: const EdgeInsets.symmetric(vertical: T.s12), child: Container( width: 48, height: 4, decoration: BoxDecoration( color: cs.outlineVariant, borderRadius: BorderRadius.circular(2), ), ), ), ), Expanded( child: ListView( controller: scrollController, padding: EdgeInsets.zero, children: [ _buildFieldsSection( context, l10n, theme, cs, tripz), const SizedBox(height: T.s16), _buildSavedPlaces(context, theme, cs), const SizedBox(height: T.s8), _buildAutocompleteResults(context, l10n, theme, cs), _buildPickFromMap(context, l10n, cs), const SizedBox(height: T.s32), ], ), ), ], ), ); }, ), ), ], ), ); } Widget _buildFieldsSection( BuildContext context, AppLocalizations l10n, ThemeData theme, ColorScheme cs, TripzColors tripz, ) { return Padding( padding: const EdgeInsets.symmetric(horizontal: T.pagePadding), child: Column( children: [ // Rider selection chip — "For me / For someone else" Row( children: [ FilterChip( selected: _forSelf, label: const Text('طلب لنفسي'), avatar: const Icon(Icons.person_rounded, size: 16), onSelected: (val) => setState(() => _forSelf = true), ), const SizedBox(width: T.s8), FilterChip( selected: !_forSelf, label: const Text('طلب لشخص آخر'), avatar: const Icon(Icons.people_rounded, size: 16), onSelected: (val) => setState(() => _forSelf = false), ), const Spacer(), IconButton( onPressed: _swapLocations, icon: const Icon(Icons.swap_vert_rounded), tooltip: 'تبديل الأماكن', color: cs.primary, ), ], ), const SizedBox(height: T.s8), // Origin — green dot, reverse geocoded _OriginRow( l10n: l10n, theme: theme, cs: cs, tripz: tripz, addressName: _pickupAddressName, ), // Stops for (var i = 0; i < _stopControllers.length; i++) _StopRow( index: i, controller: _stopControllers[i], focusNode: _stopFocusNodes[i], isEditing: _editingStopIndex == i, l10n: l10n, theme: theme, cs: cs, tripz: tripz, onStopTapped: () { setState(() => _editingStopIndex = i); context.read().onQueryChanged(''); _stopFocusNodes[i].requestFocus(); }, onStopChanged: (v) => context.read().onQueryChanged(v), onStopRemoved: () => _removeStop(i), ), // Dotted connector between last stop / origin and destination _DottedConnector(color: cs.outlineVariant), // Destination — red square, active with AI Voice Search button _DestinationRow( controller: _destController, focusNode: _destFocusNode, isEditing: _editingStopIndex == -1, l10n: l10n, theme: theme, cs: cs, tripz: tripz, onFieldTapped: () { setState(() => _editingStopIndex = -1); context.read().onQueryChanged(''); _destFocusNode.requestFocus(); }, onChanged: (v) => context.read().onQueryChanged(v), onVoiceSearchPressed: () => _showAiVoiceSearchDialog(context), ), const SizedBox(height: T.s8), // Add stop button if (_stopControllers.length < _maxStops) Align( alignment: Alignment.centerRight, child: TextButton.icon( onPressed: _addStop, icon: const Icon(Icons.add, size: 18), label: Text(l10n.addStop), style: TextButton.styleFrom( foregroundColor: cs.primary, padding: const EdgeInsets.symmetric(vertical: T.s8), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ), ), const SizedBox(height: T.s12), Divider(color: cs.outlineVariant.withValues(alpha: 0.3)), const SizedBox(height: T.s12), ], ), ); } Widget _buildSavedPlaces( BuildContext context, ThemeData theme, ColorScheme cs, ) { final l10n = AppLocalizations.of(context)!; return SizedBox( height: 52, child: ListView( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: T.pagePadding), children: [ _SavedPlaceChip( icon: Icons.home_rounded, label: l10n.homePlace, onTap: () {}, ), const SizedBox(width: T.s12), _SavedPlaceChip( icon: Icons.work_rounded, label: l10n.workPlace, onTap: () {}, ), const SizedBox(width: T.s12), _SavedPlaceChip( icon: Icons.star_rounded, label: l10n.favorites, onTap: () {}, ), ], ), ); } Widget _buildAutocompleteResults( BuildContext context, AppLocalizations l10n, ThemeData theme, ColorScheme cs, ) { return BlocBuilder( builder: (context, state) { if (state.searching) { return const Padding( padding: EdgeInsets.symmetric(vertical: T.s24), child: Center(child: CircularProgressIndicator()), ); } if (state.results.isEmpty && state.query.length >= 2) { return Padding( padding: const EdgeInsets.symmetric(vertical: T.s24), child: Center( child: Text(l10n.noResults, style: theme.textTheme.bodyLarge), ), ); } return Column( children: [ for (var i = 0; i < state.results.length; i++) _SearchResultTile( place: state.results[i], onTap: () => _selectPlace(state.results[i]), ), ], ); }, ); } Widget _buildPickFromMap( BuildContext context, AppLocalizations l10n, ColorScheme cs, ) { return Padding( padding: const EdgeInsets.symmetric( horizontal: T.pagePadding, vertical: T.s8, ), child: _PickFromMapTile( onTap: () => _selectPlace({'pickFromMap': true}), ), ); } } // ── Origin row ── class _OriginRow extends StatelessWidget { final AppLocalizations l10n; final ThemeData theme; final ColorScheme cs; final TripzColors tripz; final String? addressName; const _OriginRow({ required this.l10n, required this.theme, required this.cs, required this.tripz, this.addressName, }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: T.s4), child: Row( children: [ SizedBox( width: 48, child: Center( child: Container( width: 12, height: 12, decoration: BoxDecoration( color: tripz.success, shape: BoxShape.circle, border: Border.all( color: tripz.success.withValues(alpha: 0.25), width: 4, ), ), ), ), ), Expanded( child: Container( padding: const EdgeInsets.symmetric( horizontal: T.s12, vertical: T.s12, ), decoration: BoxDecoration( color: cs.surfaceContainerLow, borderRadius: BorderRadius.circular(T.r12), border: Border.all( color: cs.outlineVariant.withValues(alpha: 0.3), ), ), child: Text( (addressName != null && addressName!.isNotEmpty) ? addressName! : l10n.pickupLocation, style: theme.textTheme.bodyLarge?.copyWith( color: (addressName != null && addressName!.isNotEmpty) ? cs.onSurface : cs.onSurfaceVariant, fontWeight: (addressName != null && addressName!.isNotEmpty) ? FontWeight.w600 : FontWeight.normal, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ), ], ), ); } } // ── Stop row ── class _StopRow extends StatelessWidget { final int index; final TextEditingController controller; final FocusNode focusNode; final bool isEditing; final AppLocalizations l10n; final ThemeData theme; final ColorScheme cs; final TripzColors tripz; final VoidCallback onStopTapped; final ValueChanged onStopChanged; final VoidCallback onStopRemoved; const _StopRow({ required this.index, required this.controller, required this.focusNode, required this.isEditing, required this.l10n, required this.theme, required this.cs, required this.tripz, required this.onStopTapped, required this.onStopChanged, required this.onStopRemoved, }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: T.s4), child: Row( children: [ SizedBox( width: 48, child: Center( child: Container( width: 16, height: 16, decoration: BoxDecoration( color: cs.surface, shape: BoxShape.circle, border: Border.all(color: cs.onSurfaceVariant, width: 2), ), child: Center( child: Text( '${index + 1}', style: theme.textTheme.labelSmall?.copyWith( color: cs.onSurfaceVariant, fontSize: 10, height: 1, ), ), ), ), ), ), Expanded( child: GestureDetector( onTap: onStopTapped, child: Container( decoration: BoxDecoration( color: isEditing ? cs.surface : cs.surfaceContainerLow, borderRadius: BorderRadius.circular(T.r12), border: Border.all( color: isEditing ? cs.primary : cs.outlineVariant.withValues(alpha: 0.3), width: isEditing ? 2 : 1, ), boxShadow: isEditing ? const [T.shadowSm] : null, ), child: Row( children: [ Expanded( child: TextField( controller: controller, focusNode: focusNode, readOnly: !isEditing, decoration: InputDecoration( hintText: '${l10n.addStop} ${index + 1}', border: InputBorder.none, contentPadding: const EdgeInsets.symmetric( horizontal: T.s12, vertical: T.s12, ), hintStyle: theme.textTheme.bodyLarge?.copyWith( color: cs.onSurfaceVariant.withValues(alpha: 0.7), ), ), style: theme.textTheme.bodyLarge?.copyWith( color: cs.onSurface, ), onChanged: onStopChanged, ), ), if (isEditing && controller.text.isNotEmpty) IconButton( onPressed: onStopRemoved, icon: const Icon(Icons.close, size: 18), color: cs.onSurfaceVariant, splashRadius: 18, ), ], ), ), ), ), ], ), ); } } // ── Dotted connector line ── class _DottedConnector extends StatelessWidget { final Color color; const _DottedConnector({required this.color}); @override Widget build(BuildContext context) { return SizedBox( width: 48, child: CustomPaint( size: const Size(48, 20), painter: _DottedLinePainter(color: color), ), ); } } class _DottedLinePainter extends CustomPainter { final Color color; _DottedLinePainter({required this.color}); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = color ..strokeWidth = 1.5 ..style = PaintingStyle.stroke; const dashHeight = 3.0; const gapHeight = 3.0; double y = 4; while (y < size.height) { canvas.drawLine( Offset(size.width / 2, y), Offset(size.width / 2, y + dashHeight), paint, ); y += dashHeight + gapHeight; } } @override bool shouldRepaint(covariant _DottedLinePainter old) => old.color != color; } // ── Destination row ── class _DestinationRow extends StatelessWidget { final TextEditingController controller; final FocusNode focusNode; final bool isEditing; final AppLocalizations l10n; final ThemeData theme; final ColorScheme cs; final TripzColors tripz; final VoidCallback onFieldTapped; final ValueChanged onChanged; final VoidCallback? onVoiceSearchPressed; const _DestinationRow({ required this.controller, required this.focusNode, required this.isEditing, required this.l10n, required this.theme, required this.cs, required this.tripz, required this.onFieldTapped, required this.onChanged, this.onVoiceSearchPressed, }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: T.s4), child: Row( children: [ SizedBox( width: 48, child: Center( child: Container( width: 16, height: 16, decoration: BoxDecoration( color: cs.error, borderRadius: BorderRadius.circular(3), ), child: Icon(Icons.stop_rounded, size: 12, color: cs.onError), ), ), ), Expanded( child: GestureDetector( onTap: onFieldTapped, child: Container( decoration: BoxDecoration( color: cs.surface, borderRadius: BorderRadius.circular(T.r12), border: Border.all( color: isEditing ? cs.primary : cs.outlineVariant, width: isEditing ? 2 : 1, ), boxShadow: isEditing ? const [T.shadowSm] : null, ), child: Row( children: [ Expanded( child: TextField( controller: controller, focusNode: focusNode, readOnly: !isEditing, decoration: InputDecoration( hintText: l10n.whereTo, border: InputBorder.none, contentPadding: const EdgeInsets.symmetric( horizontal: T.s16, vertical: T.s12, ), hintStyle: theme.textTheme.bodyLarge?.copyWith( color: cs.onSurfaceVariant.withValues(alpha: 0.7), ), ), style: theme.textTheme.bodyLarge?.copyWith( color: cs.onSurface, ), onChanged: onChanged, ), ), if (onVoiceSearchPressed != null) IconButton( onPressed: onVoiceSearchPressed, icon: Icon(Icons.mic_rounded, color: cs.primary), tooltip: 'البحث الصوتي الذكي (AI Voice)', ), ], ), ), ), ), ], ), ); } } // ── Saved place chip ── class _SavedPlaceChip extends StatelessWidget { final IconData icon; final String label; final VoidCallback onTap; const _SavedPlaceChip({ required this.icon, required this.label, required this.onTap, }); @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final theme = Theme.of(context); return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(T.r12), child: Container( padding: const EdgeInsets.symmetric(horizontal: T.s12, vertical: T.s8), decoration: BoxDecoration( color: cs.surfaceContainerLow, borderRadius: BorderRadius.circular(T.r12), border: Border.all(color: cs.outlineVariant.withValues(alpha: 0.3)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 32, height: 32, decoration: BoxDecoration( color: cs.surface, shape: BoxShape.circle, boxShadow: const [T.shadowSm], ), child: Icon(icon, color: cs.primary, size: 20), ), const SizedBox(width: T.s8), Text( label, style: theme.textTheme.titleSmall?.copyWith( color: cs.onSurface, fontSize: 16, ), ), ], ), ), ); } } // ── Autocomplete search result tile ── class _SearchResultTile extends StatelessWidget { final PlaceModel place; final VoidCallback onTap; const _SearchResultTile({required this.place, required this.onTap}); @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final theme = Theme.of(context); return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(T.r8), child: Padding( padding: const EdgeInsets.symmetric( vertical: T.s12, horizontal: T.pagePadding, ), child: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: cs.surfaceContainerHighest, shape: BoxShape.circle, ), child: Icon( Icons.location_on_outlined, color: cs.onSurfaceVariant, size: 20, ), ), const SizedBox(width: T.s12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( place.name, style: theme.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w500, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), if (place.address != null && place.address!.isNotEmpty) ...[ const SizedBox(height: T.s4), Text( place.address!, style: theme.textTheme.bodySmall?.copyWith( color: cs.onSurfaceVariant, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ], ), ), Icon(Icons.chevron_right_rounded, color: cs.onSurfaceVariant), ], ), ), ); } } // ── Pick from map tile ── class _PickFromMapTile extends StatelessWidget { final VoidCallback onTap; const _PickFromMapTile({required this.onTap}); @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final cs = Theme.of(context).colorScheme; final theme = Theme.of(context); return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(T.r12), child: Container( padding: const EdgeInsets.symmetric( horizontal: T.s12, vertical: T.s12, ), decoration: BoxDecoration( color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(T.r12), ), child: Row( children: [ Icon(Icons.map_rounded, color: cs.primary, size: T.iconMd), const SizedBox(width: T.s12), Text( l10n.setLocationOnMap, style: theme.textTheme.bodyLarge?.copyWith( color: cs.onSurface, fontWeight: FontWeight.w500, ), ), ], ), ), ); } }