import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:latlong2/latlong.dart'; import '../providers/current_mission_provider.dart'; import '../providers/geocoding_provider.dart'; import '../providers/map_controller_provider.dart'; import '../providers/mission_meta_provider.dart'; import '../providers/wind_provider.dart'; import 'nav_icon_button.dart'; /// Ortssuche (Nominatim) + Home-/Fit-Buttons, eingebettet als Mittelteil /// der Plan/Fly-Kopfleiste (HTML-Demonstrator: #searchWrap/#homeBtn/ /// #fitBtn sitzen alle in derselben Leiste wie die Modus-Buttons). /// /// Vollstaendig selbststaendig (kein Callback-Durchreichen von aussen): /// greift direkt auf den geteilten mapControllerProvider und /// currentMissionProvider zu, da diese Aktionen konzeptionell zur /// Kartensteuerung gehoeren, nicht zum Elternwidget. class MapSearchControls extends ConsumerStatefulWidget { const MapSearchControls({super.key}); @override ConsumerState createState() => _MapSearchControlsState(); } class _MapSearchControlsState extends ConsumerState { final _controller = TextEditingController(); @override void dispose() { _controller.dispose(); super.dispose(); } Future _submitSearch() async { final query = _controller.text.trim(); FocusScope.of(context).unfocus(); if (query.isEmpty) return; final messenger = ScaffoldMessenger.of(context); try { final result = await ref.read(nominatimServiceProvider).search(query); if (result == null) { messenger.showSnackBar(const SnackBar(content: Text('Location not found'))); return; } ref.read(mapControllerProvider).move(LatLng(result.lat, result.lon), 15); // Eine Ortssuche beginnt immer eine neue, nach dem Ort benannte // Mission statt nur die Karte zu verschieben (HTML-Demonstrator: // setMissionFromPlace(), aufgerufen aus doSearch()) - vorherige // Aenderungen werden dabei zuerst autosaved, gehen also nicht verloren. await ref .read(currentMissionMetaProvider.notifier) .startNewFromPlace(result.label); await ref.read(headerWindProvider.notifier).refresh(); } catch (_) { messenger.showSnackBar(const SnackBar(content: Text('Search failed'))); } } /// Zentriert auf den ersten Wegpunkt (HTML-Demonstrator: homeBtn). void _onHomePressed() { final waypoints = ref.read(currentMissionProvider); if (waypoints.isEmpty) { ScaffoldMessenger.of(context) .showSnackBar(const SnackBar(content: Text('No waypoints yet'))); return; } final home = waypoints.first; ref.read(mapControllerProvider).move(LatLng(home.lat, home.lon), 16); } /// Zoomt auf die gesamte Mission (HTML-Demonstrator: fitToWaypoints()). void _onFitPressed() { final waypoints = ref.read(currentMissionProvider); if (waypoints.isEmpty) { ScaffoldMessenger.of(context) .showSnackBar(const SnackBar(content: Text('No waypoints to fit'))); return; } fitMapToWaypoints(ref, waypoints); } @override Widget build(BuildContext context) { return Row( children: [ Expanded( child: Container( // 38 statt vorher 34, deckungsgleich mit HeaderWindPill/ // FlightModePill/NavIconButton (Nutzerwunsch: einheitliche // Kopfleisten-Hoehe wie im HTML-Demonstrator vermessen). height: 38, padding: const EdgeInsets.only(left: 12, right: 4), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.55), borderRadius: BorderRadius.circular(19), ), child: Row( children: [ Expanded( child: TextField( controller: _controller, onSubmitted: (_) => _submitSearch(), textInputAction: TextInputAction.search, style: const TextStyle(color: Colors.white, fontSize: 13), decoration: InputDecoration( isDense: true, border: InputBorder.none, hintText: 'Search location', hintStyle: TextStyle( color: Colors.white.withValues(alpha: 0.55), ), ), ), ), IconButton( onPressed: _submitSearch, icon: const Icon(Icons.search, color: Colors.white, size: 16), padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 26, minHeight: 26), ), ], ), ), ), const SizedBox(width: 8), NavIconButton(icon: Icons.home, onTap: _onHomePressed), const SizedBox(width: 8), NavIconButton(icon: Icons.crop_free, onTap: _onFitPressed), ], ); } }