Auto-fetch wind, drop manual fetch/validate buttons, fix it in fly mode
Redesigns the waypoint list panel to match the drone-status/missions panels: no title bar, close button floats over the tab row, tabs get more vertical padding. Removes the "Fetch wind" and "Validate" header buttons along with the now-fully-orphaned WindValidatePanel screen and its WindService support code (fetchValidation, WindValidationResult and friends) - nothing else referenced them. Wind is now fetched automatically instead: HeaderWindNotifier gains ensurePerWaypointWind(), called when the waypoint list panel opens and when the header pill's per-waypoint toggle switches on. A lat/lon signature of the waypoint list tracks whether the fetched data is still current, so a ref.listen(currentMissionProvider) in the same notifier refetches only when the list itself changes (position added/ removed/moved) - not on every altitude/speed edit. Waypoints that already carry wind data (e.g. loaded from a saved mission) short- circuit the check instead of re-fetching pointlessly. Also wires per-waypoint wind markers into the fly-mode map, which never had them (plan_screen.dart built windMarkers; fly_screen.dart just never did) - the reported "doesn't work in fly mode" bug. windMarkerWidth() moved from a plan_screen.dart-private method to wind_marker_pill.dart so both screens can share it. Test containers in widget_test.dart now override windServiceProvider with a MockClient so the new auto-fetch trigger can't reach the real network during tests.
This commit is contained in:
@@ -15,7 +15,6 @@ import '../providers/wind_provider.dart';
|
||||
import 'full_value_chart.dart';
|
||||
import 'waypoint_chip.dart';
|
||||
import 'wind_arrow_icon.dart';
|
||||
import 'wind_validate_panel.dart';
|
||||
|
||||
/// Welcher Tab beim Oeffnen des Panels aktiv ist (Doku 3.6/3.10) - public,
|
||||
/// damit Aufrufer wie die Warnungsliste direkt auf Altitude/Speed
|
||||
@@ -86,6 +85,12 @@ class _WaypointListPanelState extends ConsumerState<WaypointListPanel> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tab = widget.initialTab;
|
||||
// Wind pro Wegpunkt automatisch abrufen, sobald das Panel geoeffnet
|
||||
// wird (Doku: "wind automatisch abrufen, wenn das menue geoeffnet
|
||||
// wird") - ersetzt den vorherigen manuellen "Fetch wind"-Knopf.
|
||||
// ensurePerWaypointWind() selbst entscheidet, ob dafuer ueberhaupt ein
|
||||
// Request noetig ist (siehe dortige Doku).
|
||||
ref.read(headerWindProvider.notifier).ensurePerWaypointWind();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_scrollToActive();
|
||||
// Startet das Panel direkt auf dem Altitude-Tab (z.B. aus der
|
||||
@@ -120,39 +125,6 @@ class _WaypointListPanelState extends ConsumerState<WaypointListPanel> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Holt Wind fuer alle Wegpunkte gebuendelt (Doku 3.8, HTML-Demonstrator:
|
||||
/// fetchWindForWaypoints()) und schreibt das Ergebnis komplett zurueck.
|
||||
Future<void> _onFetchWind() async {
|
||||
final waypoints = ref.read(currentMissionProvider);
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
if (waypoints.isEmpty) {
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(content: Text('No waypoints to fetch wind for')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
messenger.showSnackBar(const SnackBar(content: Text('Fetching wind…')));
|
||||
final updated =
|
||||
await ref.read(windServiceProvider).fetchWindForWaypoints(waypoints);
|
||||
ref.read(currentMissionProvider.notifier).replaceAll(updated);
|
||||
messenger.showSnackBar(const SnackBar(content: Text('Wind data updated')));
|
||||
}
|
||||
|
||||
/// Oeffnet das Wind-Validierungspanel fuer den aktuellen Referenzort
|
||||
/// (erster Wegpunkt, sonst Kartenzentrum - Doku 3.8).
|
||||
void _onValidateWind() {
|
||||
final location = windReferenceLocation(ref);
|
||||
if (location == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('No location available yet')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (_) => WindValidatePanel(lat: location.$1, lon: location.$2),
|
||||
));
|
||||
}
|
||||
|
||||
/// Zentriert die Liste auf den aktuell aktiven Wegpunkt (Doku 3.6: das
|
||||
/// Panel oeffnet sich beim Editieren eines Wegpunkts ueber das
|
||||
/// Halo-Menue - ohne Auto-Scroll waere er bei laengeren Missionen nicht
|
||||
@@ -186,83 +158,71 @@ class _WaypointListPanelState extends ConsumerState<WaypointListPanel> {
|
||||
return Material(
|
||||
color: const Color(0xEB080808),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
// Kein eigener Titel-Header mehr (Doku: "nach dem neuen
|
||||
// Designschema anpassen und ueberschrift entfernen", analog Drone-
|
||||
// Status- und Missions-/Drohnen-Panel) - der Schliessen-Knopf
|
||||
// schwebt stattdessen ueber der Tab-Zeile, die dafuer rechts Platz
|
||||
// frei laesst (siehe unten).
|
||||
child: Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 8, 12),
|
||||
child: Row(
|
||||
children: [
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Waypoints',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
_HeaderActionButton(
|
||||
icon: Icons.air,
|
||||
label: 'Fetch wind',
|
||||
tint: const Color(0xFF4D9DFF),
|
||||
onTap: _onFetchWind,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_HeaderActionButton(
|
||||
icon: Icons.rule,
|
||||
label: 'Validate',
|
||||
tint: const Color(0xFFC8A04D),
|
||||
onTap: _onValidateWind,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: widget.onClose,
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
Column(
|
||||
children: [
|
||||
_tabButton('List', PanelTab.list),
|
||||
_tabButton('Altitude', PanelTab.altitude),
|
||||
_tabButton('Speed', PanelTab.speed),
|
||||
Padding(
|
||||
// Rechts Platz fuer den schwebenden Schliessen-Knopf,
|
||||
// damit "Speed" nicht darunter verschwindet.
|
||||
padding: const EdgeInsets.only(right: 48),
|
||||
child: Row(
|
||||
children: [
|
||||
_tabButton('List', PanelTab.list),
|
||||
_tabButton('Altitude', PanelTab.altitude),
|
||||
_tabButton('Speed', PanelTab.speed),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(color: Colors.white24, height: 1),
|
||||
Expanded(
|
||||
child: switch (_tab) {
|
||||
PanelTab.list => _listTab(waypoints, routeGeometry, droneProfile),
|
||||
PanelTab.altitude => _chartTab(
|
||||
waypoints,
|
||||
routeGeometry,
|
||||
values: [for (final w in waypoints) w.altitudeM],
|
||||
min: DefaultDroneProfile.altMin,
|
||||
max: droneProfile.maxAltitudeM,
|
||||
step: DefaultDroneProfile.altStep,
|
||||
pxPerUnit: 5,
|
||||
unit: ' m',
|
||||
onChanged: widget.onAltitudeChanged,
|
||||
terrainPoints: terrainState.profile?.points,
|
||||
terrainDangerRanges: terrainState.profile != null
|
||||
? terrainDangerRanges(waypoints, terrainState.profile!)
|
||||
: null,
|
||||
terrainLoading: terrainState.loading,
|
||||
),
|
||||
PanelTab.speed => _chartTab(
|
||||
waypoints,
|
||||
routeGeometry,
|
||||
values: [for (final w in waypoints) w.speedMs],
|
||||
min: droneProfile.minSpeed,
|
||||
max: droneProfile.maxSpeed,
|
||||
step: DefaultDroneProfile.speedStep,
|
||||
pxPerUnit: 22,
|
||||
unit: ' m/s',
|
||||
onChanged: widget.onSpeedChanged,
|
||||
legWind: computeLegWind(waypoints),
|
||||
groundSpeeds: computeGroundSpeeds(waypoints),
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(color: Colors.white24, height: 1),
|
||||
Expanded(
|
||||
child: switch (_tab) {
|
||||
PanelTab.list => _listTab(waypoints, routeGeometry, droneProfile),
|
||||
PanelTab.altitude => _chartTab(
|
||||
waypoints,
|
||||
routeGeometry,
|
||||
values: [for (final w in waypoints) w.altitudeM],
|
||||
min: DefaultDroneProfile.altMin,
|
||||
max: droneProfile.maxAltitudeM,
|
||||
step: DefaultDroneProfile.altStep,
|
||||
pxPerUnit: 5,
|
||||
unit: ' m',
|
||||
onChanged: widget.onAltitudeChanged,
|
||||
terrainPoints: terrainState.profile?.points,
|
||||
terrainDangerRanges: terrainState.profile != null
|
||||
? terrainDangerRanges(waypoints, terrainState.profile!)
|
||||
: null,
|
||||
terrainLoading: terrainState.loading,
|
||||
),
|
||||
PanelTab.speed => _chartTab(
|
||||
waypoints,
|
||||
routeGeometry,
|
||||
values: [for (final w in waypoints) w.speedMs],
|
||||
min: droneProfile.minSpeed,
|
||||
max: droneProfile.maxSpeed,
|
||||
step: DefaultDroneProfile.speedStep,
|
||||
pxPerUnit: 22,
|
||||
unit: ' m/s',
|
||||
onChanged: widget.onSpeedChanged,
|
||||
legWind: computeLegWind(waypoints),
|
||||
groundSpeeds: computeGroundSpeeds(waypoints),
|
||||
),
|
||||
},
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: IconButton(
|
||||
onPressed: widget.onClose,
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -286,7 +246,9 @@ class _WaypointListPanelState extends ConsumerState<WaypointListPanel> {
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
// 18 statt vorher 10 (analog Drone-Status-/Missions-Panel: Tabs
|
||||
// wirkten seit dem Wegfall der Titelzeile zu gequetscht).
|
||||
padding: const EdgeInsets.symmetric(vertical: 18),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
@@ -542,49 +504,3 @@ class _WaypointListPanelState extends ConsumerState<WaypointListPanel> {
|
||||
_ => '—',
|
||||
};
|
||||
}
|
||||
|
||||
/// Kleiner getoenter Aktions-Button im Panel-Header (HTML-Demonstrator:
|
||||
/// #windFetchBtn/#windValidateBtn).
|
||||
class _HeaderActionButton extends StatelessWidget {
|
||||
const _HeaderActionButton({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.tint,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final Color tint;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: tint.withValues(alpha: 0.18),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: tint.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 15, color: Colors.white),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user