Files
dmc/app/lib/ui/widgets/top_nav_bar.dart
T
Constantin LeueandClaude Sonnet 5 dc8e6f9d6e Add location search, home/fit map buttons, and slim OSM attribution
- 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>
2026-07-28 09:06:16 +02:00

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),
),
);
}
}