import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../transport/bluetooth/bluetooth_device_info.dart'; import '../../../transport/bluetooth/bluetooth_system_settings.dart'; import '../../../transport/link_transport.dart'; import '../../dmc_colors.dart'; import '../../providers/bluetooth_connection_provider.dart'; import '../../providers/connection_settings_provider.dart'; import '../../providers/database_provider.dart'; /// Einstellungen: Drohnen-Profile (Doku 3.5/7.3, noch nicht hier /// angebunden - siehe DroneProfileEditor) und Verbindung (Doku /// Kommunikationsschicht Abschnitt 7). class SettingsScreen extends StatelessWidget { const SettingsScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Settings')), // Bewusst nicht symmetrisch: links den echten Geraete-Inset einhalten // (z.B. eine Frontkamera-Aussparung im Querformat), rechts bis an den // Bildschirmrand - anders als in der Fusszeile (BottomStatsBar) soll // hier keine gespiegelte/erzwungene Symmetrie zwischen beiden Seiten // entstehen. body: ListView( padding: EdgeInsets.only( left: 16 + MediaQuery.of(context).padding.left, top: 16, bottom: 16, ), children: const [_ConnectionSection()], ), ); } } class _ConnectionSection extends ConsumerWidget { const _ConnectionSection(); @override Widget build(BuildContext context, WidgetRef ref) { final linkState = ref.watch(linkStateProvider).value; final errorReason = ref.watch(linkErrorReasonProvider); final bondedDevices = ref.watch(bondedDevicesProvider); final connectionSettings = ref.watch(connectionSettingsProvider).value; final log = ref.watch(connectionLogProvider); final packetCount = ref.watch(connectionLogProvider.notifier).packetCount; return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text('Connection', style: Theme.of(context).textTheme.titleMedium), const Spacer(), _StatePill(state: linkState), ], ), const SizedBox(height: 12), const Text('Type: Bluetooth'), const SizedBox(height: 4), const Text( 'Wi-Fi and USB support are coming later.', style: TextStyle(fontSize: 12, color: Colors.grey), ), if (errorReason != null) ...[ const SizedBox(height: 12), _ErrorHint(reason: errorReason), ], const SizedBox(height: 16), Row( children: [ ElevatedButton( onPressed: () => _connect(ref), child: const Text('Connect'), ), const SizedBox(width: 8), OutlinedButton( onPressed: () => ref.read(bluetoothTransportProvider).disconnect(), child: const Text('Disconnect'), ), const SizedBox(width: 8), TextButton( onPressed: BluetoothSystemSettings.openBluetoothSettings, child: const Text('Pair device'), ), ], ), const SizedBox(height: 16), SwitchListTile( contentPadding: EdgeInsets.zero, title: const Text('Auto-connect when entering Fly mode'), value: connectionSettings?.connectionAutoConnect ?? true, onChanged: (value) async { await ref .read(appSettingsRepositoryProvider) .setConnectionAutoConnect(value); ref.invalidate(connectionSettingsProvider); }, ), const SizedBox(height: 8), Text('Paired devices', style: Theme.of(context).textTheme.titleSmall), const SizedBox(height: 4), bondedDevices.when( data: (devices) => devices.isEmpty ? const Text( 'No paired devices yet. Pair one once via the system ' 'dialog using "Pair device".', style: TextStyle(fontSize: 12, color: Colors.grey), ) : RadioGroup( groupValue: connectionSettings?.connectionDeviceAddress, onChanged: (address) => _selectDevice(ref, address, devices), child: Column( children: [ for (final device in devices) RadioListTile( contentPadding: EdgeInsets.zero, dense: true, value: device.address, title: Text(device.displayName), subtitle: Text( device.address, style: const TextStyle(fontSize: 11), ), ), ], ), ), loading: () => const Padding( padding: EdgeInsets.symmetric(vertical: 8), child: LinearProgressIndicator(), ), error: (e, _) => Text( 'Could not read paired devices: $e', style: const TextStyle(fontSize: 12, color: Colors.grey), ), ), const SizedBox(height: 16), Text( 'Connection log ($packetCount packets received)', style: Theme.of(context).textTheme.titleSmall, ), const SizedBox(height: 4), SizedBox( height: 120, child: log.isEmpty ? const Text( 'No events yet.', style: TextStyle(fontSize: 12, color: Colors.grey), ) : ListView.builder( reverse: true, itemCount: log.length, itemBuilder: (context, index) { final entry = log[log.length - 1 - index]; return Text( '${_formatTime(entry.timestamp)} ${entry.message}', style: const TextStyle(fontSize: 12, fontFamily: 'monospace'), ); }, ), ), ], ), ), ); } Future _connect(WidgetRef ref) async { try { await ref.read(bluetoothTransportProvider).connect(); } on LinkConnectException { // Grund/Nachricht sind bereits ueber lastErrorReason/state gemeldet // (siehe LinkConnectException-Doku) - die Oberflaeche reagiert darauf // ueber linkErrorReasonProvider/_ErrorHint, hier ist nichts weiter zu tun. } } String _formatTime(DateTime t) => '${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}:${t.second.toString().padLeft(2, '0')}'; Future _selectDevice( WidgetRef ref, String? address, List devices, ) async { if (address == null) return; await ref .read(appSettingsRepositoryProvider) .setConnectionDeviceAddress(address); ref.invalidate(connectionSettingsProvider); try { await ref.read(bluetoothTransportProvider).connect(); } on LinkConnectException { // siehe _connect() oben } } } class _StatePill extends StatelessWidget { const _StatePill({required this.state}); final LinkState? state; @override Widget build(BuildContext context) { final (label, color) = switch (state) { 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), decoration: BoxDecoration( color: color.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(12), border: Border.all(color: color), ), child: Text(label, style: TextStyle(color: color, fontSize: 12)), ); } } class _ErrorHint extends StatelessWidget { const _ErrorHint({required this.reason}); final LinkErrorReason reason; @override Widget build(BuildContext context) { final (text, action) = switch (reason) { LinkErrorReason.bluetoothOff => ( 'Bluetooth is off. Please enable it in system settings.', _HintAction('Settings', BluetoothSystemSettings.openBluetoothSettings), ), LinkErrorReason.permissionDenied => ( 'The "Nearby devices" permission is required. Please tap ' '"Connect" again.', null, ), LinkErrorReason.permissionDeniedPermanently => ( 'The "Nearby devices" permission was permanently denied. Please ' 'grant it manually in the app settings.', _HintAction('App settings', BluetoothSystemSettings.openAppSettings), ), LinkErrorReason.deviceSelectionRequired => ( 'No paired mLRS device found. Please select a device below.', null, ), LinkErrorReason.connectionFailed => ( 'Connection to the device failed.', null, ), LinkErrorReason.unknown => ('Unknown connection error.', null), }; return Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: DmcColors.warnRed.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Expanded(child: Text(text, style: const TextStyle(fontSize: 12))), if (action != null) TextButton(onPressed: action.onTap, child: Text(action.label)), ], ), ); } } class _HintAction { const _HintAction(this.label, this.onTap); final String label; final Future Function() onTap; }