- TopNavBar: Nominatim geocoding search field plus Home (center on first waypoint) and Fit (zoom to mission bounds) buttons (HTML prototype: #searchWrap/#homeBtn/#fitBtn). Both show a SnackBar instead of doing nothing when there are no waypoints yet. - Renders as its own bar just below TopModeBar rather than fused into the same pill: TopModeBar lives in AppShell, a layer above PlanScreen, which owns the MapController these actions actually need. Fusing them would need lifting the MapController to shared state - reasonable follow-up if pixel-fidelity with the prototype's single bar matters later, but not necessary for the functionality itself. - Replaced SimpleAttributionWidget with a compact custom attribution: OSM's tile usage policy requires visible attribution to stay, so instead of removing it (as first asked) I shrank it and dropped the "flutter_map |" prefix per the user's follow-up choice, so it reads cleanly against the now-opaque bottom bar instead of looking like a second banner. Added 5 tests (nav bar renders, home/fit SnackBars with no waypoints, empty search is a no-op). All 26 tests and flutter analyze pass. Verified on the Pixel_10a emulator: searched "Berlin" and confirmed the map flew there, dropped a waypoint, panned far away, and confirmed Home re-centers and snaps onto it exactly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
111 lines
3.3 KiB
Dart
111 lines
3.3 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
/// Suchleiste + Navigations-Buttons (HTML-Demonstrator: #searchWrap/
|
|
/// #homeBtn/#fitBtn). Rendert unterhalb der Plan/Fly-Kopfleiste statt in
|
|
/// dieselbe Pille eingeschmolzen - TopModeBar liegt im AppShell, eine
|
|
/// Ebene ueber PlanScreen, das den MapController besitzt, den Suche/
|
|
/// Home/Fit tatsaechlich benoetigen. Ein gemeinsamer Riverpod-State fuer
|
|
/// den MapController koennte das spaeter fusionieren.
|
|
class TopNavBar extends StatefulWidget {
|
|
const TopNavBar({
|
|
super.key,
|
|
required this.onSearch,
|
|
required this.onHome,
|
|
required this.onFit,
|
|
});
|
|
|
|
final ValueChanged<String> onSearch;
|
|
final VoidCallback onHome;
|
|
final VoidCallback onFit;
|
|
|
|
@override
|
|
State<TopNavBar> createState() => _TopNavBarState();
|
|
}
|
|
|
|
class _TopNavBarState extends State<TopNavBar> {
|
|
final _controller = TextEditingController();
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _submit() {
|
|
widget.onSearch(_controller.text);
|
|
FocusScope.of(context).unfocus();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 260,
|
|
height: 38,
|
|
padding: const EdgeInsets.only(left: 12, right: 4),
|
|
decoration: BoxDecoration(
|
|
color: Colors.black.withValues(alpha: 0.68),
|
|
borderRadius: BorderRadius.circular(19),
|
|
border: Border.all(color: Colors.white.withValues(alpha: 0.4)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _controller,
|
|
onSubmitted: (_) => _submit(),
|
|
textInputAction: TextInputAction.search,
|
|
style: const TextStyle(color: Colors.white, fontSize: 14),
|
|
decoration: InputDecoration(
|
|
isDense: true,
|
|
border: InputBorder.none,
|
|
hintText: 'Search location',
|
|
hintStyle:
|
|
TextStyle(color: Colors.white.withValues(alpha: 0.55)),
|
|
),
|
|
),
|
|
),
|
|
IconButton(
|
|
onPressed: _submit,
|
|
icon: const Icon(Icons.search, color: Colors.white, size: 18),
|
|
padding: EdgeInsets.zero,
|
|
constraints: const BoxConstraints(minWidth: 30, minHeight: 30),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
_NavIconButton(icon: Icons.home, onTap: widget.onHome),
|
|
const SizedBox(width: 8),
|
|
_NavIconButton(icon: Icons.crop_free, onTap: widget.onFit),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _NavIconButton extends StatelessWidget {
|
|
const _NavIconButton({required this.icon, required this.onTap});
|
|
|
|
final IconData icon;
|
|
final VoidCallback onTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
width: 38,
|
|
height: 38,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: Colors.black.withValues(alpha: 0.65),
|
|
border: Border.all(color: Colors.white.withValues(alpha: 0.4)),
|
|
),
|
|
child: Icon(icon, color: Colors.white, size: 18),
|
|
),
|
|
);
|
|
}
|
|
}
|