UI adjustments

This commit is contained in:
Constantin Leue
2026-07-30 21:34:48 +02:00
parent 078d58dbc0
commit 2ce1444b90
2 changed files with 98 additions and 77 deletions
@@ -47,16 +47,16 @@ class _ConnectionSection extends ConsumerWidget {
children: [ children: [
Row( Row(
children: [ children: [
Text('Verbindung', style: Theme.of(context).textTheme.titleMedium), Text('Connection', style: Theme.of(context).textTheme.titleMedium),
const Spacer(), const Spacer(),
_StatePill(state: linkState), _StatePill(state: linkState),
], ],
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
const Text('Art: Bluetooth'), const Text('Type: Bluetooth'),
const SizedBox(height: 4), const SizedBox(height: 4),
const Text( const Text(
'WLAN und USB folgen spaeter (Doku Kommunikationsschicht 6/7).', 'Wi-Fi and USB support are coming later.',
style: TextStyle(fontSize: 12, color: Colors.grey), style: TextStyle(fontSize: 12, color: Colors.grey),
), ),
if (errorReason != null) ...[ if (errorReason != null) ...[
@@ -68,24 +68,24 @@ class _ConnectionSection extends ConsumerWidget {
children: [ children: [
ElevatedButton( ElevatedButton(
onPressed: () => _connect(ref), onPressed: () => _connect(ref),
child: const Text('Verbinden'), child: const Text('Connect'),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
OutlinedButton( OutlinedButton(
onPressed: () => ref.read(bluetoothTransportProvider).disconnect(), onPressed: () => ref.read(bluetoothTransportProvider).disconnect(),
child: const Text('Trennen'), child: const Text('Disconnect'),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
TextButton( TextButton(
onPressed: BluetoothSystemSettings.openBluetoothSettings, onPressed: BluetoothSystemSettings.openBluetoothSettings,
child: const Text('Geraet koppeln'), child: const Text('Pair device'),
), ),
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
SwitchListTile( SwitchListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: const Text('Beim Wechsel in Fly automatisch verbinden'), title: const Text('Auto-connect when entering Fly mode'),
value: connectionSettings?.connectionAutoConnect ?? true, value: connectionSettings?.connectionAutoConnect ?? true,
onChanged: (value) async { onChanged: (value) async {
await ref await ref
@@ -95,13 +95,13 @@ class _ConnectionSection extends ConsumerWidget {
}, },
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text('Gekoppelte Geraete', style: Theme.of(context).textTheme.titleSmall), Text('Paired devices', style: Theme.of(context).textTheme.titleSmall),
const SizedBox(height: 4), const SizedBox(height: 4),
bondedDevices.when( bondedDevices.when(
data: (devices) => devices.isEmpty data: (devices) => devices.isEmpty
? const Text( ? const Text(
'Keine gekoppelten Geraete. Ueber "Geraet koppeln" ' 'No paired devices yet. Pair one once via the system '
'zunaechst einmalig ueber den Systemdialog koppeln.', 'dialog using "Pair device".',
style: TextStyle(fontSize: 12, color: Colors.grey), style: TextStyle(fontSize: 12, color: Colors.grey),
) )
: RadioGroup<String>( : RadioGroup<String>(
@@ -129,13 +129,13 @@ class _ConnectionSection extends ConsumerWidget {
child: LinearProgressIndicator(), child: LinearProgressIndicator(),
), ),
error: (e, _) => Text( error: (e, _) => Text(
'Gekoppelte Geraete konnten nicht gelesen werden: $e', 'Could not read paired devices: $e',
style: const TextStyle(fontSize: 12, color: Colors.grey), style: const TextStyle(fontSize: 12, color: Colors.grey),
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
'Verbindungsprotokoll ($packetCount Datenpakete empfangen)', 'Connection log ($packetCount packets received)',
style: Theme.of(context).textTheme.titleSmall, style: Theme.of(context).textTheme.titleSmall,
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
@@ -143,7 +143,7 @@ class _ConnectionSection extends ConsumerWidget {
height: 120, height: 120,
child: log.isEmpty child: log.isEmpty
? const Text( ? const Text(
'Noch keine Ereignisse.', 'No events yet.',
style: TextStyle(fontSize: 12, color: Colors.grey), style: TextStyle(fontSize: 12, color: Colors.grey),
) )
: ListView.builder( : ListView.builder(
@@ -203,10 +203,10 @@ class _StatePill extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final (label, color) = switch (state) { final (label, color) = switch (state) {
LinkState.connected => ('Verbunden', DmcColors.liveGreen), LinkState.connected => ('Connected', DmcColors.liveGreen),
LinkState.connecting => ('Verbinde ...', DmcColors.pafAmber), LinkState.connecting => ('Connecting…', DmcColors.pafAmber),
LinkState.error => ('Fehler', DmcColors.warnRed), LinkState.error => ('Error', DmcColors.warnRed),
LinkState.disconnected || null => ('Getrennt', Colors.grey), LinkState.disconnected || null => ('Disconnected', Colors.grey),
}; };
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
@@ -229,29 +229,28 @@ class _ErrorHint extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final (text, action) = switch (reason) { final (text, action) = switch (reason) {
LinkErrorReason.bluetoothOff => ( LinkErrorReason.bluetoothOff => (
'Bluetooth ist aus. Bitte in den Systemeinstellungen einschalten.', 'Bluetooth is off. Please enable it in system settings.',
_HintAction('Einstellungen', BluetoothSystemSettings.openBluetoothSettings), _HintAction('Settings', BluetoothSystemSettings.openBluetoothSettings),
), ),
LinkErrorReason.permissionDenied => ( LinkErrorReason.permissionDenied => (
'"Geraete in der Naehe"-Berechtigung wird benoetigt. Bitte erneut ' 'The "Nearby devices" permission is required. Please tap '
'auf "Verbinden" tippen.', '"Connect" again.',
null, null,
), ),
LinkErrorReason.permissionDeniedPermanently => ( LinkErrorReason.permissionDeniedPermanently => (
'"Geraete in der Naehe"-Berechtigung wurde dauerhaft abgelehnt. ' 'The "Nearby devices" permission was permanently denied. Please '
'Bitte in den App-Einstellungen manuell erteilen.', 'grant it manually in the app settings.',
_HintAction('App-Einstellungen', BluetoothSystemSettings.openAppSettings), _HintAction('App settings', BluetoothSystemSettings.openAppSettings),
), ),
LinkErrorReason.deviceSelectionRequired => ( LinkErrorReason.deviceSelectionRequired => (
'Kein gekoppeltes mLRS-Geraet gefunden. Bitte unten ein Geraet ' 'No paired mLRS device found. Please select a device below.',
'auswaehlen.',
null, null,
), ),
LinkErrorReason.connectionFailed => ( LinkErrorReason.connectionFailed => (
'Verbindung zum Geraet ist fehlgeschlagen.', 'Connection to the device failed.',
null, null,
), ),
LinkErrorReason.unknown => ('Unbekannter Verbindungsfehler.', null), LinkErrorReason.unknown => ('Unknown connection error.', null),
}; };
return Container( return Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
+50 -28
View File
@@ -1,3 +1,5 @@
import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -7,9 +9,11 @@ import '../providers/mission_meta_provider.dart';
import 'missions_drones_panel.dart'; import 'missions_drones_panel.dart';
/// Statuszeile am unteren Bildschirmrand (HTML-Demonstrator: #bottomBar/ /// Statuszeile am unteren Bildschirmrand (HTML-Demonstrator: #bottomBar/
/// #statsRow). Details-Pille (mittig) oeffnet die Wegpunktliste; die /// #statsRow). Details-Pille bleibt mittig und oeffnet die Wegpunktliste;
/// Mission-/Drohnen-Chips (links) oeffnen das Verwaltungsmenue mit dem /// Mission-Chip links und Drohnen-Chip rechts oeffnen das Verwaltungsmenue
/// passenden Starttab (HTML-Demonstrator: #missionNameBtn/#droneNameBtn). /// mit dem passenden Starttab (HTML-Demonstrator: #missionNameBtn/
/// #droneNameBtn). Warnungs- und Settings-Knopf sitzen neben dem
/// Drohnen-Chip rechts aussen.
/// Der Send-Button ist bewusst nicht Teil dieses Schritts - die dafuer /// Der Send-Button ist bewusst nicht Teil dieses Schritts - die dafuer
/// noetige Upload-Anbindung existiert noch nicht. /// noetige Upload-Anbindung existiert noch nicht.
class BottomStatsBar extends ConsumerWidget { class BottomStatsBar extends ConsumerWidget {
@@ -44,6 +48,18 @@ class BottomStatsBar extends ConsumerWidget {
final missionName = ref.watch(currentMissionMetaProvider).name; final missionName = ref.watch(currentMissionMetaProvider).name;
final droneName = ref.watch(activeDroneProfileProvider).name; final droneName = ref.watch(activeDroneProfileProvider).name;
// Gespiegelter Randabstand (wie in PlanScreen fuer die Drehraeder, Doku:
// "Symmetrischer Randabstand auf beiden Seiten"): die groessere der
// beiden echten Seiten-Insets wird auf BEIDE Seiten angewendet, statt
// SafeAreas Standardverhalten (jede Seite nur ihren eigenen, ggf. 0)
// zu uebernehmen - sonst waere z.B. eine Frontkamera-Aussparung im
// Querformat nur auf einer Seite der Fusszeile sichtbar beruecksichtigt,
// je nachdem wie herum das Geraet gehalten wird.
final safeEdgeInset = math.max(
MediaQuery.of(context).padding.left,
MediaQuery.of(context).padding.right,
);
// Row statt Stack: ein Align/Center ohne width-/heightFactor nimmt sich // Row statt Stack: ein Align/Center ohne width-/heightFactor nimmt sich
// immer den gesamten verfuegbaren Platz seines Elternwidgets (auch wenn // immer den gesamten verfuegbaren Platz seines Elternwidgets (auch wenn
// sein Kind viel kleiner ist) - in einem Stack ohne begrenzende // sein Kind viel kleiner ist) - in einem Stack ohne begrenzende
@@ -53,49 +69,57 @@ class BottomStatsBar extends ConsumerWidget {
// dagegen zuverlaessig zentriert. // dagegen zuverlaessig zentriert.
return SafeArea( return SafeArea(
top: false, top: false,
left: false,
right: false,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12), padding: EdgeInsets.fromLTRB(
12 + safeEdgeInset,
10,
12 + safeEdgeInset,
10,
),
child: Row( child: Row(
children: [ children: [
// Nur noch die Mission-Pille links - sie darf die gesamte
// verfuegbare Breite dieser Seite nutzen (Align loosent die
// Constraints statt sie wie Expanded zu erzwingen, das
// begrenzt auf die verfuegbare Breite, ohne einen kuenstlich
// kleinen Fixwert vorzugeben).
Expanded( Expanded(
child: Align( child: Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Row(
children: [
Flexible(
child: _NameChip( child: _NameChip(
label: 'Mission: $missionName', label: 'Mission: $missionName',
color: Colors.white.withValues(alpha: 0.12), color: Colors.white.withValues(alpha: 0.12),
textColor: Colors.white, textColor: Colors.white,
maxWidth: 160,
onTap: () => Navigator.of(context).push(MaterialPageRoute( onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (_) => const MissionsDronesPanel.missions(), builder: (_) => const MissionsDronesPanel.missions(),
)), )),
), ),
), ),
const SizedBox(width: 6),
Flexible(
child: _NameChip(
label: 'Drone: $droneName',
color: const Color(0xFF4D9DFF).withValues(alpha: 0.18),
textColor: const Color(0xFFCFE2FF),
maxWidth: 140,
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (_) => const MissionsDronesPanel.drones(),
)),
),
),
],
),
),
), ),
_detailsPill(), _detailsPill(),
// Drohnen-Pille + Warnungs-/Settings-Knopf rechts. Die Pille
// sitzt in Flexible, damit sie bei langen Namen zu Gunsten der
// beiden festgroessen Rundknoepfe schrumpft/ellipsiert, statt
// sie aus dem sichtbaren Bereich zu draengen.
Expanded( Expanded(
child: Align( child: Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Flexible(
child: _NameChip(
label: 'Drone: $droneName',
color: const Color(0xFF4D9DFF).withValues(alpha: 0.18),
textColor: const Color(0xFFCFE2FF),
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (_) => const MissionsDronesPanel.drones(),
)),
),
),
const SizedBox(width: 6),
if (warnings.isNotEmpty) ...[ if (warnings.isNotEmpty) ...[
_WarningButton(onTap: onWarningsTap), _WarningButton(onTap: onWarningsTap),
const SizedBox(width: 6), const SizedBox(width: 6),
@@ -215,21 +239,20 @@ class _NameChip extends StatelessWidget {
required this.color, required this.color,
required this.textColor, required this.textColor,
required this.onTap, required this.onTap,
required this.maxWidth,
}); });
final String label; final String label;
final Color color; final Color color;
final Color textColor; final Color textColor;
final VoidCallback onTap; final VoidCallback onTap;
final double maxWidth;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Kein fester maxWidth mehr - die Pille darf die verfuegbare Breite
// ihres Elternwidgets nutzen (Align/Flexible am Aufrufort begrenzen sie
// bereits auf die dort tatsaechlich verfuegbare Breite).
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -247,7 +270,6 @@ class _NameChip extends StatelessWidget {
), ),
), ),
), ),
),
); );
} }
} }