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:
@@ -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 = {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user