settings ui clean up

This commit is contained in:
Constantin Leue
2026-08-02 08:45:24 +02:00
parent 607dbb6584
commit 24db38dc33
+87 -109
View File
@@ -53,37 +53,29 @@ class _ConnectionSection extends ConsumerWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('Connection', style: Theme.of(context).textTheme.titleMedium), // Fuer WLAN rendert _WifiPanel das Typ-Dropdown selbst als Teil
const SizedBox(height: 12), // seiner kompakten ersten Zeile (zusammen mit Connect/Disconnect
DropdownButtonFormField<ConnectionType>( // und den Status-Pillen) - fuer die anderen Verbindungsarten
initialValue: selectedType, // bleibt die bisherige, einfachere Kopfzeile bestehen.
decoration: const InputDecoration( if (selectedType != ConnectionType.wifi) ...[
labelText: 'Type', Text('Connection', style: Theme.of(context).textTheme.titleMedium),
isDense: true, const SizedBox(height: 12),
border: OutlineInputBorder(), DropdownButtonFormField<ConnectionType>(
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 [ const SizedBox(height: 16),
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),
switch (selectedType) { switch (selectedType) {
ConnectionType.wifi => const _WifiPanel(), ConnectionType.wifi => const _WifiPanel(),
ConnectionType.bluetooth => const _BluetoothPanel(), 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 { class _NotAvailablePanel extends StatelessWidget {
const _NotAvailablePanel({required this.label}); const _NotAvailablePanel({required this.label});
@@ -122,7 +125,6 @@ class _WifiPanel extends ConsumerStatefulWidget {
class _WifiPanelState extends ConsumerState<_WifiPanel> { class _WifiPanelState extends ConsumerState<_WifiPanel> {
late final TextEditingController _ssidPrefixController; late final TextEditingController _ssidPrefixController;
late final TextEditingController _hostController;
late final TextEditingController _portController; late final TextEditingController _portController;
bool _controllersSeeded = false; bool _controllersSeeded = false;
@@ -130,14 +132,12 @@ class _WifiPanelState extends ConsumerState<_WifiPanel> {
void initState() { void initState() {
super.initState(); super.initState();
_ssidPrefixController = TextEditingController(); _ssidPrefixController = TextEditingController();
_hostController = TextEditingController();
_portController = TextEditingController(); _portController = TextEditingController();
} }
@override @override
void dispose() { void dispose() {
_ssidPrefixController.dispose(); _ssidPrefixController.dispose();
_hostController.dispose();
_portController.dispose(); _portController.dispose();
super.dispose(); super.dispose();
} }
@@ -155,7 +155,6 @@ class _WifiPanelState extends ConsumerState<_WifiPanel> {
// oben ausgeloest, und das Feld wird synchron gesetzt, bevor der // oben ausgeloest, und das Feld wird synchron gesetzt, bevor der
// Transport auf LinkState.connected wechselt (siehe UdpTransport.connect). // Transport auf LinkState.connected wechselt (siehe UdpTransport.connect).
final connectedSsid = ref.read(wifiTransportProvider).connectedSsid; final connectedSsid = ref.read(wifiTransportProvider).connectedSsid;
final rememberedSsid = connectionSettings?.connectionWifiLastSsid;
// Controller einmalig aus den gespeicherten Einstellungen befuellen - // Controller einmalig aus den gespeicherten Einstellungen befuellen -
// nicht bei jedem Rebuild, sonst wird waehrend der Nutzereingabe // nicht bei jedem Rebuild, sonst wird waehrend der Nutzereingabe
@@ -163,7 +162,6 @@ class _WifiPanelState extends ConsumerState<_WifiPanel> {
if (!_controllersSeeded && connectionSettings != null) { if (!_controllersSeeded && connectionSettings != null) {
_controllersSeeded = true; _controllersSeeded = true;
_ssidPrefixController.text = connectionSettings.connectionWifiSsidPrefix; _ssidPrefixController.text = connectionSettings.connectionWifiSsidPrefix;
_hostController.text = connectionSettings.connectionWifiHost ?? '';
_portController.text = connectionSettings.connectionWifiPort.toString(); _portController.text = connectionSettings.connectionWifiPort.toString();
} }
@@ -172,50 +170,36 @@ class _WifiPanelState extends ConsumerState<_WifiPanel> {
children: [ children: [
Row( Row(
children: [ children: [
Text('WiFi (UDP)', style: Theme.of(context).textTheme.titleSmall), SizedBox(
width: 130,
child: DropdownButtonFormField<ConnectionType>(
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(), const Spacer(),
_StreamActivityPill(isReceiving: isReceiving), _StreamActivityPill(isReceiving: isReceiving),
const SizedBox(width: 6), 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) ...[ if (errorReason != null) ...[
const SizedBox(height: 12), const SizedBox(height: 12),
_WifiErrorHint(reason: errorReason), _WifiErrorHint(reason: errorReason),
], ],
const SizedBox(height: 16), const SizedBox(height: 12),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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( SwitchListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: const Text('Auto-connect when entering Fly mode'), title: const Text('Auto-connect when entering Fly mode'),
subtitle: const Text( subtitle: const Text(
'Only once a network has been remembered (see above).', 'Only once a network has been remembered.',
style: TextStyle(fontSize: 11), style: TextStyle(fontSize: 11),
), ),
value: connectionSettings?.connectionAutoConnect ?? true, value: connectionSettings?.connectionAutoConnect ?? true,
@@ -292,7 +249,7 @@ class _WifiPanelState extends ConsumerState<_WifiPanel> {
ref.invalidate(connectionSettingsProvider); ref.invalidate(connectionSettingsProvider);
}, },
), ),
const SizedBox(height: 16), const SizedBox(height: 8),
Text( Text(
'Connection log ($packetCount packets received)', 'Connection log ($packetCount packets received)',
style: Theme.of(context).textTheme.titleSmall, style: Theme.of(context).textTheme.titleSmall,
@@ -330,14 +287,6 @@ class _WifiPanelState extends ConsumerState<_WifiPanel> {
ref.invalidate(connectionSettingsProvider); ref.invalidate(connectionSettingsProvider);
} }
Future<void> _saveHost(String value) async {
final trimmed = value.trim();
await ref
.read(appSettingsRepositoryProvider)
.setConnectionWifiHost(trimmed.isEmpty ? null : trimmed);
ref.invalidate(connectionSettingsProvider);
}
Future<void> _savePort(String value) async { Future<void> _savePort(String value) async {
final port = int.tryParse(value); final port = int.tryParse(value);
if (port == null || port <= 0 || port > 65535) return; 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')}'; '${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 { class _WifiErrorHint extends StatelessWidget {
const _WifiErrorHint({required this.reason}); const _WifiErrorHint({required this.reason});
@@ -570,14 +543,19 @@ class _StreamActivityPill extends StatelessWidget {
} }
class _StatePill extends StatelessWidget { class _StatePill extends StatelessWidget {
const _StatePill({required this.state}); const _StatePill({required this.state, this.connectedLabel});
final LinkState? state; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final (label, color) = switch (state) { final (label, color) = switch (state) {
LinkState.connected => ('Connected', DmcColors.liveGreen), LinkState.connected => (connectedLabel ?? 'Connected', DmcColors.liveGreen),
LinkState.connecting => ('Connecting…', DmcColors.pafAmber), LinkState.connecting => ('Connecting…', DmcColors.pafAmber),
LinkState.error => ('Error', DmcColors.warnRed), LinkState.error => ('Error', DmcColors.warnRed),
LinkState.disconnected || null => ('Disconnected', Colors.grey), LinkState.disconnected || null => ('Disconnected', Colors.grey),