import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../api/connect/rmv/rmv_models.dart'; import '../../../../api/errors/error_mapper.dart'; import '../../../../state/app/modules/rmv/repository/rmv_repository.dart'; import '../../../../state/app/modules/settings/bloc/settings_cubit.dart'; import '../../../../utils/debouncer.dart'; import '../../../../widget/app_progress_indicator.dart'; import '../../../../widget/centered_leading.dart'; import '../favorites_controller.dart'; /// Modal search sheet for picking a [StopLocation]. Shows favorites + recents /// when the search field is empty, switches to live search results as soon as /// the user types. Returns the chosen stop via [Navigator.pop], or `null` if /// the user dismisses the sheet. Future showStationPickerSheet( BuildContext context, { String title = 'Station auswählen', }) => showModalBottomSheet( context: context, isScrollControlled: true, showDragHandle: true, useSafeArea: true, builder: (sheetCtx) => _StationPickerSheet(title: title), ); class _StationPickerSheet extends StatefulWidget { final String title; const _StationPickerSheet({required this.title}); @override State<_StationPickerSheet> createState() => _StationPickerSheetState(); } class _StationPickerSheetState extends State<_StationPickerSheet> { static const _debounceTag = 'rmv_station_search'; final TextEditingController _controller = TextEditingController(); final RmvRepository _repo = RmvRepository(); List? _results; bool _loading = false; Object? _error; String _query = ''; @override void dispose() { Debouncer.cancel(_debounceTag); _controller.dispose(); super.dispose(); } void _onChanged(String value) { final trimmed = value.trim(); setState(() => _query = trimmed); if (trimmed.length < 2) { setState(() { _results = null; _error = null; _loading = false; }); Debouncer.cancel(_debounceTag); return; } Debouncer.debounce(_debounceTag, const Duration(milliseconds: 300), () { _runSearch(trimmed); }); } Future _runSearch(String q) async { setState(() { _loading = true; _error = null; }); try { final results = await _repo.searchStops(q, max: 25); if (!mounted || _query != q) return; setState(() { _results = results; _loading = false; }); } catch (e) { if (!mounted || _query != q) return; setState(() { _error = e; _loading = false; }); } } @override Widget build(BuildContext context) { final settings = context.read(); final favorites = settings.val().rmvSettings.favoriteStations; final recents = settings.val().rmvSettings.recentStations; final viewInsets = MediaQuery.of(context).viewInsets; return Padding( padding: EdgeInsets.only(bottom: viewInsets.bottom), child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 4, 16, 8), child: Text( widget.title, style: Theme.of(context).textTheme.titleLarge, ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: TextField( controller: _controller, autofocus: true, onChanged: _onChanged, decoration: InputDecoration( hintText: 'Station suchen…', prefixIcon: const Icon(Icons.search), suffixIcon: _query.isEmpty ? null : IconButton( icon: const Icon(Icons.clear), onPressed: () { _controller.clear(); _onChanged(''); }, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), ), ), ), const SizedBox(height: 8), ConstrainedBox( constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.6, minHeight: 200, ), child: _body(favorites, recents), ), ], ), ); } Widget _body(List favorites, List recents) { if (_loading) { return const Center(child: AppProgressIndicator.medium()); } final err = _error; if (err != null) { return Padding( padding: const EdgeInsets.all(16), child: Text( errorToUserMessage(err), style: TextStyle(color: Theme.of(context).colorScheme.error), ), ); } final results = _results; if (results != null) { if (results.isEmpty) { return Center( child: Padding( padding: const EdgeInsets.all(16), child: Text( 'Keine Station für "$_query" gefunden.', style: Theme.of(context).textTheme.bodyMedium, ), ), ); } return ListView( children: results .map((s) => _tile(s, leadingIcon: Icons.directions_transit)) .toList(), ); } // Empty query → favorites + recents. final widgets = []; if (favorites.isNotEmpty) { widgets.add(_sectionHeader('Favoriten')); widgets.addAll( favorites.map((s) => _tile(s, leadingIcon: Icons.star)), ); } if (recents.isNotEmpty) { widgets.add(_sectionHeader('Zuletzt verwendet')); widgets.addAll( recents.map((s) => _tile(s, leadingIcon: Icons.history)), ); } if (widgets.isEmpty) { return Center( child: Padding( padding: const EdgeInsets.all(16), child: Text( 'Tippe oben einen Stationsnamen ein, um die RMV-Datenbank zu durchsuchen.', style: Theme.of(context).textTheme.bodyMedium, textAlign: TextAlign.center, ), ), ); } return ListView(children: widgets); } Widget _sectionHeader(String text) => Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), child: Text( text, style: Theme.of(context).textTheme.labelLarge?.copyWith( color: Theme.of(context).colorScheme.secondary, ), ), ); Widget _tile(StopLocation stop, {required IconData leadingIcon}) => ListTile( leading: CenteredLeading(Icon(leadingIcon)), title: Text(stop.name), subtitle: stop.description == null ? null : Text(stop.description!), onTap: () { RmvFavoritesController(context.read()).addRecent(stop); Navigator.of(context).pop(stop); }, ); }