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,35 +38,25 @@ 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: [
Column(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 8, 12), // Rechts Platz fuer den schwebenden Schliessen-Knopf, damit
// "System Messages" nicht darunter verschwindet.
padding: const EdgeInsets.only(right: 48),
child: Row( 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), _tabButton('Status', _PanelTab.status),
_tabButton('System Messages', _PanelTab.messages), _tabButton('System Messages', _PanelTab.messages),
], ],
), ),
),
const Divider(color: Colors.white24, height: 1), const Divider(color: Colors.white24, height: 1),
Expanded( Expanded(
child: switch (_tab) { child: switch (_tab) {
@@ -76,6 +66,16 @@ class _DroneStatusMessagesPanelState extends ConsumerState<DroneStatusMessagesPa
), ),
], ],
), ),
Positioned(
top: 4,
right: 4,
child: IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close, color: Colors.white),
),
),
],
),
), ),
); );
} }
@@ -144,15 +144,11 @@ 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
// direkt unter der Sensor-Status-Zeile (Doku), noch vor den
// farbcodierten Feldern. 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).
[
_statusRow( _statusRow(
'Arming', 'Arming',
frame == null ? '—' : (frame.armed ? 'Armed' : 'Disarmed'), frame == null ? '—' : (frame.armed ? 'Armed' : 'Disarmed'),
@@ -160,13 +156,10 @@ class _DroneStatusMessagesPanelState extends ConsumerState<DroneStatusMessagesPa
), ),
_statusRow('Temperatures', _formatTemperatures(frame)), _statusRow('Temperatures', _formatTemperatures(frame)),
_statusRow('Flight mode', frame != null ? frame.flightMode : '—'), _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( _statusRow(
'GPS fix type', 'GPS fix type',
_formatFixType(frame), _formatFixType(frame),
@@ -178,8 +171,6 @@ class _DroneStatusMessagesPanelState extends ConsumerState<DroneStatusMessagesPa
frame != null && frame.hasFix ? frame.hdop.toStringAsFixed(2) : '—', frame != null && frame.hasFix ? frame.hdop.toStringAsFixed(2) : '—',
level: fieldStatus.hdop, level: fieldStatus.hdop,
), ),
],
[
_statusRow( _statusRow(
'Current draw', 'Current draw',
frame != null ? '${frame.currentA.toStringAsFixed(1)} A' : '—', frame != null ? '${frame.currentA.toStringAsFixed(1)} A' : '—',
@@ -190,12 +181,8 @@ class _DroneStatusMessagesPanelState extends ConsumerState<DroneStatusMessagesPa
frame != null ? '${frame.batteryVoltage.toStringAsFixed(2)} V' : '—', frame != null ? '${frame.batteryVoltage.toStringAsFixed(2)} V' : '—',
level: fieldStatus.batteryVoltage, level: fieldStatus.batteryVoltage,
), ),
],
[
_statusRow('RX quality', frame != null ? '${frame.linkQuality}%' : '—', level: fieldStatus.rxQuality), _statusRow('RX quality', frame != null ? '${frame.linkQuality}%' : '—', level: fieldStatus.rxQuality),
_statusRow('RX SNR', frame != null ? '${frame.snrDb} dB' : '—', level: fieldStatus.rxSnr), _statusRow('RX SNR', frame != null ? '${frame.snrDb} dB' : '—', level: fieldStatus.rxSnr),
],
[
_statusRow( _statusRow(
'GPS coordinates', 'GPS coordinates',
frame != null && frame.hasFix frame != null && frame.hasFix
@@ -203,38 +190,34 @@ class _DroneStatusMessagesPanelState extends ConsumerState<DroneStatusMessagesPa
: '—', : '—',
), ),
_statusRow('Heading', frame != null ? '${frame.headingDeg.round()}°' : '—'), _statusRow('Heading', frame != null ? '${frame.headingDeg.round()}°' : '—'),
],
[
_statusRow('Altitude', _formatAltitude(frame)), _statusRow('Altitude', _formatAltitude(frame)),
_statusRow('Speed', frame != null ? '${frame.speedMs.toStringAsFixed(1)} m/s' : '—'), _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 i = 0; i < tiles.length; i++) ...[ for (var j = 0; j < chunk.length; j++) ...[
if (i > 0) const SizedBox(width: 8), if (j > 0) const SizedBox(width: 8),
Expanded(child: tiles[i]), Expanded(child: chunk[j]),
], ],
], ],
), ));
]; }
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();