| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295 |
- 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 '../home/obd_controller.dart';
- import '../home/weather_controller.dart';
- /// Settings card for the optional ELM327 adapter and location weather.
- class CarDataSection extends StatefulWidget {
- const CarDataSection({super.key});
- @override
- State<CarDataSection> createState() => _CarDataSectionState();
- }
- class _CarDataSectionState extends State<CarDataSection>
- with WidgetsBindingObserver {
- @override
- void initState() {
- super.initState();
- WidgetsBinding.instance.addObserver(this);
- WidgetsBinding.instance.addPostFrameCallback((_) {
- if (!mounted) return;
- unawaited(
- context.read<ObdController>().refreshDevices(requestPermission: false),
- );
- });
- }
- @override
- void dispose() {
- WidgetsBinding.instance.removeObserver(this);
- super.dispose();
- }
- @override
- void didChangeAppLifecycleState(AppLifecycleState state) {
- if (state == AppLifecycleState.resumed) {
- unawaited(
- context.read<ObdController>().refreshDevices(requestPermission: false),
- );
- }
- }
- @override
- Widget build(BuildContext context) {
- final colors = context.colors;
- final theme = Theme.of(context);
- final obd = context.watch<ObdController>();
- final weather = context.watch<WeatherController>();
- final selected = obd.selectedAddress;
- final options = <BluetoothDeviceInfo>[...obd.devices];
- 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
- ? 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,
- );
- 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),
- 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: (value) async {
- await obd.selectAdapter(value);
- if (!context.mounted) return;
- if (obd.error != null) {
- _message(context, obd.error!);
- }
- },
- ),
- if (obd.devicesError != null && !obd.needsBluetoothPermission) ...[
- const SizedBox(height: 6),
- Text(
- obd.devicesError!,
- style: theme.textTheme.bodySmall?.copyWith(color: colors.danger),
- ),
- ],
- if (obd.bluetoothEnabled == false) ...[
- 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(
- '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),
- ),
- Divider(color: colors.border, height: 30),
- Row(
- children: [
- Container(
- width: 38,
- height: 38,
- decoration: BoxDecoration(
- color: colors.surfaceStrong,
- borderRadius: BorderRadius.circular(13),
- border: Border.all(color: colors.border),
- ),
- alignment: Alignment.center,
- child: AppIcon(
- 'assets/icons/cloud.svg',
- size: 18,
- color: colors.inkMuted,
- ),
- ),
- const SizedBox(width: 14),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- 'Location temperature',
- style: theme.textTheme.bodyLarge,
- ),
- const SizedBox(height: 2),
- Text(
- 'Show temperature from current location if no OBD-II connected.',
- style: theme.textTheme.bodySmall?.copyWith(
- color: colors.inkMuted,
- ),
- ),
- ],
- ),
- ),
- const SizedBox(width: 8),
- Switch(
- value: weather.enabled,
- onChanged: (value) async {
- final ok = await weather.setEnabled(value);
- if (!ok && context.mounted) {
- _message(
- context,
- weather.error ??
- 'Location permission is required for weather.',
- );
- }
- },
- thumbColor: WidgetStateProperty.resolveWith<Color?>(
- (states) => states.contains(WidgetState.selected)
- ? colors.accentInk
- : colors.inkFaint,
- ),
- trackColor: WidgetStateProperty.resolveWith<Color?>(
- (states) => states.contains(WidgetState.selected)
- ? colors.accent
- : colors.surfaceStrong,
- ),
- trackOutlineColor: WidgetStatePropertyAll(colors.border),
- ),
- ],
- ),
- if (weather.enabled && weather.error != null) ...[
- const SizedBox(height: 4),
- Text(
- weather.error!,
- 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)));
- }
- }
|