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(
color: const Color(0xEB080808),
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: [
Padding(
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(
Column(
children: [
_tabButton('Status', _PanelTab.status),
_tabButton('System Messages', _PanelTab.messages),
Padding(
// 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),
Expanded(
child: switch (_tab) {
_PanelTab.status => _statusTab(),
_PanelTab.messages => _messagesTab(),
},
Positioned(
top: 4,
right: 4,
child: IconButton(
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.
_sensorStatusRow(frame),
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([
// 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(
'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 : '—'),
],
_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
// farblich kodieren", Schwellwerte je Drohnenprofil - siehe
// computeTelemetryFieldStatus), informelle Felder ohne Ampel-
// Bewertung (Koordinaten/Heading/Alt/Speed/Vertical speed)
// danach.
[
_statusRow(
'GPS fix type',
_formatFixType(frame),
level: frame != null ? _fixTypeLevel(frame.fixType) : null,
),
_statusRow('GPS satellites', frame != null ? '${frame.numSat}' : '—', level: fieldStatus.numSat),
_statusRow(
'GPS precision (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(
'Battery voltage',
frame != null ? '${frame.batteryVoltage.toStringAsFixed(2)} V' : '—',
level: fieldStatus.batteryVoltage,
),
],
[
_statusRow('RX quality', frame != null ? '${frame.linkQuality}%' : '—', level: fieldStatus.rxQuality),
_statusRow('RX SNR', frame != null ? '${frame.snrDb} dB' : '—', level: fieldStatus.rxSnr),
],
[
_statusRow(
'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' : '—'),
],
// Bewertung (Koordinaten/Heading/Alt/Speed/Vertical speed) danach.
_statusRow(
'GPS fix type',
_formatFixType(frame),
level: frame != null ? _fixTypeLevel(frame.fixType) : null,
),
_statusRow('GPS satellites', frame != null ? '${frame.numSat}' : '—', level: fieldStatus.numSat),
_statusRow(
'GPS precision (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(
'Battery voltage',
frame != null ? '${frame.batteryVoltage.toStringAsFixed(2)} V' : '—',
level: fieldStatus.batteryVoltage,
),
_statusRow('RX quality', frame != null ? '${frame.linkQuality}%' : '—', level: fieldStatus.rxQuality),
_statusRow('RX SNR', frame != null ? '${frame.snrDb} dB' : '—', level: fieldStatus.rxSnr),
_statusRow(
'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
// steig/sinkrate hinzufuegen") - eigene Zeile, da kein Partnerfeld
// mehr uebrig ist.
[_statusRow('Vertical speed', _formatVerticalSpeed(frame))],
// steig/sinkrate hinzufuegen").
_statusRow('Vertical speed', _formatVerticalSpeed(frame)),
]),
],
);
}
/// Ordnet die Statuszeilen in explizit vorgegebenen Gruppen an (Doku: "2
/// felder pro zeile im drone status menue erlauben um platz effizienter zu
/// nutzen", inzwischen mit einzelnen 3er-Zeilen wie Arming/Temperatures/
/// Flight mode) - anders als eine reine Zweier-Paarbildung braucht das
/// gemischte 2er/3er-Layout eine explizite Gruppierung pro Zeile statt
/// automatischem Chunking.
List<Widget> _statusRows(List<List<Widget>> rows) {
return [
for (final tiles in rows)
Row(
children: [
for (var i = 0; i < tiles.length; i++) ...[
if (i > 0) const SizedBox(width: 8),
Expanded(child: tiles[i]),
],
/// Ordnet die Statuszeilen zu je [perRow] pro Zeile an (Doku: "standardmaessig
/// drei felder pro zeile im drone status menue anzeigen") - bei einer nicht
/// durch [perRow] teilbaren Feldanzahl bleibt die letzte Zeile schmaler,
/// statt sie kuenstlich mit leeren Kacheln aufzufuellen.
List<Widget> _statusRows(List<Widget> tiles, {int perRow = 3}) {
final rows = <Widget>[];
for (var i = 0; i < tiles.length; i += perRow) {
final chunk = tiles.skip(i).take(perRow).toList();
rows.add(Row(
children: [
for (var j = 0; j < chunk.length; j++) ...[
if (j > 0) const SizedBox(width: 8),
Expanded(child: chunk[j]),
],
),
];
],
));
}
return rows;
}
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/transport/mock/mock_flight_controller_link.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/halo_menu.dart';
import 'package:dmc_app/ui/widgets/header_wind_pill.dart';
@@ -1160,7 +1161,7 @@ void main() {
// steht im Widget-Baum zuerst.
await tester.tap(find.byType(PaperPlaneIcon).first);
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.pumpAndSettle();