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(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Connection', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 12),
DropdownButtonFormField<ConnectionType>(
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<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 [
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<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(),
_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<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 {
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),