import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../core/theme/app_colors.dart'; import '../../core/widgets/app_icon.dart'; import '../../data/models/bluetooth_device_info.dart'; import '../../data/models/bluetooth_power_state.dart'; import '../home/obd_controller.dart'; /// Settings card for the optional Bluetooth Classic ELM327 adapter only. class ObdSettingsCard extends StatefulWidget { const ObdSettingsCard({super.key}); @override State createState() => _ObdSettingsCardState(); } class _ObdSettingsCardState extends State with WidgetsBindingObserver { bool _showAllDevices = false; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; unawaited( context.read().refreshDevices( requestPermission: false, force: true, ), ); }); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state != AppLifecycleState.resumed || !mounted) return; // The controller performs a cheap power-state check. It only lists bonded // devices again when the cached list is missing, so returning from Android // Settings does not hit the Bluetooth stack with a bond query every time. unawaited(context.read().refreshDevices(dueToResume: true)); } @override Widget build(BuildContext context) { final colors = context.colors; final theme = Theme.of(context); final obd = context.watch(); final selected = obd.selectedAddress; final options = [ ...obd.obdDevices, if (_showAllDevices) ...obd.otherDevices, ]; if (selected != null && !options.any((d) => d.address == selected)) { options.add( BluetoothDeviceInfo(name: 'Saved adapter', address: selected), ); } final selectedInOptions = options.any((d) => d.address == selected); final dropdownValue = selectedInOptions ? selected : null; final statusColor = obd.connected ? colors.accent : obd.error != null || obd.bluetoothState == BluetoothPowerState.unknown ? colors.danger : colors.inkFaint; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 42, height: 42, decoration: BoxDecoration( color: colors.accentSoft, borderRadius: BorderRadius.circular(14), ), alignment: Alignment.center, child: AppIcon( 'assets/icons/car.svg', size: 20, color: colors.accent, ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('OBD-II adapter', style: theme.textTheme.titleMedium), const SizedBox(height: 2), Row( children: [ Container( width: 7, height: 7, decoration: BoxDecoration( color: statusColor, shape: BoxShape.circle, ), ), const SizedBox(width: 7), Expanded( child: Text( obd.statusLabel, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall?.copyWith( color: colors.inkMuted, ), ), ), ], ), ], ), ), TextButton.icon( onPressed: obd.loadingDevices ? null : () async { final ok = await obd.refreshDevices( requestPermission: true, force: true, ); if (!ok && context.mounted) { _message( context, obd.devicesError ?? 'Could not read paired Bluetooth devices.', ); } }, icon: AppIcon( 'assets/icons/search.svg', size: 16, color: colors.accent, ), label: Text(obd.needsBluetoothPermission ? 'Allow' : 'Find'), style: TextButton.styleFrom(foregroundColor: colors.accent), ), ], ), const SizedBox(height: 12), DropdownButtonFormField( key: ValueKey('${dropdownValue ?? 'none'}-$_showAllDevices'), initialValue: dropdownValue, isExpanded: true, dropdownColor: colors.surface, style: theme.textTheme.bodyLarge?.copyWith(color: colors.ink), decoration: const InputDecoration( labelText: 'Bluetooth device', isDense: true, ), items: [ const DropdownMenuItem(value: null, child: Text('None')), for (final device in options) DropdownMenuItem( value: device.address, child: Text( device.label, maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], onChanged: obd.loadingDevices ? null : (value) async { await obd.selectAdapter(value); if (!context.mounted) return; if (obd.error != null) { _message(context, obd.error!); } }, ), if (obd.otherDevices.isNotEmpty) ...[ const SizedBox(height: 2), TextButton( onPressed: () { setState(() => _showAllDevices = !_showAllDevices); }, style: TextButton.styleFrom( foregroundColor: colors.accent, padding: EdgeInsets.zero, alignment: Alignment.centerLeft, ), child: Text( _showAllDevices ? 'Show only OBD-II adapters' : 'Show all paired devices', ), ), if (_showAllDevices) Text( 'Phones, headsets and other devices usually cannot act as an ' 'OBD-II adapter.', style: theme.textTheme.bodySmall?.copyWith( color: colors.inkMuted, ), ), ], if (obd.canRetry) ...[ const SizedBox(height: 2), TextButton( onPressed: () async { await obd.retryConnection(); if (!context.mounted) return; if (obd.error != null) { _message(context, obd.error!); } }, style: TextButton.styleFrom( foregroundColor: colors.accent, padding: EdgeInsets.zero, alignment: Alignment.centerLeft, ), child: const Text('Retry connection'), ), ], if (obd.devicesError != null && !obd.needsBluetoothPermission && (obd.bluetoothState.isOn || obd.bluetoothState == BluetoothPowerState.unavailable)) ...[ const SizedBox(height: 6), Text( obd.devicesError!, style: theme.textTheme.bodySmall?.copyWith(color: colors.danger), ), ], if (obd.bluetoothOff) ...[ const SizedBox(height: 2), TextButton.icon( onPressed: () async { final opened = await obd.openBluetoothSettings(); if (!opened && context.mounted) { _message(context, 'Could not open Bluetooth settings.'); } }, icon: AppIcon( 'assets/icons/settings.svg', size: 16, color: colors.accent, ), label: const Text('Open Bluetooth settings'), style: TextButton.styleFrom( foregroundColor: colors.accent, padding: EdgeInsets.zero, alignment: Alignment.centerLeft, ), ), ], const SizedBox(height: 8), Text( 'Only Bluetooth Classic OBD-II adapters are listed. Pair the ELM327 ' 'in Android Bluetooth settings first. CarDash only reads standard ' 'OBD-II PIDs; it never writes.', style: theme.textTheme.bodySmall?.copyWith(color: colors.inkMuted), ), ], ); } void _message(BuildContext context, String message) { final messenger = ScaffoldMessenger.of(context); messenger.hideCurrentSnackBar(); messenger.showSnackBar(SnackBar(content: Text(message))); } }