From 24db38dc33d7b5e71ed2ca914cda6bfca9709baa Mon Sep 17 00:00:00 2001 From: Constantin Leue <77660791+Consti3D@users.noreply.github.com> Date: Sun, 2 Aug 2026 08:45:24 +0200 Subject: [PATCH] settings ui clean up --- .../ui/screens/settings/settings_screen.dart | 196 ++++++++---------- 1 file changed, 87 insertions(+), 109 deletions(-) diff --git a/app/lib/ui/screens/settings/settings_screen.dart b/app/lib/ui/screens/settings/settings_screen.dart index 83b8b0e..b437227 100644 --- a/app/lib/ui/screens/settings/settings_screen.dart +++ b/app/lib/ui/screens/settings/settings_screen.dart @@ -53,37 +53,29 @@ class _ConnectionSection extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Connection', style: Theme.of(context).textTheme.titleMedium), - const SizedBox(height: 12), - DropdownButtonFormField( - initialValue: selectedType, - decoration: const InputDecoration( - labelText: 'Type', - isDense: true, - border: OutlineInputBorder(), + // Fuer WLAN rendert _WifiPanel das Typ-Dropdown selbst als Teil + // seiner kompakten ersten Zeile (zusammen mit Connect/Disconnect + // und den Status-Pillen) - fuer die anderen Verbindungsarten + // bleibt die bisherige, einfachere Kopfzeile bestehen. + if (selectedType != ConnectionType.wifi) ...[ + Text('Connection', style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 12), + DropdownButtonFormField( + initialValue: selectedType, + decoration: const InputDecoration( + labelText: 'Type', + isDense: true, + border: OutlineInputBorder(), + ), + items: _connectionTypeItems, + onChanged: (type) async { + if (type == null) return; + await ref.read(appSettingsRepositoryProvider).setConnectionType(type); + ref.invalidate(connectionSettingsProvider); + }, ), - items: const [ - DropdownMenuItem(value: ConnectionType.wifi, child: Text('WiFi')), - DropdownMenuItem( - value: ConnectionType.cellular5g, - child: Text('5G (coming soon)'), - ), - DropdownMenuItem( - value: ConnectionType.bluetooth, - child: Text('Bluetooth'), - ), - DropdownMenuItem( - value: ConnectionType.usb, - child: Text('USB (coming soon)'), - ), - ], - onChanged: (type) async { - if (type == null) return; - await ref.read(appSettingsRepositoryProvider).setConnectionType(type); - ref.invalidate(connectionSettingsProvider); - }, - ), - const SizedBox(height: 16), + const SizedBox(height: 16), + ], switch (selectedType) { ConnectionType.wifi => const _WifiPanel(), ConnectionType.bluetooth => const _BluetoothPanel(), @@ -99,6 +91,17 @@ class _ConnectionSection extends ConsumerWidget { } } +/// Geteilt zwischen der kompakten WLAN-Zeile ([_WifiPanel]) und der +/// generischen Kopfzeile fuer die anderen Verbindungsarten +/// ([_ConnectionSection]), damit die Beschriftungen an beiden Stellen +/// gleich lauten. +const _connectionTypeItems = [ + DropdownMenuItem(value: ConnectionType.wifi, child: Text('WiFi')), + DropdownMenuItem(value: ConnectionType.cellular5g, child: Text('5G (soon)')), + DropdownMenuItem(value: ConnectionType.bluetooth, child: Text('Bluetooth')), + DropdownMenuItem(value: ConnectionType.usb, child: Text('USB (soon)')), +]; + class _NotAvailablePanel extends StatelessWidget { const _NotAvailablePanel({required this.label}); @@ -122,7 +125,6 @@ class _WifiPanel extends ConsumerStatefulWidget { class _WifiPanelState extends ConsumerState<_WifiPanel> { late final TextEditingController _ssidPrefixController; - late final TextEditingController _hostController; late final TextEditingController _portController; bool _controllersSeeded = false; @@ -130,14 +132,12 @@ class _WifiPanelState extends ConsumerState<_WifiPanel> { void initState() { super.initState(); _ssidPrefixController = TextEditingController(); - _hostController = TextEditingController(); _portController = TextEditingController(); } @override void dispose() { _ssidPrefixController.dispose(); - _hostController.dispose(); _portController.dispose(); super.dispose(); } @@ -155,7 +155,6 @@ class _WifiPanelState extends ConsumerState<_WifiPanel> { // oben ausgeloest, und das Feld wird synchron gesetzt, bevor der // Transport auf LinkState.connected wechselt (siehe UdpTransport.connect). final connectedSsid = ref.read(wifiTransportProvider).connectedSsid; - final rememberedSsid = connectionSettings?.connectionWifiLastSsid; // Controller einmalig aus den gespeicherten Einstellungen befuellen - // nicht bei jedem Rebuild, sonst wird waehrend der Nutzereingabe @@ -163,7 +162,6 @@ class _WifiPanelState extends ConsumerState<_WifiPanel> { if (!_controllersSeeded && connectionSettings != null) { _controllersSeeded = true; _ssidPrefixController.text = connectionSettings.connectionWifiSsidPrefix; - _hostController.text = connectionSettings.connectionWifiHost ?? ''; _portController.text = connectionSettings.connectionWifiPort.toString(); } @@ -172,50 +170,36 @@ class _WifiPanelState extends ConsumerState<_WifiPanel> { children: [ Row( children: [ - Text('WiFi (UDP)', style: Theme.of(context).textTheme.titleSmall), + SizedBox( + width: 130, + child: DropdownButtonFormField( + initialValue: ConnectionType.wifi, + decoration: const InputDecoration(isDense: true, border: OutlineInputBorder()), + items: _connectionTypeItems, + onChanged: (type) async { + if (type == null) return; + await ref.read(appSettingsRepositoryProvider).setConnectionType(type); + ref.invalidate(connectionSettingsProvider); + }, + ), + ), + const SizedBox(width: 8), + _ConnectToggleButton( + state: linkState, + onConnect: () => _connect(ref), + onDisconnect: () => ref.read(wifiTransportProvider).disconnect(), + ), const Spacer(), _StreamActivityPill(isReceiving: isReceiving), const SizedBox(width: 6), - _StatePill(state: linkState), + _StatePill(state: linkState, connectedLabel: connectedSsid), ], ), - const SizedBox(height: 4), - const Text( - 'The app connects to the mLRS bridge WiFi itself (a system dialog ' - 'will ask you to confirm) so your normal mobile data connection ' - 'keeps working for maps and weather. The peer address is learned ' - 'from the first incoming packet.', - style: TextStyle(fontSize: 12, color: Colors.grey), - ), - const SizedBox(height: 4), - const Text( - 'Do not manually connect to or save the mLRS network in your ' - 'system WiFi settings - that would make it the default route and ' - 'break maps/weather.', - style: TextStyle(fontSize: 12, color: Colors.grey), - ), - const SizedBox(height: 8), - if (connectedSsid != null) - Text( - 'Connected to: $connectedSsid', - style: const TextStyle(fontSize: 12, color: DmcColors.liveGreen), - ) - else if (rememberedSsid != null) - Text( - 'Remembered network: $rememberedSsid', - style: const TextStyle(fontSize: 12, color: Colors.grey), - ) - else - const Text( - 'No network remembered yet - connect once to remember it for ' - 'automatic reconnection.', - style: TextStyle(fontSize: 12, color: Colors.grey), - ), if (errorReason != null) ...[ const SizedBox(height: 12), _WifiErrorHint(reason: errorReason), ], - const SizedBox(height: 16), + const SizedBox(height: 12), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -250,38 +234,11 @@ class _WifiPanelState extends ConsumerState<_WifiPanel> { ), ], ), - const SizedBox(height: 8), - TextField( - controller: _hostController, - decoration: const InputDecoration( - labelText: 'Host (optional)', - hintText: 'auto (network gateway, then learned from traffic)', - isDense: true, - border: OutlineInputBorder(), - ), - onSubmitted: (value) => _saveHost(value), - onEditingComplete: () => _saveHost(_hostController.text), - ), - const SizedBox(height: 16), - Row( - children: [ - ElevatedButton( - onPressed: () => _connect(ref), - child: const Text('Connect'), - ), - const SizedBox(width: 8), - OutlinedButton( - onPressed: () => ref.read(wifiTransportProvider).disconnect(), - child: const Text('Disconnect'), - ), - ], - ), - const SizedBox(height: 8), SwitchListTile( contentPadding: EdgeInsets.zero, title: const Text('Auto-connect when entering Fly mode'), subtitle: const Text( - 'Only once a network has been remembered (see above).', + 'Only once a network has been remembered.', style: TextStyle(fontSize: 11), ), value: connectionSettings?.connectionAutoConnect ?? true, @@ -292,7 +249,7 @@ class _WifiPanelState extends ConsumerState<_WifiPanel> { ref.invalidate(connectionSettingsProvider); }, ), - const SizedBox(height: 16), + const SizedBox(height: 8), Text( 'Connection log ($packetCount packets received)', style: Theme.of(context).textTheme.titleSmall, @@ -330,14 +287,6 @@ class _WifiPanelState extends ConsumerState<_WifiPanel> { ref.invalidate(connectionSettingsProvider); } - Future _saveHost(String value) async { - final trimmed = value.trim(); - await ref - .read(appSettingsRepositoryProvider) - .setConnectionWifiHost(trimmed.isEmpty ? null : trimmed); - ref.invalidate(connectionSettingsProvider); - } - Future _savePort(String value) async { final port = int.tryParse(value); if (port == null || port <= 0 || port > 65535) return; @@ -358,6 +307,30 @@ class _WifiPanelState extends ConsumerState<_WifiPanel> { '${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}:${t.second.toString().padLeft(2, '0')}'; } +/// Runder Connect/Disconnect-Umschalter fuer die kompakte erste Zeile des +/// WLAN-Panels (Doku: "kompakter gestalten") - ein Knopf statt zwei, der je +/// nach [state] Beschriftung und Aktion wechselt. +class _ConnectToggleButton extends StatelessWidget { + const _ConnectToggleButton({ + required this.state, + required this.onConnect, + required this.onDisconnect, + }); + + final LinkState? state; + final VoidCallback onConnect; + final VoidCallback onDisconnect; + + @override + Widget build(BuildContext context) { + final isActive = + state == LinkState.connected || state == LinkState.connecting; + return isActive + ? OutlinedButton(onPressed: onDisconnect, child: const Text('Disconnect')) + : ElevatedButton(onPressed: onConnect, child: const Text('Connect')); + } +} + class _WifiErrorHint extends StatelessWidget { const _WifiErrorHint({required this.reason}); @@ -570,14 +543,19 @@ class _StreamActivityPill extends StatelessWidget { } class _StatePill extends StatelessWidget { - const _StatePill({required this.state}); + const _StatePill({required this.state, this.connectedLabel}); final LinkState? state; + /// Ueberschreibt die Beschriftung im verbundenen Zustand - z.B. der + /// WLAN-Netzname statt des generischen "Connected" (Doku: "die connected + /// pill soll den WiFi-Namen anzeigen"). + final String? connectedLabel; + @override Widget build(BuildContext context) { final (label, color) = switch (state) { - LinkState.connected => ('Connected', DmcColors.liveGreen), + LinkState.connected => (connectedLabel ?? 'Connected', DmcColors.liveGreen), LinkState.connecting => ('Connecting…', DmcColors.pafAmber), LinkState.error => ('Error', DmcColors.warnRed), LinkState.disconnected || null => ('Disconnected', Colors.grey),