settings ui clean up
This commit is contained in:
@@ -53,6 +53,11 @@ class _ConnectionSection extends ConsumerWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 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>(
|
||||
@@ -62,21 +67,7 @@ class _ConnectionSection extends ConsumerWidget {
|
||||
isDense: true,
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
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)'),
|
||||
),
|
||||
],
|
||||
items: _connectionTypeItems,
|
||||
onChanged: (type) async {
|
||||
if (type == null) return;
|
||||
await ref.read(appSettingsRepositoryProvider).setConnectionType(type);
|
||||
@@ -84,6 +75,7 @@ class _ConnectionSection extends ConsumerWidget {
|
||||
},
|
||||
),
|
||||
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),
|
||||
|
||||
Reference in New Issue
Block a user