import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/region/geo.dart'; import '../../core/region/region_config.dart'; import '../../core/region/region_repository.dart'; import '../../l10n/app_localizations.dart'; import 'comune.dart'; import 'comuni_repository.dart'; import 'coordinate_input.dart'; /// What the user picked out of search. class SearchResult { const SearchResult({required this.point, required this.label}); final GeoPoint point; /// How to name it if it gets saved as a place. final String label; } /// One field that takes either a municipality name or coordinates. /// /// Two separate searches would make the user decide up front which kind of /// thing they are looking for, which they should not have to: a string either /// parses as coordinates or it does not, and the answer is obvious from the /// text itself. class SearchScreen extends ConsumerStatefulWidget { const SearchScreen({super.key}); @override ConsumerState createState() => _SearchScreenState(); } class _SearchScreenState extends ConsumerState { final TextEditingController _controller = TextEditingController(); String _query = ''; @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final index = ref.watch(comuneIndexProvider); final region = ref.watch(regionConfigProvider).value; final coordinates = parseCoordinates(_query); final matches = switch (index) { AsyncData(:final value) => value.search(_query), _ => const [], }; return Scaffold( appBar: AppBar( title: TextField( controller: _controller, autofocus: true, textInputAction: TextInputAction.search, decoration: InputDecoration( hintText: l10n.searchHint, border: InputBorder.none, ), onChanged: (value) => setState(() => _query = value), ), actions: [ if (_query.isNotEmpty) IconButton( icon: const Icon(Icons.clear), onPressed: () { _controller.clear(); setState(() => _query = ''); }, ), ], ), body: switch (index) { AsyncError(:final error) => Center( child: Padding( padding: const EdgeInsets.all(24), child: Text('${l10n.dataUnavailable}\n$error'), ), ), AsyncData() => _results( context, coordinates: coordinates, matches: matches, region: region, ), _ => Center(child: Text(l10n.loading)), }, ); } Widget _results( BuildContext context, { required GeoPoint? coordinates, required List matches, required RegionConfig? region, }) { final l10n = AppLocalizations.of(context); final theme = Theme.of(context); if (_query.trim().isEmpty) { return Padding( padding: const EdgeInsets.all(24), child: Text(l10n.searchHelp, style: theme.textTheme.bodyMedium), ); } final outsideRegion = coordinates != null && region != null && !region.bounds.contains(coordinates); return ListView( children: [ if (coordinates != null) ListTile( leading: const Icon(Icons.my_location), title: Text(l10n.searchCoordinatesResult), subtitle: Text( '${coordinates.latitude.toStringAsFixed(4)}, ' '${coordinates.longitude.toStringAsFixed(4)}' // Saying so beats letting them travel to an empty map. '${outsideRegion ? '\n${l10n.searchOutsideRegion}' : ''}', style: outsideRegion ? TextStyle(color: theme.colorScheme.error) : null, ), onTap: () => Navigator.of(context).pop( SearchResult( point: coordinates, label: '${coordinates.latitude.toStringAsFixed(4)}, ' '${coordinates.longitude.toStringAsFixed(4)}', ), ), ), if (coordinates != null && matches.isNotEmpty) const Divider(), if (matches.isNotEmpty) Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), child: Text( l10n.searchComuniCount(matches.length), style: theme.textTheme.labelMedium?.copyWith( color: theme.colorScheme.primary, ), ), ), for (final comune in matches) ListTile( leading: const Icon(Icons.location_city), title: Text(comune.name), subtitle: Text(comune.province), onTap: () => Navigator.of(context) .pop(SearchResult(point: comune.point, label: comune.name)), ), if (matches.isEmpty && coordinates == null) Padding( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(l10n.searchNoResults, style: theme.textTheme.titleSmall), const SizedBox(height: 8), Text(l10n.searchHelp, style: theme.textTheme.bodySmall), ], ), ), ], ); } }