| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282 |
- 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<ObdSettingsCard> createState() => _ObdSettingsCardState();
- }
- class _ObdSettingsCardState extends State<ObdSettingsCard>
- with WidgetsBindingObserver {
- bool _showAllDevices = false;
- @override
- void initState() {
- super.initState();
- WidgetsBinding.instance.addObserver(this);
- WidgetsBinding.instance.addPostFrameCallback((_) {
- if (!mounted) return;
- unawaited(
- context.read<ObdController>().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<ObdController>().refreshDevices(dueToResume: true));
- }
- @override
- Widget build(BuildContext context) {
- final colors = context.colors;
- final theme = Theme.of(context);
- final obd = context.watch<ObdController>();
- final selected = obd.selectedAddress;
- final options = <BluetoothDeviceInfo>[
- ...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<String?>(
- 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<String?>(value: null, child: Text('None')),
- for (final device in options)
- DropdownMenuItem<String?>(
- 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)));
- }
- }
|