summaryrefslogtreecommitdiff
path: root/lib/home.dart
blob: b9dce40e5bb05541904ad2123f66f2d8ec61ca71 (plain)
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:region_settings/region_settings.dart';
import 'package:speedometer/jotai.dart';

import 'main.dart';
import 'settings.dart';

extension on SpeedUnit {
  double fromMetersPerSecond(double metersPerSecond) => switch (this) {
    SpeedUnit.kilometersPerHour => metersPerSecond * 3.6,
    SpeedUnit.milesPerHour => metersPerSecond * 2.236936,
  };

  String get acronym => switch (this) {
    SpeedUnit.kilometersPerHour => 'kmph',
    SpeedUnit.milesPerHour => 'mph',
  };
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  StreamSubscription<Position>? _positionStream;

  // I was not able to get StreamBuilder or FutureBuilder to work correctly,
  // so these are stored manually.
  bool _usesMetric = true;
  double _speed = 0.0;
  double _speedAccuracy = 0.0;

  void _initPositionStream(LocationAccuracy locationAccuracy) {
    _positionStream =
        Geolocator.getPositionStream(
          locationSettings: LocationSettings(accuracy: locationAccuracy),
        ).listen((Position? position) {
          if (position != null) {
            setState(() {
              _speed = position.speed;
              _speedAccuracy = position.speedAccuracy;
            });
          }
        });
    _positionStream?.onError((error) {
      _positionStream?.cancel();
      Timer(Duration(seconds: 1), () => _initPositionStream(locationAccuracy));
    });
  }

  @override
  void initState() {
    super.initState();

    Geolocator.checkPermission()
        .then(
          (permission) => permission == LocationPermission.denied
              ? Geolocator.requestPermission()
              : permission,
        )
        .then((permission) {
          if (![
            LocationPermission.deniedForever,
            LocationPermission.denied,
          ].contains(permission)) {
            _initPositionStream(locationAccuracyObservable.value);
          }
        });

    locationAccuracyObservable.subscribe((locationAccuracy) {
      _positionStream?.cancel();
      _initPositionStream(locationAccuracy);
    });

    // It takes time to load this data in, but I've only noticed it in debug mode
    RegionSettings.getUsesMetricSystem().then(
      (usesMetricSystem) => setState(() => _usesMetric = usesMetricSystem),
    );
  }

  @override
  void dispose() {
    super.dispose();
    _positionStream?.cancel();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        actions: [
          // This button is big enough and descriptive enough to be easily
          // discoverable, but small enough to not get in the way.
          TextButton.icon(
            icon: Icon(Icons.settings),
            label: Text('Settings'),
            onPressed: () => Navigator.of(
              context,
            ).push(MaterialPageRoute(builder: (context) => SettingsPage())),
          ),
        ],
      ),
      body: Center(
        child: ObserverBuilder(
          observable: speedUnitsObservable,
          builder: (context, speedUnitsSetting, _) {
            final speedUnits =
                speedUnitsSetting ??
                (_usesMetric
                    ? SpeedUnit.kilometersPerHour
                    : SpeedUnit.milesPerHour);

            return Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                Flex(direction: Axis.horizontal),
                Text(
                  '${speedUnits.fromMetersPerSecond(_speed).round()} ${speedUnits.acronym}',
                  style: Theme.of(context).textTheme.displayLarge,
                ),
                ObserverBuilder(
                  observable: showMarginOfErrorObservable,
                  builder: (context, showMarginOfError, _) =>
                      showMarginOfError == true
                      ? Text(
                          '± ${speedUnits.fromMetersPerSecond(_speedAccuracy).round()} ${speedUnits.acronym}',
                          style: Theme.of(context).textTheme.displaySmall,
                        )
                      : SizedBox(),
                ),
              ],
            );
          },
        ),
      ),
    );
  }
}