diff --git a/app/lib/ui/screens/settings/settings_screen.dart b/app/lib/ui/screens/settings/settings_screen.dart index c40e209..b0b4bda 100644 --- a/app/lib/ui/screens/settings/settings_screen.dart +++ b/app/lib/ui/screens/settings/settings_screen.dart @@ -47,16 +47,16 @@ class _ConnectionSection extends ConsumerWidget { children: [ Row( children: [ - Text('Verbindung', style: Theme.of(context).textTheme.titleMedium), + Text('Connection', style: Theme.of(context).textTheme.titleMedium), const Spacer(), _StatePill(state: linkState), ], ), const SizedBox(height: 12), - const Text('Art: Bluetooth'), + const Text('Type: Bluetooth'), const SizedBox(height: 4), 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), ), if (errorReason != null) ...[ @@ -68,24 +68,24 @@ class _ConnectionSection extends ConsumerWidget { children: [ ElevatedButton( onPressed: () => _connect(ref), - child: const Text('Verbinden'), + child: const Text('Connect'), ), const SizedBox(width: 8), OutlinedButton( onPressed: () => ref.read(bluetoothTransportProvider).disconnect(), - child: const Text('Trennen'), + child: const Text('Disconnect'), ), const SizedBox(width: 8), TextButton( onPressed: BluetoothSystemSettings.openBluetoothSettings, - child: const Text('Geraet koppeln'), + child: const Text('Pair device'), ), ], ), const SizedBox(height: 16), SwitchListTile( 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, onChanged: (value) async { await ref @@ -95,13 +95,13 @@ class _ConnectionSection extends ConsumerWidget { }, ), 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), bondedDevices.when( data: (devices) => devices.isEmpty ? const Text( - 'Keine gekoppelten Geraete. Ueber "Geraet koppeln" ' - 'zunaechst einmalig ueber den Systemdialog koppeln.', + 'No paired devices yet. Pair one once via the system ' + 'dialog using "Pair device".', style: TextStyle(fontSize: 12, color: Colors.grey), ) : RadioGroup( @@ -129,13 +129,13 @@ class _ConnectionSection extends ConsumerWidget { child: LinearProgressIndicator(), ), error: (e, _) => Text( - 'Gekoppelte Geraete konnten nicht gelesen werden: $e', + 'Could not read paired devices: $e', style: const TextStyle(fontSize: 12, color: Colors.grey), ), ), const SizedBox(height: 16), Text( - 'Verbindungsprotokoll ($packetCount Datenpakete empfangen)', + 'Connection log ($packetCount packets received)', style: Theme.of(context).textTheme.titleSmall, ), const SizedBox(height: 4), @@ -143,7 +143,7 @@ class _ConnectionSection extends ConsumerWidget { height: 120, child: log.isEmpty ? const Text( - 'Noch keine Ereignisse.', + 'No events yet.', style: TextStyle(fontSize: 12, color: Colors.grey), ) : ListView.builder( @@ -203,10 +203,10 @@ class _StatePill extends StatelessWidget { @override Widget build(BuildContext context) { final (label, color) = switch (state) { - LinkState.connected => ('Verbunden', DmcColors.liveGreen), - LinkState.connecting => ('Verbinde ...', DmcColors.pafAmber), - LinkState.error => ('Fehler', DmcColors.warnRed), - LinkState.disconnected || null => ('Getrennt', Colors.grey), + LinkState.connected => ('Connected', DmcColors.liveGreen), + LinkState.connecting => ('Connecting…', DmcColors.pafAmber), + LinkState.error => ('Error', DmcColors.warnRed), + LinkState.disconnected || null => ('Disconnected', Colors.grey), }; return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), @@ -229,29 +229,28 @@ class _ErrorHint extends StatelessWidget { Widget build(BuildContext context) { final (text, action) = switch (reason) { LinkErrorReason.bluetoothOff => ( - 'Bluetooth ist aus. Bitte in den Systemeinstellungen einschalten.', - _HintAction('Einstellungen', BluetoothSystemSettings.openBluetoothSettings), + 'Bluetooth is off. Please enable it in system settings.', + _HintAction('Settings', BluetoothSystemSettings.openBluetoothSettings), ), LinkErrorReason.permissionDenied => ( - '"Geraete in der Naehe"-Berechtigung wird benoetigt. Bitte erneut ' - 'auf "Verbinden" tippen.', + 'The "Nearby devices" permission is required. Please tap ' + '"Connect" again.', null, ), LinkErrorReason.permissionDeniedPermanently => ( - '"Geraete in der Naehe"-Berechtigung wurde dauerhaft abgelehnt. ' - 'Bitte in den App-Einstellungen manuell erteilen.', - _HintAction('App-Einstellungen', BluetoothSystemSettings.openAppSettings), + 'The "Nearby devices" permission was permanently denied. Please ' + 'grant it manually in the app settings.', + _HintAction('App settings', BluetoothSystemSettings.openAppSettings), ), LinkErrorReason.deviceSelectionRequired => ( - 'Kein gekoppeltes mLRS-Geraet gefunden. Bitte unten ein Geraet ' - 'auswaehlen.', + 'No paired mLRS device found. Please select a device below.', null, ), LinkErrorReason.connectionFailed => ( - 'Verbindung zum Geraet ist fehlgeschlagen.', + 'Connection to the device failed.', null, ), - LinkErrorReason.unknown => ('Unbekannter Verbindungsfehler.', null), + LinkErrorReason.unknown => ('Unknown connection error.', null), }; return Container( padding: const EdgeInsets.all(8), diff --git a/app/lib/ui/widgets/bottom_stats_bar.dart b/app/lib/ui/widgets/bottom_stats_bar.dart index a1ad393..113c0f0 100644 --- a/app/lib/ui/widgets/bottom_stats_bar.dart +++ b/app/lib/ui/widgets/bottom_stats_bar.dart @@ -1,3 +1,5 @@ +import 'dart:math' as math; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -7,9 +9,11 @@ import '../providers/mission_meta_provider.dart'; import 'missions_drones_panel.dart'; /// Statuszeile am unteren Bildschirmrand (HTML-Demonstrator: #bottomBar/ -/// #statsRow). Details-Pille (mittig) oeffnet die Wegpunktliste; die -/// Mission-/Drohnen-Chips (links) oeffnen das Verwaltungsmenue mit dem -/// passenden Starttab (HTML-Demonstrator: #missionNameBtn/#droneNameBtn). +/// #statsRow). Details-Pille bleibt mittig und oeffnet die Wegpunktliste; +/// Mission-Chip links und Drohnen-Chip rechts oeffnen das Verwaltungsmenue +/// 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 /// noetige Upload-Anbindung existiert noch nicht. class BottomStatsBar extends ConsumerWidget { @@ -44,6 +48,18 @@ class BottomStatsBar extends ConsumerWidget { final missionName = ref.watch(currentMissionMetaProvider).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 // immer den gesamten verfuegbaren Platz seines Elternwidgets (auch wenn // sein Kind viel kleiner ist) - in einem Stack ohne begrenzende @@ -53,49 +69,57 @@ class BottomStatsBar extends ConsumerWidget { // dagegen zuverlaessig zentriert. return SafeArea( top: false, + left: false, + right: false, child: Padding( - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12), + padding: EdgeInsets.fromLTRB( + 12 + safeEdgeInset, + 10, + 12 + safeEdgeInset, + 10, + ), child: Row( 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( child: Align( alignment: Alignment.centerLeft, - child: Row( - children: [ - Flexible( - child: _NameChip( - label: 'Mission: $missionName', - color: Colors.white.withValues(alpha: 0.12), - textColor: Colors.white, - maxWidth: 160, - onTap: () => Navigator.of(context).push(MaterialPageRoute( - 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(), - )), - ), - ), - ], + child: _NameChip( + label: 'Mission: $missionName', + color: Colors.white.withValues(alpha: 0.12), + textColor: Colors.white, + onTap: () => Navigator.of(context).push(MaterialPageRoute( + builder: (_) => const MissionsDronesPanel.missions(), + )), ), ), ), _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( child: Align( alignment: Alignment.centerRight, child: Row( mainAxisSize: MainAxisSize.min, 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) ...[ _WarningButton(onTap: onWarningsTap), const SizedBox(width: 6), @@ -215,36 +239,34 @@ class _NameChip extends StatelessWidget { required this.color, required this.textColor, required this.onTap, - required this.maxWidth, }); final String label; final Color color; final Color textColor; final VoidCallback onTap; - final double maxWidth; @override 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( onTap: onTap, - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: maxWidth), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7), - decoration: BoxDecoration( - color: color, - borderRadius: BorderRadius.circular(14), - ), - child: Text( - label, - overflow: TextOverflow.ellipsis, - maxLines: 1, - style: TextStyle( - color: textColor, - fontWeight: FontWeight.w600, - fontSize: 12, - ), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(14), + ), + child: Text( + label, + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: TextStyle( + color: textColor, + fontWeight: FontWeight.w600, + fontSize: 12, ), ), ),