Add ability to select distance unit
Some checks failed
Some checks failed
This commit is contained in:
@@ -11,6 +11,7 @@ import 'package:mileograph_flutter/components/pages/traction.dart';
|
||||
import 'package:mileograph_flutter/objects/objects.dart';
|
||||
import 'package:mileograph_flutter/services/api_service.dart';
|
||||
import 'package:mileograph_flutter/services/data_service.dart';
|
||||
import 'package:mileograph_flutter/services/distance_unit_service.dart';
|
||||
import 'package:mileograph_flutter/services/navigation_guard.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
@@ -235,6 +235,7 @@ extension _NewEntryDraftLogic on _NewEntryPageState {
|
||||
required String id,
|
||||
bool includeTimestamp = true,
|
||||
}) {
|
||||
final units = _distanceUnits(context);
|
||||
final routeStations = _routeResult?.calculatedRoute ?? [];
|
||||
final endTime = _legEndDateTime;
|
||||
final originTime = _originDateTime;
|
||||
@@ -249,7 +250,7 @@ extension _NewEntryDraftLogic on _NewEntryPageState {
|
||||
? _endController.text.trim()
|
||||
: (routeStations.isNotEmpty ? routeStations.last : '');
|
||||
final mileageVal = _useManualMileage
|
||||
? double.tryParse(_mileageController.text.trim()) ?? 0
|
||||
? (units.milesFromInput(_mileageController.text.trim()) ?? 0)
|
||||
: (_routeResult?.distance ?? 0);
|
||||
final tractionPayload = _buildTractionPayload();
|
||||
final commonPayload = {
|
||||
@@ -338,6 +339,7 @@ extension _NewEntryDraftLogic on _NewEntryPageState {
|
||||
final destination = payload['leg_destination'] as String? ?? '';
|
||||
final tripRaw = payload['leg_trip'];
|
||||
final tripId = tripRaw is num ? tripRaw.toInt() : null;
|
||||
final units = _distanceUnits(context);
|
||||
|
||||
List<String> routeStations = [];
|
||||
RouteResult? restoredRouteResult;
|
||||
@@ -416,14 +418,20 @@ extension _NewEntryDraftLogic on _NewEntryPageState {
|
||||
final miles = (payload['leg_distance'] as num?)?.toDouble();
|
||||
_mileageController.text = miles == null || miles == 0
|
||||
? ''
|
||||
: miles.toStringAsFixed(2);
|
||||
: units.format(
|
||||
miles,
|
||||
decimals: 2,
|
||||
includeUnit: false,
|
||||
);
|
||||
} else {
|
||||
_startController.text =
|
||||
routeStations.isNotEmpty ? routeStations.first : '';
|
||||
_endController.text =
|
||||
routeStations.isNotEmpty ? routeStations.last : '';
|
||||
final dist = _routeResult?.distance ?? 0;
|
||||
_mileageController.text = dist == 0 ? '' : dist.toStringAsFixed(2);
|
||||
_mileageController.text = dist == 0
|
||||
? ''
|
||||
: units.format(dist, decimals: 2, includeUnit: false);
|
||||
}
|
||||
|
||||
final tractionRaw = data['tractionItems'];
|
||||
|
||||
@@ -150,6 +150,7 @@ class _DraftListBodyState extends State<_DraftListBody> {
|
||||
final payload = draft.data['payload'];
|
||||
if (payload is! Map) return '';
|
||||
final map = Map<String, dynamic>.from(payload);
|
||||
final units = context.read<DistanceUnitService>();
|
||||
final parts = <String>[];
|
||||
if ((map['leg_trip'] as int? ?? 0) != 0) {
|
||||
parts.add('Trip ${map['leg_trip']}');
|
||||
@@ -164,7 +165,7 @@ class _DraftListBodyState extends State<_DraftListBody> {
|
||||
(map['leg_distance'] as num?)?.toDouble() ??
|
||||
(map['leg_mileage'] as num?)?.toDouble();
|
||||
if (mileage != null && mileage > 0) {
|
||||
parts.add('${mileage.toStringAsFixed(1)} mi');
|
||||
parts.add(units.format(mileage, decimals: 1));
|
||||
} else if (map['leg_route'] is List &&
|
||||
(map['leg_route'] as List).isNotEmpty) {
|
||||
parts.add('Route ${(map['leg_route'] as List).length} stops');
|
||||
@@ -176,4 +177,3 @@ class _DraftListBodyState extends State<_DraftListBody> {
|
||||
return parts.join(' • ');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
final DeepCollectionEquality _snapshotEquality =
|
||||
const DeepCollectionEquality();
|
||||
String? _activeDraftId;
|
||||
DistanceUnit? _lastDistanceUnit;
|
||||
|
||||
bool get _isEditing => widget.editLegId != null;
|
||||
bool get _draftPersistenceEnabled =>
|
||||
@@ -100,6 +101,58 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
setState(fn);
|
||||
}
|
||||
|
||||
DistanceUnitService _distanceUnits(BuildContext context) =>
|
||||
context.read<DistanceUnitService>();
|
||||
|
||||
String _formatDistance(
|
||||
DistanceUnitService units,
|
||||
double miles, {
|
||||
int decimals = 1,
|
||||
bool includeUnit = true,
|
||||
}) {
|
||||
return units.format(
|
||||
miles,
|
||||
decimals: decimals,
|
||||
includeUnit: includeUnit,
|
||||
);
|
||||
}
|
||||
|
||||
String _manualMileageLabel(DistanceUnit unit) {
|
||||
switch (unit) {
|
||||
case DistanceUnit.milesDecimal:
|
||||
return 'Mileage (mi)';
|
||||
case DistanceUnit.kilometers:
|
||||
return 'Mileage (km)';
|
||||
case DistanceUnit.milesChains:
|
||||
return 'Mileage (m.ch)';
|
||||
}
|
||||
}
|
||||
|
||||
double _manualMilesFromInput(DistanceUnitService units) {
|
||||
return units.milesFromInput(_mileageController.text) ?? 0;
|
||||
}
|
||||
|
||||
double _milesFromInputWithUnit(DistanceUnit unit) {
|
||||
return DistanceFormatter(unit)
|
||||
.parseInputMiles(_mileageController.text.trim()) ??
|
||||
0;
|
||||
}
|
||||
|
||||
void _syncManualFieldUnit(DistanceUnit currentUnit) {
|
||||
if (!_useManualMileage) {
|
||||
_lastDistanceUnit = currentUnit;
|
||||
return;
|
||||
}
|
||||
final previousUnit = _lastDistanceUnit ?? currentUnit;
|
||||
if (previousUnit == currentUnit) return;
|
||||
|
||||
final miles = _milesFromInputWithUnit(previousUnit);
|
||||
final nextText = DistanceFormatter(currentUnit)
|
||||
.format(miles, decimals: 2, includeUnit: false);
|
||||
_mileageController.text = nextText;
|
||||
_lastDistanceUnit = currentUnit;
|
||||
}
|
||||
|
||||
Widget _buildTripSelector(BuildContext context) {
|
||||
final trips = context.watch<DataService>().tripList;
|
||||
final sorted = [...trips]..sort((a, b) => b.tripId.compareTo(a.tripId));
|
||||
@@ -224,9 +277,15 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
),
|
||||
);
|
||||
if (result != null) {
|
||||
final units = _distanceUnits(context);
|
||||
setState(() {
|
||||
_routeResult = result;
|
||||
_mileageController.text = result.distance.toStringAsFixed(2);
|
||||
_mileageController.text = _formatDistance(
|
||||
units,
|
||||
result.distance,
|
||||
decimals: 2,
|
||||
includeUnit: false,
|
||||
);
|
||||
_useManualMileage = false;
|
||||
});
|
||||
_saveDraft();
|
||||
@@ -426,6 +485,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
final endTime = DateTime.tryParse(json['leg_end_time'] ?? '');
|
||||
final routeStations = _parseRouteStations(json['leg_route']);
|
||||
final mileageVal = (json['leg_mileage'] as num?)?.toDouble() ?? 0.0;
|
||||
final units = _distanceUnits(context);
|
||||
final useManual = routeStations.isEmpty;
|
||||
final routeResult = useManual
|
||||
? null
|
||||
@@ -484,7 +544,12 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
_endDelayController.text = endDelay.toString();
|
||||
_mileageController.text = mileageVal == 0
|
||||
? ''
|
||||
: mileageVal.toStringAsFixed(2);
|
||||
: _formatDistance(
|
||||
units,
|
||||
mileageVal,
|
||||
decimals: 2,
|
||||
includeUnit: false,
|
||||
);
|
||||
_tractionItems
|
||||
..clear()
|
||||
..addAll(tractionItems);
|
||||
@@ -728,11 +793,15 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
key: _formKey,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final distanceUnitService = context.watch<DistanceUnitService>();
|
||||
final currentDistanceUnit = distanceUnitService.unit;
|
||||
_syncManualFieldUnit(currentDistanceUnit);
|
||||
final twoCol = !isMobile && constraints.maxWidth > 1000;
|
||||
final tractionEmpty = _tractionItems.length == 1;
|
||||
final mileageEmpty = !_useManualMileage && _routeResult == null;
|
||||
final balancePanels = twoCol && tractionEmpty && mileageEmpty;
|
||||
final balancedHeight = balancePanels ? 165.0 : null;
|
||||
final mileageLabel = _manualMileageLabel(currentDistanceUnit);
|
||||
|
||||
final entryPanel = _section('Entry', [
|
||||
Row(
|
||||
@@ -948,9 +1017,13 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Mileage (mi)',
|
||||
border: OutlineInputBorder(),
|
||||
decoration: InputDecoration(
|
||||
labelText: mileageLabel,
|
||||
helperText: currentDistanceUnit ==
|
||||
DistanceUnit.milesChains
|
||||
? 'Enter as miles.chains (e.g., 12.40 for 12m 40c)'
|
||||
: null,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
)
|
||||
else if (_routeResult != null)
|
||||
@@ -958,7 +1031,11 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Calculated mileage'),
|
||||
subtitle: Text(
|
||||
'${_routeResult!.distance.toStringAsFixed(2)} mi',
|
||||
_formatDistance(
|
||||
distanceUnitService,
|
||||
_routeResult!.distance,
|
||||
decimals: 2,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
@@ -973,7 +1050,17 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
label: Text(_useManualMileage ? 'Manual' : 'Automatic'),
|
||||
selected: _useManualMileage,
|
||||
onSelected: (val) {
|
||||
setState(() => _useManualMileage = val);
|
||||
setState(() {
|
||||
_useManualMileage = val;
|
||||
if (val && _routeResult != null) {
|
||||
_mileageController.text = _formatDistance(
|
||||
distanceUnitService,
|
||||
_routeResult!.distance,
|
||||
decimals: 2,
|
||||
includeUnit: false,
|
||||
);
|
||||
}
|
||||
});
|
||||
_saveDraft();
|
||||
_scheduleMatchUpdate();
|
||||
},
|
||||
|
||||
@@ -4,11 +4,12 @@ extension _NewEntrySubmitLogic on _NewEntryPageState {
|
||||
Future<bool> _validateRequiredFields() async {
|
||||
final missing = <String>[];
|
||||
|
||||
final units = _distanceUnits(context);
|
||||
if (_useManualMileage) {
|
||||
if (_startController.text.trim().isEmpty) missing.add('From');
|
||||
if (_endController.text.trim().isEmpty) missing.add('To');
|
||||
final mileageText = _mileageController.text.trim();
|
||||
if (double.tryParse(mileageText) == null) {
|
||||
if (mileageText.isEmpty || units.milesFromInput(mileageText) == null) {
|
||||
missing.add('Mileage');
|
||||
}
|
||||
} else {
|
||||
@@ -58,8 +59,9 @@ extension _NewEntrySubmitLogic on _NewEntryPageState {
|
||||
final endVal = _useManualMileage
|
||||
? _endController.text.trim()
|
||||
: (routeStations.isNotEmpty ? routeStations.last : '');
|
||||
final units = _distanceUnits(context);
|
||||
final mileageVal = _useManualMileage
|
||||
? double.tryParse(_mileageController.text.trim()) ?? 0
|
||||
? (units.milesFromInput(_mileageController.text.trim()) ?? 0)
|
||||
: (_routeResult?.distance ?? 0);
|
||||
final tractionPayload = _buildTractionPayload();
|
||||
final endTime = _legEndDateTime;
|
||||
|
||||
Reference in New Issue
Block a user