- 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>
87 lines
2.5 KiB
Dart
87 lines
2.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
/// Swipe-adjustierbarer Wertechip in der Wegpunktliste (HTML-Demonstrator:
|
|
/// .value-chip / chipFieldConfig). Vertikal ziehen aendert den Wert in
|
|
/// festen Schritten von 10 pro 14px - nach oben erhoeht.
|
|
class WaypointChip extends StatefulWidget {
|
|
const WaypointChip({
|
|
super.key,
|
|
required this.value,
|
|
required this.unit,
|
|
required this.min,
|
|
required this.max,
|
|
required this.onChanged,
|
|
});
|
|
|
|
final double value;
|
|
final String unit;
|
|
final double min;
|
|
final double max;
|
|
final ValueChanged<double> onChanged;
|
|
|
|
static const _stepSize = 10.0;
|
|
static const _pxPerStep = 14.0;
|
|
|
|
@override
|
|
State<WaypointChip> createState() => _WaypointChipState();
|
|
}
|
|
|
|
class _WaypointChipState extends State<WaypointChip> {
|
|
double? _dragStartY;
|
|
double _dragStartValue = 0;
|
|
bool _flash = false;
|
|
|
|
void _onDragStart(DragStartDetails details) {
|
|
_dragStartY = details.globalPosition.dy;
|
|
_dragStartValue = widget.value;
|
|
}
|
|
|
|
void _onDragUpdate(DragUpdateDetails details) {
|
|
final startY = _dragStartY;
|
|
if (startY == null) return;
|
|
final deltaY = startY - details.globalPosition.dy;
|
|
final steps = (deltaY / WaypointChip._pxPerStep).round();
|
|
var val = _dragStartValue + steps * WaypointChip._stepSize;
|
|
val = val.clamp(widget.min, widget.max);
|
|
if (val != widget.value) {
|
|
widget.onChanged(val);
|
|
setState(() => _flash = true);
|
|
Future.delayed(const Duration(milliseconds: 120), () {
|
|
if (mounted) setState(() => _flash = false);
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GestureDetector(
|
|
onVerticalDragStart: _onDragStart,
|
|
onVerticalDragUpdate: _onDragUpdate,
|
|
onVerticalDragEnd: (_) => _dragStartY = null,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: _flash
|
|
? const Color(0xFF4D9DFF).withValues(alpha: 0.4)
|
|
: Colors.black.withValues(alpha: 0.85),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: _flash
|
|
? const Color(0xFF8FD0FF)
|
|
: const Color(0xFF4D9DFF).withValues(alpha: 0.7),
|
|
width: 1.5,
|
|
),
|
|
),
|
|
child: Text(
|
|
'${widget.value.round()}${widget.unit}',
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 13,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|