Add ability to select distance unit
Some checks failed
Some checks failed
This commit is contained in:
211
lib/services/distance_unit_service.dart
Normal file
211
lib/services/distance_unit_service.dart
Normal file
@@ -0,0 +1,211 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
enum DistanceUnit {
|
||||
milesDecimal,
|
||||
milesChains,
|
||||
kilometers,
|
||||
}
|
||||
|
||||
extension DistanceUnitLabels on DistanceUnit {
|
||||
String get label {
|
||||
switch (this) {
|
||||
case DistanceUnit.milesDecimal:
|
||||
return 'Miles (decimal)';
|
||||
case DistanceUnit.milesChains:
|
||||
return 'Miles & chains';
|
||||
case DistanceUnit.kilometers:
|
||||
return 'Kilometers';
|
||||
}
|
||||
}
|
||||
|
||||
String get shortLabel {
|
||||
switch (this) {
|
||||
case DistanceUnit.milesDecimal:
|
||||
return 'mi';
|
||||
case DistanceUnit.milesChains:
|
||||
return 'm.ch';
|
||||
case DistanceUnit.kilometers:
|
||||
return 'km';
|
||||
}
|
||||
}
|
||||
|
||||
String get _prefsValue => toString().split('.').last;
|
||||
|
||||
static DistanceUnit fromPrefs(String raw) {
|
||||
return DistanceUnit.values.firstWhere(
|
||||
(u) => u._prefsValue == raw,
|
||||
orElse: () => DistanceUnit.milesDecimal,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DistanceUnitService extends ChangeNotifier {
|
||||
static const _prefsKey = 'distance_unit';
|
||||
static const double kmPerMile = 1.609344;
|
||||
static const double chainsPerMile = 80.0;
|
||||
|
||||
DistanceUnitService() {
|
||||
_load();
|
||||
}
|
||||
|
||||
DistanceUnit _unit = DistanceUnit.milesDecimal;
|
||||
bool _loaded = false;
|
||||
|
||||
DistanceUnit get unit => _unit;
|
||||
bool get isLoaded => _loaded;
|
||||
|
||||
Future<void> _load() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final saved = prefs.getString(_prefsKey);
|
||||
if (saved != null && saved.trim().isNotEmpty) {
|
||||
_unit = DistanceUnitLabels.fromPrefs(saved.trim());
|
||||
}
|
||||
_loaded = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setUnit(DistanceUnit unit) async {
|
||||
_unit = unit;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_prefsKey, unit._prefsValue);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
double? milesFromInput(String input) =>
|
||||
DistanceFormatter(_unit).parseInputMiles(input);
|
||||
|
||||
String format(double miles,
|
||||
{int decimals = 1, bool includeUnit = true}) =>
|
||||
DistanceFormatter(_unit)
|
||||
.format(miles, decimals: decimals, includeUnit: includeUnit);
|
||||
|
||||
double toDisplay(double miles, {int decimals = 1}) =>
|
||||
DistanceFormatter(_unit).convertMiles(miles, decimals: decimals);
|
||||
}
|
||||
|
||||
class DistanceFormatter {
|
||||
DistanceFormatter(this.unit);
|
||||
|
||||
final DistanceUnit unit;
|
||||
|
||||
String format(double miles,
|
||||
{int decimals = 1, bool includeUnit = true}) {
|
||||
decimals = decimals.clamp(1, 2);
|
||||
if (unit == DistanceUnit.milesChains) {
|
||||
// Always show chains with two decimals.
|
||||
decimals = 2;
|
||||
}
|
||||
switch (unit) {
|
||||
case DistanceUnit.milesDecimal:
|
||||
final value = _numberFormat(decimals).format(miles);
|
||||
return includeUnit ? '$value mi' : value;
|
||||
case DistanceUnit.kilometers:
|
||||
final kms = miles * DistanceUnitService.kmPerMile;
|
||||
final value = _numberFormat(decimals).format(kms);
|
||||
return includeUnit ? '$value km' : value;
|
||||
case DistanceUnit.milesChains:
|
||||
final value = _formatMilesChains(miles);
|
||||
return includeUnit ? '$value mi' : value;
|
||||
}
|
||||
}
|
||||
|
||||
double convertMiles(double miles, {int decimals = 1}) {
|
||||
decimals = decimals.clamp(1, 2);
|
||||
switch (unit) {
|
||||
case DistanceUnit.milesDecimal:
|
||||
return double.parse(miles.toStringAsFixed(decimals));
|
||||
case DistanceUnit.kilometers:
|
||||
final kms = miles * DistanceUnitService.kmPerMile;
|
||||
return double.parse(kms.toStringAsFixed(decimals));
|
||||
case DistanceUnit.milesChains:
|
||||
// Return miles again; chains representation handled by format.
|
||||
return double.parse(miles.toStringAsFixed(decimals));
|
||||
}
|
||||
}
|
||||
|
||||
double milesFromDisplayValue(double value) {
|
||||
switch (unit) {
|
||||
case DistanceUnit.milesDecimal:
|
||||
return value;
|
||||
case DistanceUnit.kilometers:
|
||||
return value / DistanceUnitService.kmPerMile;
|
||||
case DistanceUnit.milesChains:
|
||||
// Value already represents miles when parsed via parseMilesChains.
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
double? parseInputMiles(String input) {
|
||||
final trimmed = input.trim();
|
||||
if (trimmed.isEmpty) return null;
|
||||
switch (unit) {
|
||||
case DistanceUnit.milesDecimal:
|
||||
return double.tryParse(trimmed.replaceAll(',', ''));
|
||||
case DistanceUnit.kilometers:
|
||||
final km = double.tryParse(trimmed.replaceAll(',', ''));
|
||||
if (km == null) return null;
|
||||
return km / DistanceUnitService.kmPerMile;
|
||||
case DistanceUnit.milesChains:
|
||||
return _parseMilesChains(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
NumberFormat _numberFormat(int decimals) {
|
||||
final pattern =
|
||||
decimals == 1 ? '#,##0.0' : '#,##0.00';
|
||||
return NumberFormat(pattern);
|
||||
}
|
||||
|
||||
String _formatMilesChains(double miles, {int decimals = 1}) {
|
||||
final totalChains = miles * DistanceUnitService.chainsPerMile;
|
||||
var milesPart = totalChains ~/ DistanceUnitService.chainsPerMile;
|
||||
final chainRemainder =
|
||||
totalChains - (milesPart * DistanceUnitService.chainsPerMile);
|
||||
|
||||
// Always show chains as two digits (00-79), rounded to the nearest chain.
|
||||
var roundedChains = chainRemainder.roundToDouble();
|
||||
if (roundedChains >= DistanceUnitService.chainsPerMile) {
|
||||
milesPart += 1;
|
||||
roundedChains -= DistanceUnitService.chainsPerMile;
|
||||
}
|
||||
final chainText = NumberFormat('00').format(roundedChains);
|
||||
return '$milesPart.$chainText';
|
||||
}
|
||||
|
||||
double? _parseMilesChains(String raw) {
|
||||
final cleaned = raw
|
||||
.toLowerCase()
|
||||
.replaceAll(',', '')
|
||||
.replaceAll('m', '.')
|
||||
.replaceAll('c', '.')
|
||||
.replaceAll(RegExp(r'\s+'), '.')
|
||||
.replaceAll(RegExp(r'\.+'), '.')
|
||||
.trim();
|
||||
if (cleaned.isEmpty) return null;
|
||||
|
||||
final parts = cleaned.split('.');
|
||||
if (parts.isEmpty) return null;
|
||||
|
||||
final milesPart =
|
||||
int.tryParse(parts[0].isEmpty ? '0' : parts[0]);
|
||||
if (milesPart == null) return null;
|
||||
double chainsPart = 0;
|
||||
if (parts.length >= 2) {
|
||||
final chainRaw = parts
|
||||
.sublist(1)
|
||||
.join()
|
||||
.trim();
|
||||
if (chainRaw.isNotEmpty) {
|
||||
final parsedChains = double.tryParse(chainRaw);
|
||||
if (parsedChains == null) return null;
|
||||
chainsPart = parsedChains;
|
||||
}
|
||||
}
|
||||
final totalMiles =
|
||||
milesPart +
|
||||
(chainsPart / DistanceUnitService.chainsPerMile);
|
||||
return totalMiles;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user