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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
3091476771
commit
dc8e6f9d6e
@@ -72,13 +72,38 @@ class MissionMap extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
CircleLayer(circles: waypointMarkers),
|
||||
SimpleAttributionWidget(
|
||||
alignment: Alignment.bottomLeft,
|
||||
source: const Text('OpenStreetMap contributors'),
|
||||
onTap: () =>
|
||||
launchUrl(Uri.parse('https://www.openstreetmap.org/copyright')),
|
||||
),
|
||||
const _CompactAttribution(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Schlanke Pflicht-Attribution fuer die OSM-Kacheln (Tile Usage Policy) -
|
||||
/// bewusst ohne den "flutter_map |"-Praefix von SimpleAttributionWidget und
|
||||
/// dezenter gestaltet, damit sie nicht wie ein zweiter Balken ueber der
|
||||
/// Bottom-Bar wirkt. Bleibt sichtbar und antippbar, da die Policy sichtbare
|
||||
/// Namensnennung verlangt.
|
||||
class _CompactAttribution extends StatelessWidget {
|
||||
const _CompactAttribution();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: GestureDetector(
|
||||
onTap: () =>
|
||||
launchUrl(Uri.parse('https://www.openstreetmap.org/copyright')),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
|
||||
color: Colors.white.withValues(alpha: 0.5),
|
||||
child: const Text(
|
||||
'© OpenStreetMap contributors',
|
||||
style: TextStyle(fontSize: 9, color: Colors.black87),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user