import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../domain/telemetry/drone_event.dart'; import '../../domain/telemetry/drone_status.dart'; import '../../domain/telemetry/telemetry_field_status.dart'; import '../dmc_colors.dart'; import '../providers/active_drone_profile_provider.dart'; import '../providers/drone_event_log_provider.dart'; import '../providers/telemetry_provider.dart'; enum _PanelTab { status, warnings } /// Vollbild-Statusmenue fuer den Fly-Modus (Doku: "ein neues menue anlegen /// fuer die neue drone status pille, nach dem vorbild des menues fuer /// drones and missions, aber jetzt fuer drone status and warnings") - /// dieselbe Tab-Struktur wie [MissionsDronesPanel], nur mit den beiden /// telemetriebezogenen Tabs Status/Warnings statt Missions/Drones. /// Erreichbar ueber die Drohnen-Status-Pille (Status-Tab) bzw. den /// Warnungs-Knopf (Warnings-Tab) in der Fusszeile im Fly-Modus (siehe /// bottom_stats_bar.dart/fly_screen.dart). class DroneStatusWarningsPanel extends ConsumerStatefulWidget { const DroneStatusWarningsPanel.status({super.key}) : _initialTab = _PanelTab.status; const DroneStatusWarningsPanel.warnings({super.key}) : _initialTab = _PanelTab.warnings; final _PanelTab _initialTab; @override ConsumerState createState() => _DroneStatusWarningsPanelState(); } class _DroneStatusWarningsPanelState extends ConsumerState { late _PanelTab _tab = widget._initialTab; @override Widget build(BuildContext context) { 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( 'Drone Status & Warnings', style: TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.w700, ), ), ), IconButton( onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.close, color: Colors.white), ), ], ), ), Row( children: [ _tabButton('Status', _PanelTab.status), _tabButton('Warnings', _PanelTab.warnings), ], ), const Divider(color: Colors.white24, height: 1), Expanded( child: switch (_tab) { _PanelTab.status => _statusTab(), _PanelTab.warnings => _warningsTab(), }, ), ], ), ), ); } 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, ), ), ), ), ); } // --- Status --- Widget _statusTab() { final telemetry = ref.watch(telemetryProvider); final droneProfile = ref.watch(activeDroneProfileProvider); return telemetry.when( data: (frame) { final fieldStatus = computeTelemetryFieldStatus( numSat: frame.numSat, hasFix: frame.hasFix, hdop: frame.hdop, currentA: frame.currentA, batteryVoltage: frame.batteryVoltage, linkQuality: frame.linkQuality, snrDb: frame.snrDb, profile: droneProfile, ); return ListView( padding: const EdgeInsets.all(16), children: _statusGrid([ // Farbcodierte Felder zuerst (Doku: "drone status felder im // menue auch farblich kodieren", Schwellwerte je Drohnenprofil - // siehe computeTelemetryFieldStatus), informelle Felder ohne // Ampel-Bewertung (Koordinaten/Heading/Alt/Speed) danach. Je 2 // Felder pro Zeile (Doku: "platz effizienter nutzen"). _statusRow('GPS satellites', '${frame.numSat}', level: fieldStatus.numSat), _statusRow( 'GPS precision (HDOP)', frame.hasFix ? frame.hdop.toStringAsFixed(2) : '—', level: fieldStatus.hdop, ), _statusRow( 'Current draw', '${frame.currentA.toStringAsFixed(1)} A', level: fieldStatus.currentA, ), _statusRow( 'Battery voltage', '${frame.batteryVoltage.toStringAsFixed(2)} V', level: fieldStatus.batteryVoltage, ), _statusRow('RX quality', '${frame.linkQuality}%', level: fieldStatus.rxQuality), _statusRow('RX SNR', '${frame.snrDb} dB', level: fieldStatus.rxSnr), _statusRow( 'GPS coordinates', frame.hasFix ? '${frame.lat.toStringAsFixed(6)}, ${frame.lon.toStringAsFixed(6)}' : '—', ), _statusRow('Heading', '${frame.headingDeg.round()}°'), _statusRow('Altitude', '${frame.altitudeM.toStringAsFixed(0)} m'), _statusRow('Speed', '${frame.speedMs.toStringAsFixed(1)} m/s'), ]), ); }, loading: () => const Center( child: CircularProgressIndicator(color: Color(0xFF4D9DFF)), ), error: (err, st) => const Center( child: Text('No telemetry', style: TextStyle(color: Colors.white60, fontSize: 15)), ), ); } /// Ordnet die Statuszeilen zu je 2 pro Zeile an (Doku: "2 felder pro zeile /// im drone status menue erlauben um platz effizienter zu nutzen") - bei /// ungerader Feldanzahl bleibt die letzte Zeile einspaltig statt sie /// kuenstlich zu strecken. List _statusGrid(List tiles) { final rows = []; for (var i = 0; i < tiles.length; i += 2) { final hasPair = i + 1 < tiles.length; // Kein CrossAxisAlignment.stretch: diese Row sitzt direkt als // ListView-Kind, das seinen Kindern unbegrenzte Hoehe gibt (0<=h<= // Infinity) - stretch verlangt aber eine endliche Hoehe, um die Kacheln // darauf zu strecken, und wirft sonst "BoxConstraints forces an // infinite height" (fuehrte zum komplett schwarzen Status-Tab). Default // (center) reicht, da beide Kacheln einer Zeile ohnehin fast gleich // hohen Inhalt haben. rows.add(Row( children: [ Expanded(child: tiles[i]), if (hasPair) ...[ const SizedBox(width: 8), Expanded(child: tiles[i + 1]), ], ], )); } return rows; } static const _signalColors = { SignalLevel.green: DmcColors.flyModeColor, SignalLevel.yellow: DmcColors.pafAmber, SignalLevel.red: DmcColors.warnRed, }; // Label ueber Wert statt nebeneinander (Doku: "2 felder pro zeile") - bei // halber Zeilenbreite kollidiert ein nebeneinander angeordnetes Label wie // "GPS precision (HDOP)" sonst mit dem Wert; gestapelt bleibt auch bei // laengeren Labeln/Werten genug Platz. Widget _statusRow(String label, String value, {SignalLevel? level}) { final valueColor = level != null ? _signalColors[level]! : Colors.white; return Container( margin: const EdgeInsets.symmetric(vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.04), borderRadius: BorderRadius.circular(10), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: const TextStyle(color: Colors.white70, fontSize: 12), overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Row( children: [ if (level != null) ...[ Container( width: 8, height: 8, margin: const EdgeInsets.only(right: 6), decoration: BoxDecoration(color: valueColor, shape: BoxShape.circle), ), ], Flexible( child: Text( value, style: TextStyle(color: valueColor, fontWeight: FontWeight.w600), overflow: TextOverflow.ellipsis, ), ), ], ), ], ), ); } // --- Warnings --- Widget _warningsTab() { final events = ref.watch(droneEventLogProvider); if (events.isEmpty) { return const Center( child: Text('No events yet', style: TextStyle(color: Colors.white60, fontSize: 15)), ); } return ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), itemCount: events.length, itemBuilder: (context, index) => _eventRow(events[index]), ); } Widget _eventRow(DroneEvent event) { final color = event.severity == DroneEventSeverity.critical ? DmcColors.warnRed : DmcColors.pafAmber; return Container( margin: const EdgeInsets.symmetric(vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.04), border: Border.all(color: color.withValues(alpha: 0.4)), borderRadius: BorderRadius.circular(10), ), child: Row( children: [ Text(_formatTime(event.timestamp), style: const TextStyle(color: Colors.white60)), const SizedBox(width: 12), Expanded( child: Text( event.message, style: TextStyle(color: color, fontWeight: FontWeight.w600), ), ), ], ), ); } String _formatTime(DateTime dt) => '${dt.hour.toString().padLeft(2, '0')}:' '${dt.minute.toString().padLeft(2, '0')}:' '${dt.second.toString().padLeft(2, '0')}'; }