Files
dmc/app/lib/ui/widgets/map_search_controls.dart
Constantin Leue 0d3dba2482 Unify header pill/button height to 38px, matching the HTML prototype
Measured the prototype screenshot pixel-for-pixel: content pills take
up ~79% of the header's total height, matching its own CSS
(height:38px, topBarWrap padding:5px 10px -> 48px total). The Flutter
port had drifted to 34px for HeaderWindPill, MapSearchControls, and
NavIconButton, while FlightModePill was left at the original 38px -
so Fly mode looked inconsistent against itself, and both modes wasted
more header height as margin than the prototype does. Bringing all
four back to a shared 38px fixes both.
2026-08-07 22:06:23 +02:00

137 lines
5.0 KiB
Dart

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<MapSearchControls> createState() => _MapSearchControlsState();
}
class _MapSearchControlsState extends ConsumerState<MapSearchControls> {
final _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Future<void> _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),
],
);
}
}