Some checks failed
Release / meta (push) Successful in 14s
Release / web-build (push) Has been cancelled
Release / release-dev (push) Has been cancelled
Release / release-master (push) Has been cancelled
Release / linux-build (push) Has been cancelled
Release / android-build (push) Has been cancelled
212 lines
6.2 KiB
Dart
212 lines
6.2 KiB
Dart
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) {
|
|
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;
|
|
}
|
|
}
|