Add waypoint list panel and bottom stats bar
- BottomStatsBar: persistent "Details: N WP · dist m · duration" pill
(HTML prototype's #bottomBar/#statsRow), tap opens the full waypoint
list. Mission/drone name and the Send button are intentionally not part
of this - mission library and upload aren't wired up yet.
- A warn banner ("Route exceeds drone capabilities at highlighted point")
appears above it whenever buildRouteGeometry finds a bad vertex or leg -
a direct, low-effort payoff from the fillet geometry work.
- WaypointListPanel: full-screen overlay listing every waypoint
(altitude/speed/execute/catch radius), matching the prototype's List
tab. Only one row editable at a time; editing swaps plain text for
draggable WaypointChip values and an execute dropdown. Row tap (outside
edit mode) centers the map on that waypoint and closes the panel; delete
removes it and adjusts the reticle's target index the same way the halo
menu's remove already does.
- WaypointChip: vertical-drag value chip (HTML prototype's .value-chip),
fixed step of 10 per 14px matching chipFieldConfig - simpler than the
original's dual vertical/horizontal axis-lock gesture, which felt like
more fidelity than the interaction warrants here.
- Provider gained setCatchRadius and setAction (direct assignment, as
opposed to toggleAction's toggle-to-none semantics used by the halo
menu) to support the list's editable fields.
- Altitude/Speed full-chart tabs from the prototype (drag-to-adjust
altitude/speed profile charts) are a separate, larger feature and not
part of this step.
Centering on a waypoint from the list calls MapController.move(), which -
like the reticle's auto-snap - doesn't emit MoveEnd, so snap detection is
invoked manually afterward to match the prototype's resulting behavior.
Added 4 widget tests (open empty list, row-tap centers and closes, delete
removes and updates the empty state, chip drag updates altitude) - one
required a pump(duration) after the drag to flush the chip's 120ms flash
Timer, which was otherwise still pending at test teardown. All 15 tests
and flutter analyze pass. Manually verified on the Pixel_10a emulator:
opened the list with two waypoints, edited a row's altitude via chip drag
(confirmed value change, accounting for device-pixel-ratio scaling vs the
widget-test logical-pixel case), and confirmed row-tap re-centers/closes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b93bbab23b
commit
d3c5a68636
@@ -0,0 +1,78 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Statuszeile am unteren Bildschirmrand (HTML-Demonstrator: #bottomBar/
|
||||
/// #statsRow). Antippen oeffnet die Wegpunktliste. Mission-/Drohnen-Name
|
||||
/// und der Send-Button sind bewusst nicht Teil dieses Schritts - die
|
||||
/// dafuer noetige Missionsverwaltung bzw. Upload-Anbindung existiert noch
|
||||
/// nicht.
|
||||
class BottomStatsBar extends StatelessWidget {
|
||||
const BottomStatsBar({
|
||||
super.key,
|
||||
required this.waypointCount,
|
||||
required this.totalDistanceM,
|
||||
required this.totalDurationSec,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final int waypointCount;
|
||||
final double totalDistanceM;
|
||||
final double totalDurationSec;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
top: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Center(
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 9),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Details:',
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const Icon(Icons.flag, size: 14, color: Colors.white),
|
||||
const SizedBox(width: 4),
|
||||
Text('$waypointCount WP',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14)),
|
||||
const SizedBox(width: 14),
|
||||
const Icon(Icons.straighten, size: 14, color: Colors.white),
|
||||
const SizedBox(width: 4),
|
||||
Text('${totalDistanceM.round()} m',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14)),
|
||||
const SizedBox(width: 14),
|
||||
const Icon(Icons.access_time, size: 14, color: Colors.white),
|
||||
const SizedBox(width: 4),
|
||||
Text(_fmtDuration(totalDurationSec),
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _fmtDuration(double seconds) {
|
||||
final total = seconds.round();
|
||||
final minutes = total ~/ 60;
|
||||
final secs = total % 60;
|
||||
return '$minutes:${secs.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user