Drop the drone status panel's title bar, default the grid to 3 columns

The title row cost a full line of vertical space for a redundant
label. The close button now floats over the tab row instead, which
reserves 48px on its right so "System Messages" doesn't sit under it.
Also replaced the manually grouped 2/3-column row layout with uniform
3-per-row chunking of a flat field list, matching the user's request
to default to three fields per row.
This commit is contained in:
Constantin Leue
2026-08-07 23:03:43 +02:00
parent e9a5b3ea0b
commit 03c24b830e
2 changed files with 96 additions and 112 deletions
@@ -38,41 +38,41 @@ class _DroneStatusMessagesPanelState extends ConsumerState<DroneStatusMessagesPa
return Material( return Material(
color: const Color(0xEB080808), color: const Color(0xEB080808),
child: SafeArea( child: SafeArea(
child: Column( // Kein eigener Titel-Header mehr (Nutzerwunsch: mehr Platz fuer den
// Inhalt) - der Schliessen-Knopf schwebt stattdessen ueber der
// Tab-Zeile, die dafuer rechts Platz frei laesst (siehe unten),
// statt eine ganze Zeile fuer Titel+Knopf zu opfern.
child: Stack(
children: [ children: [
Padding( Column(
padding: const EdgeInsets.fromLTRB(16, 12, 8, 12),
child: Row(
children: [
const Expanded(
child: Text(
'Drone Status & System Messages',
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: [ children: [
_tabButton('Status', _PanelTab.status), Padding(
_tabButton('System Messages', _PanelTab.messages), // Rechts Platz fuer den schwebenden Schliessen-Knopf, damit
// "System Messages" nicht darunter verschwindet.
padding: const EdgeInsets.only(right: 48),
child: Row(
children: [
_tabButton('Status', _PanelTab.status),
_tabButton('System Messages', _PanelTab.messages),
],
),
),
const Divider(color: Colors.white24, height: 1),
Expanded(
child: switch (_tab) {
_PanelTab.status => _statusTab(),
_PanelTab.messages => _messagesTab(),
},
),
], ],
), ),
const Divider(color: Colors.white24, height: 1), Positioned(
Expanded( top: 4,
child: switch (_tab) { right: 4,
_PanelTab.status => _statusTab(), child: IconButton(
_PanelTab.messages => _messagesTab(), onPressed: () => Navigator.of(context).pop(),
}, icon: const Icon(Icons.close, color: Colors.white),
),
), ),
], ],
), ),
@@ -144,97 +144,80 @@ class _DroneStatusMessagesPanelState extends ConsumerState<DroneStatusMessagesPa
// Wertkachel bieten wuerde. // Wertkachel bieten wuerde.
_sensorStatusRow(frame), _sensorStatusRow(frame),
const SizedBox(height: 8), const SizedBox(height: 8),
// Flugmodus zeigt bei fehlender Nav-Uebersteuerung "Idle" (siehe
// MspNavMode._modeNames) - das heisst nur "kein GPS-Nav-Modus
// aktiv", nicht zwingend "steht am Boden" (reine Angle/Horizon/
// Acro-Fluglage ist ueber MSP_NAV_STATUS nicht sichtbar).
..._statusRows([ ..._statusRows([
// Arming + erste 3 Temperaturen + Flugmodus als eigene Zeile _statusRow(
// direkt unter der Sensor-Status-Zeile (Doku), noch vor den 'Arming',
// farbcodierten Feldern. Flugmodus zeigt bei fehlender frame == null ? '—' : (frame.armed ? 'Armed' : 'Disarmed'),
// Nav-Uebersteuerung "Idle" (siehe MspNavMode._modeNames) - das level: frame?.armed == true ? SignalLevel.green : null,
// heisst nur "kein GPS-Nav-Modus aktiv", nicht zwingend "steht am ),
// Boden" (reine Angle/Horizon/Acro-Fluglage ist ueber _statusRow('Temperatures', _formatTemperatures(frame)),
// MSP_NAV_STATUS nicht sichtbar). _statusRow('Flight mode', frame != null ? frame.flightMode : '—'),
[
_statusRow(
'Arming',
frame == null ? '—' : (frame.armed ? 'Armed' : 'Disarmed'),
level: frame?.armed == true ? SignalLevel.green : null,
),
_statusRow('Temperatures', _formatTemperatures(frame)),
_statusRow('Flight mode', frame != null ? frame.flightMode : '—'),
],
// Farbcodierte Felder (Doku: "drone status felder im menue auch // Farbcodierte Felder (Doku: "drone status felder im menue auch
// farblich kodieren", Schwellwerte je Drohnenprofil - siehe // farblich kodieren", Schwellwerte je Drohnenprofil - siehe
// computeTelemetryFieldStatus), informelle Felder ohne Ampel- // computeTelemetryFieldStatus), informelle Felder ohne Ampel-
// Bewertung (Koordinaten/Heading/Alt/Speed/Vertical speed) // Bewertung (Koordinaten/Heading/Alt/Speed/Vertical speed) danach.
// danach. _statusRow(
[ 'GPS fix type',
_statusRow( _formatFixType(frame),
'GPS fix type', level: frame != null ? _fixTypeLevel(frame.fixType) : null,
_formatFixType(frame), ),
level: frame != null ? _fixTypeLevel(frame.fixType) : null, _statusRow('GPS satellites', frame != null ? '${frame.numSat}' : '—', level: fieldStatus.numSat),
), _statusRow(
_statusRow('GPS satellites', frame != null ? '${frame.numSat}' : '—', level: fieldStatus.numSat), 'GPS precision (HDOP)',
_statusRow( frame != null && frame.hasFix ? frame.hdop.toStringAsFixed(2) : '—',
'GPS precision (HDOP)', level: fieldStatus.hdop,
frame != null && frame.hasFix ? frame.hdop.toStringAsFixed(2) : '—', ),
level: fieldStatus.hdop, _statusRow(
), 'Current draw',
], frame != null ? '${frame.currentA.toStringAsFixed(1)} A' : '—',
[ level: fieldStatus.currentA,
_statusRow( ),
'Current draw', _statusRow(
frame != null ? '${frame.currentA.toStringAsFixed(1)} A' : '—', 'Battery voltage',
level: fieldStatus.currentA, frame != null ? '${frame.batteryVoltage.toStringAsFixed(2)} V' : '—',
), level: fieldStatus.batteryVoltage,
_statusRow( ),
'Battery voltage', _statusRow('RX quality', frame != null ? '${frame.linkQuality}%' : '—', level: fieldStatus.rxQuality),
frame != null ? '${frame.batteryVoltage.toStringAsFixed(2)} V' : '—', _statusRow('RX SNR', frame != null ? '${frame.snrDb} dB' : '—', level: fieldStatus.rxSnr),
level: fieldStatus.batteryVoltage, _statusRow(
), 'GPS coordinates',
], frame != null && frame.hasFix
[ ? '${frame.lat.toStringAsFixed(6)}, ${frame.lon.toStringAsFixed(6)}'
_statusRow('RX quality', frame != null ? '${frame.linkQuality}%' : '—', level: fieldStatus.rxQuality), : '—',
_statusRow('RX SNR', frame != null ? '${frame.snrDb} dB' : '—', level: fieldStatus.rxSnr), ),
], _statusRow('Heading', frame != null ? '${frame.headingDeg.round()}°' : '—'),
[ _statusRow('Altitude', _formatAltitude(frame)),
_statusRow( _statusRow('Speed', frame != null ? '${frame.speedMs.toStringAsFixed(1)} m/s' : '—'),
'GPS coordinates',
frame != null && frame.hasFix
? '${frame.lat.toStringAsFixed(6)}, ${frame.lon.toStringAsFixed(6)}'
: '—',
),
_statusRow('Heading', frame != null ? '${frame.headingDeg.round()}°' : '—'),
],
[
_statusRow('Altitude', _formatAltitude(frame)),
_statusRow('Speed', frame != null ? '${frame.speedMs.toStringAsFixed(1)} m/s' : '—'),
],
// Ans Ende der Liste (Doku: "ans ende der liste noch die // Ans Ende der Liste (Doku: "ans ende der liste noch die
// steig/sinkrate hinzufuegen") - eigene Zeile, da kein Partnerfeld // steig/sinkrate hinzufuegen").
// mehr uebrig ist. _statusRow('Vertical speed', _formatVerticalSpeed(frame)),
[_statusRow('Vertical speed', _formatVerticalSpeed(frame))],
]), ]),
], ],
); );
} }
/// Ordnet die Statuszeilen in explizit vorgegebenen Gruppen an (Doku: "2 /// Ordnet die Statuszeilen zu je [perRow] pro Zeile an (Doku: "standardmaessig
/// felder pro zeile im drone status menue erlauben um platz effizienter zu /// drei felder pro zeile im drone status menue anzeigen") - bei einer nicht
/// nutzen", inzwischen mit einzelnen 3er-Zeilen wie Arming/Temperatures/ /// durch [perRow] teilbaren Feldanzahl bleibt die letzte Zeile schmaler,
/// Flight mode) - anders als eine reine Zweier-Paarbildung braucht das /// statt sie kuenstlich mit leeren Kacheln aufzufuellen.
/// gemischte 2er/3er-Layout eine explizite Gruppierung pro Zeile statt List<Widget> _statusRows(List<Widget> tiles, {int perRow = 3}) {
/// automatischem Chunking. final rows = <Widget>[];
List<Widget> _statusRows(List<List<Widget>> rows) { for (var i = 0; i < tiles.length; i += perRow) {
return [ final chunk = tiles.skip(i).take(perRow).toList();
for (final tiles in rows) rows.add(Row(
Row( children: [
children: [ for (var j = 0; j < chunk.length; j++) ...[
for (var i = 0; i < tiles.length; i++) ...[ if (j > 0) const SizedBox(width: 8),
if (i > 0) const SizedBox(width: 8), Expanded(child: chunk[j]),
Expanded(child: tiles[i]),
],
], ],
), ],
]; ));
}
return rows;
} }
static const _signalColors = { static const _signalColors = {
+2 -1
View File
@@ -30,6 +30,7 @@ import 'package:dmc_app/ui/providers/system_message_log_provider.dart';
import 'package:dmc_app/ui/providers/telemetry_provider.dart'; import 'package:dmc_app/ui/providers/telemetry_provider.dart';
import 'package:dmc_app/transport/mock/mock_flight_controller_link.dart'; import 'package:dmc_app/transport/mock/mock_flight_controller_link.dart';
import 'package:dmc_app/ui/widgets/drone_follow_icon.dart'; import 'package:dmc_app/ui/widgets/drone_follow_icon.dart';
import 'package:dmc_app/ui/widgets/drone_status_messages_panel.dart';
import 'package:dmc_app/ui/widgets/full_value_chart.dart'; import 'package:dmc_app/ui/widgets/full_value_chart.dart';
import 'package:dmc_app/ui/widgets/halo_menu.dart'; import 'package:dmc_app/ui/widgets/halo_menu.dart';
import 'package:dmc_app/ui/widgets/header_wind_pill.dart'; import 'package:dmc_app/ui/widgets/header_wind_pill.dart';
@@ -1160,7 +1161,7 @@ void main() {
// steht im Widget-Baum zuerst. // steht im Widget-Baum zuerst.
await tester.tap(find.byType(PaperPlaneIcon).first); await tester.tap(find.byType(PaperPlaneIcon).first);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('Drone Status & System Messages'), findsOneWidget); expect(find.byType(DroneStatusMessagesPanel), findsOneWidget);
await tester.tap(find.text('System Messages')); await tester.tap(find.text('System Messages'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();