import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../domain/mission/default_drone_profile.dart'; import '../../domain/mission/route_geometry.dart'; import '../../domain/waypoint/flat_waypoint_list.dart'; import '../providers/current_mission_provider.dart'; import 'full_value_chart.dart'; import 'waypoint_chip.dart'; enum _PanelTab { list, altitude, speed } /// Vollbild-Wegpunktliste (HTML-Demonstrator: #wpListPanel). Wird von /// PlanScreen als eigene Vollbild-Route gepusht (Navigator.push), damit sie /// wirklich alles ueberdeckt, inklusive der Kopfleiste - anders als ein /// interner Stack-Overlay innerhalb des Plan-Screens. /// /// Berechnet die Flugpfad-Geometrie (fuer bad-Markierungen) selbst aus dem /// beobachteten currentMissionProvider, damit sie auch reaktiv bleibt, /// wenn sich Wegpunkte aendern, waehrend das Panel offen ist. /// /// Nur eine Zeile gleichzeitig editierbar (rowEditIdx); Antippen einer /// Zeile ausserhalb des Edit-Modus zentriert die Karte auf diesen /// Wegpunkt und schliesst die Liste (Doku 3.6). class WaypointListPanel extends ConsumerStatefulWidget { const WaypointListPanel({ super.key, required this.activeIndex, required this.onClose, required this.onCenterOnWaypoint, required this.onDelete, required this.onAltitudeChanged, required this.onSpeedChanged, required this.onCatchRadiusChanged, required this.onActionChanged, }); final int activeIndex; final VoidCallback onClose; final void Function(int index) onCenterOnWaypoint; final void Function(int index) onDelete; final void Function(int index, double value) onAltitudeChanged; final void Function(int index, double value) onSpeedChanged; final void Function(int index, double value) onCatchRadiusChanged; final void Function(int index, WaypointAction action) onActionChanged; @override ConsumerState createState() => _WaypointListPanelState(); } class _WaypointListPanelState extends ConsumerState { int _rowEditIndex = -1; _PanelTab _tab = _PanelTab.list; static const _headerStyle = TextStyle( color: Colors.white60, fontSize: 12, fontWeight: FontWeight.w700, letterSpacing: 0.5, ); @override Widget build(BuildContext context) { final waypoints = ref.watch(currentMissionProvider); final routeGeometry = buildRouteGeometry( waypoints, minTurnRadius: DefaultDroneProfile.minTurnRadius, maxClimbRate: DefaultDroneProfile.maxClimbRate, maxDescentRate: DefaultDroneProfile.maxDescentRate, ); return Material( color: const Color(0xEB080808), child: SafeArea( child: Column( 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, ), ), ), IconButton( onPressed: widget.onClose, icon: const Icon(Icons.close, color: Colors.white), ), ], ), ), 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), _PanelTab.altitude => _chartTab( waypoints, routeGeometry, values: [for (final w in waypoints) w.altitudeM], min: DefaultDroneProfile.altMin, max: DefaultDroneProfile.altMax, step: DefaultDroneProfile.altStep, pxPerUnit: 5, unit: ' m', onChanged: widget.onAltitudeChanged, ), _PanelTab.speed => _chartTab( waypoints, routeGeometry, values: [for (final w in waypoints) w.speedMs], min: DefaultDroneProfile.speedMin, max: DefaultDroneProfile.speedMax, step: DefaultDroneProfile.speedStep, pxPerUnit: 22, unit: ' m/s', onChanged: widget.onSpeedChanged, ), }, ), ], ), ), ); } Widget _tabButton(String label, _PanelTab tab) { final active = _tab == tab; return Expanded( child: InkWell( onTap: () => setState(() => _tab = tab), child: Container( padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration( border: Border( bottom: BorderSide( color: active ? const Color(0xFF4D9DFF) : Colors.transparent, width: 2, ), ), ), child: Text( label, textAlign: TextAlign.center, style: TextStyle( color: active ? Colors.white : Colors.white60, fontWeight: FontWeight.w600, fontSize: 14, ), ), ), ), ); } Widget _chartTab( List waypoints, RouteGeometry routeGeometry, { required List values, required double min, required double max, required double step, required double pxPerUnit, required String unit, required void Function(int index, double value) onChanged, }) { return Padding( padding: const EdgeInsets.fromLTRB(8, 16, 8, 8), child: FullValueChart( waypoints: waypoints, values: values, legBad: routeGeometry.legClimbBad, vertexBad: routeGeometry.vertexBad, activeIndex: widget.activeIndex, min: min, max: max, step: step, pxPerUnit: pxPerUnit, unit: unit, onChanged: onChanged, ), ); } Widget _listTab(List waypoints, RouteGeometry routeGeometry) { return Column( children: [ if (waypoints.isNotEmpty) Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: const [ SizedBox(width: 34), Expanded(child: Text('ALTITUDE', style: _headerStyle)), Expanded(child: Text('SPEED', style: _headerStyle)), Expanded(child: Text('EXECUTE', style: _headerStyle)), Expanded(child: Text('CATCH', style: _headerStyle)), SizedBox(width: 76), ], ), ), Expanded( child: waypoints.isEmpty ? const Center( child: Text( 'No waypoints yet', style: TextStyle(color: Colors.white60, fontSize: 15), ), ) : ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: waypoints.length, itemBuilder: (context, index) => _row(waypoints[index], index, routeGeometry), ), ), ], ); } Widget _row(Waypoint wp, int index, RouteGeometry routeGeometry) { final isEditing = _rowEditIndex == index; final isActive = widget.activeIndex == index; final isBad = index < routeGeometry.vertexBad.length && routeGeometry.vertexBad[index]; return GestureDetector( onTap: () { if (_rowEditIndex >= 0) return; widget.onCenterOnWaypoint(index); }, child: Container( margin: const EdgeInsets.symmetric(vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8), decoration: BoxDecoration( color: isActive ? const Color(0xFF4D9DFF).withValues(alpha: 0.28) : Colors.white.withValues(alpha: 0.04), border: isActive ? Border.all(color: const Color(0xFF4D9DFF)) : null, borderRadius: BorderRadius.circular(10), ), child: Row( children: [ CircleAvatar( radius: 13, backgroundColor: isBad ? const Color(0xFFE2574A).withValues(alpha: 0.5) : Colors.white24, child: Text( '${index + 1}', style: const TextStyle(color: Colors.white, fontSize: 12), ), ), const SizedBox(width: 8), Expanded( child: isEditing ? WaypointChip( value: wp.altitudeM, unit: ' m', min: DefaultDroneProfile.altMin, max: DefaultDroneProfile.altMax, onChanged: (v) => widget.onAltitudeChanged(index, v), ) : Text('${wp.altitudeM.round()} m', style: const TextStyle(color: Colors.white)), ), Expanded( child: isEditing ? WaypointChip( value: wp.speedMs, unit: ' m/s', min: DefaultDroneProfile.speedMin, max: DefaultDroneProfile.speedMax, onChanged: (v) => widget.onSpeedChanged(index, v), ) : Text('${wp.speedMs.round()} m/s', style: const TextStyle(color: Colors.white)), ), Expanded( child: isEditing ? DropdownButton( value: wp.action, isDense: true, dropdownColor: const Color(0xFF1A1A1A), style: const TextStyle(color: Colors.white, fontSize: 13), items: const [ DropdownMenuItem( value: WaypointAction.none, child: Text('—'), ), DropdownMenuItem( value: WaypointAction.loiter, child: Text('Loiter'), ), DropdownMenuItem( value: WaypointAction.landing, child: Text('Landing'), ), ], onChanged: (action) { if (action != null) widget.onActionChanged(index, action); }, ) : Text(_actionLabel(wp.action), style: const TextStyle(color: Colors.white)), ), Expanded( child: isEditing ? WaypointChip( value: wp.catchRadiusM, unit: ' m', min: 5, max: 500, onChanged: (v) => widget.onCatchRadiusChanged(index, v), ) : Text('${wp.catchRadiusM.round()} m', style: const TextStyle(color: Colors.white)), ), IconButton( onPressed: () => setState( () => _rowEditIndex = isEditing ? -1 : index, ), icon: Icon( isEditing ? Icons.check : Icons.edit, color: Colors.white70, size: 18, ), ), IconButton( onPressed: () { widget.onDelete(index); if (_rowEditIndex == index) setState(() => _rowEditIndex = -1); }, icon: const Icon(Icons.delete_outline, color: Color(0xFFE2574A), size: 18), ), ], ), ), ); } String _actionLabel(WaypointAction action) => switch (action) { WaypointAction.loiter => 'Loiter', WaypointAction.landing => 'Landing', _ => '—', }; }