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')), body: ListView( padding: const EdgeInsets.all(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('Verbindung', style: Theme.of(context).textTheme.titleMedium), const Spacer(), _StatePill(state: linkState), ], ), const SizedBox(height: 12), const Text('Art: Bluetooth'), const SizedBox(height: 4), const Text( 'WLAN und USB folgen spaeter (Doku Kommunikationsschicht 6/7).', 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('Verbinden'), ), const SizedBox(width: 8), OutlinedButton( onPressed: () => ref.read(bluetoothTransportProvider).disconnect(), child: const Text('Trennen'), ), const SizedBox(width: 8), TextButton( onPressed: BluetoothSystemSettings.openBluetoothSettings, child: const Text('Geraet koppeln'), ), ], ), const SizedBox(height: 16), SwitchListTile( contentPadding: EdgeInsets.zero, title: const Text('Beim Wechsel in Fly automatisch verbinden'), value: connectionSettings?.connectionAutoConnect ?? true, onChanged: (value) async { await ref .read(appSettingsRepositoryProvider) .setConnectionAutoConnect(value); ref.invalidate(connectionSettingsProvider); }, ), const SizedBox(height: 8), Text('Gekoppelte Geraete', style: Theme.of(context).textTheme.titleSmall), const SizedBox(height: 4), bondedDevices.when( data: (devices) => devices.isEmpty ? const Text( 'Keine gekoppelten Geraete. Ueber "Geraet koppeln" ' 'zunaechst einmalig ueber den Systemdialog koppeln.', 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( 'Gekoppelte Geraete konnten nicht gelesen werden: $e', style: const TextStyle(fontSize: 12, color: Colors.grey), ), ), const SizedBox(height: 16), Text( 'Verbindungsprotokoll ($packetCount Datenpakete empfangen)', style: Theme.of(context).textTheme.titleSmall, ), const SizedBox(height: 4), SizedBox( height: 120, child: log.isEmpty ? const Text( 'Noch keine Ereignisse.', 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 => ('Verbunden', DmcColors.liveGreen), LinkState.connecting => ('Verbinde ...', DmcColors.pafAmber), LinkState.error => ('Fehler', DmcColors.warnRed), LinkState.disconnected || null => ('Getrennt', 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 ist aus. Bitte in den Systemeinstellungen einschalten.', _HintAction('Einstellungen', BluetoothSystemSettings.openBluetoothSettings), ), LinkErrorReason.permissionDenied => ( '"Geraete in der Naehe"-Berechtigung wird benoetigt. Bitte erneut ' 'auf "Verbinden" tippen.', null, ), LinkErrorReason.permissionDeniedPermanently => ( '"Geraete in der Naehe"-Berechtigung wurde dauerhaft abgelehnt. ' 'Bitte in den App-Einstellungen manuell erteilen.', _HintAction('App-Einstellungen', BluetoothSystemSettings.openAppSettings), ), LinkErrorReason.deviceSelectionRequired => ( 'Kein gekoppeltes mLRS-Geraet gefunden. Bitte unten ein Geraet ' 'auswaehlen.', null, ), LinkErrorReason.connectionFailed => ( 'Verbindung zum Geraet ist fehlgeschlagen.', null, ), LinkErrorReason.unknown => ('Unbekannter Verbindungsfehler.', 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; }