diff --git a/lib/components/calculator/calculator.dart b/lib/components/calculator/calculator.dart index 3c7fffc..fda03e0 100644 --- a/lib/components/calculator/calculator.dart +++ b/lib/components/calculator/calculator.dart @@ -97,10 +97,16 @@ class _StationAutocompleteState extends State { } class RouteCalculator extends StatefulWidget { - const RouteCalculator({super.key, this.onDistanceComputed, this.onApplyRoute}); + const RouteCalculator({ + super.key, + this.onDistanceComputed, + this.onApplyRoute, + this.initialStations, + }); final ValueChanged? onDistanceComputed; final ValueChanged? onApplyRoute; + final List? initialStations; @override State createState() => _RouteCalculatorState(); @@ -122,6 +128,9 @@ class _RouteCalculatorState extends State { super.didChangeDependencies(); if (!_fetched) { _fetched = true; + if (widget.initialStations != null && widget.initialStations!.isNotEmpty) { + context.read().stations = List.from(widget.initialStations!); + } WidgetsBinding.instance.addPostFrameCallback((_) async { final data = context.read(); final result = await data.fetchStations(); diff --git a/lib/components/pages/new_entry.dart b/lib/components/pages/new_entry.dart index 3d642a3..c6ab084 100644 --- a/lib/components/pages/new_entry.dart +++ b/lib/components/pages/new_entry.dart @@ -51,7 +51,8 @@ class _NewEntryPageState extends State { String? _activeDraftId; bool get _isEditing => widget.editLegId != null; - bool get _draftPersistenceEnabled => false; // legacy single draft disabled in favor of draft list + bool get _draftPersistenceEnabled => + false; // legacy single draft disabled in favor of draft list @override void initState() { @@ -266,8 +267,8 @@ class _NewEntryPageState extends State { if (json is! Map) { throw Exception('Unexpected response for leg $legId'); } - final beginTime = DateTime.tryParse(json['leg_begin_time'] ?? '') ?? - _selectedDate; + final beginTime = + DateTime.tryParse(json['leg_begin_time'] ?? '') ?? _selectedDate; final routeStations = _parseRouteStations(json['leg_route']); final mileageVal = (json['leg_mileage'] as num?)?.toDouble() ?? 0.0; final useManual = routeStations.isEmpty; @@ -297,13 +298,14 @@ class _NewEntryPageState extends State { _routeResult = routeResult; _startController.text = json['leg_start'] ?? ''; _endController.text = json['leg_end'] ?? ''; - _headcodeController.text = - (json['leg_headcode'] as String? ?? '').toUpperCase(); + _headcodeController.text = (json['leg_headcode'] as String? ?? '') + .toUpperCase(); _notesController.text = json['leg_notes'] ?? ''; - _networkController.text = - (json['leg_network'] as String? ?? '').toUpperCase(); - _mileageController.text = - mileageVal == 0 ? '' : mileageVal.toStringAsFixed(2); + _networkController.text = (json['leg_network'] as String? ?? '') + .toUpperCase(); + _mileageController.text = mileageVal == 0 + ? '' + : mileageVal.toStringAsFixed(2); _tractionItems ..clear() ..addAll(tractionItems); @@ -317,9 +319,9 @@ class _NewEntryPageState extends State { setState(() { _loadError = 'Failed to load entry: $e'; }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to load entry: $e')), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Failed to load entry: $e'))); } finally { _restoringDraft = false; if (mounted) { @@ -394,10 +396,7 @@ class _NewEntryPageState extends State { _TractionItem _mapLocoToTractionItem(Map loco) { final poweringRaw = loco['alloc_powering']; final powering = poweringRaw == true || poweringRaw == 1; - return _TractionItem( - loco: LocoSummary.fromJson(loco), - powering: powering, - ); + return _TractionItem(loco: LocoSummary.fromJson(loco), powering: powering); } DateTime get _legDateTime => DateTime( @@ -452,20 +451,11 @@ class _NewEntryPageState extends State { bool _draftChangedFromBaseline() { if (_loadedDraftSnapshot == null) return true; - final current = _normalizeDraftSnapshot( - _buildDraftSnapshot( - id: _activeDraftId ?? 'temp', - includeTimestamp: false, - ), + final current = _buildDraftSnapshot( + id: _activeDraftId ?? 'temp', + includeTimestamp: false, ); - final baseline = _normalizeDraftSnapshot(_loadedDraftSnapshot!); - return !_snapshotEquality.equals(baseline, current); - } - - Map _normalizeDraftSnapshot(Map snapshot) { - final normalized = Map.from(snapshot); - normalized.remove('saved_at'); - return normalized; + return !_snapshotEquality.equals(_loadedDraftSnapshot, current); } bool _formIsEmpty() { @@ -487,8 +477,9 @@ class _NewEntryPageState extends State { useRootNavigator: false, builder: (_) => AlertDialog( title: const Text('Save draft?'), - content: - const Text('Do you want to save this entry as a draft before leaving?'), + content: const Text( + 'Do you want to save this entry as a draft before leaving?', + ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(_ExitChoice.discard), @@ -637,11 +628,11 @@ class _NewEntryPageState extends State { if (!mounted) return; context.read().refreshLegs(); if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar( + ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(isEditingExisting ? 'Entry updated' : 'Entry submitted'), + content: Text( + isEditingExisting ? 'Entry updated' : 'Entry submitted', + ), ), ); _lastSubmittedSnapshot = snapshot; @@ -816,8 +807,7 @@ class _NewEntryPageState extends State { jsonEncode(drafts.map((e) => e.toJson()).toList()), ); _activeDraftId = id; - _loadedDraftSnapshot = - _normalizeDraftSnapshot(_buildDraftSnapshot(id: id, includeTimestamp: false)); + _loadedDraftSnapshot = _buildDraftSnapshot(id: id, includeTimestamp: false); return id; } @@ -882,11 +872,14 @@ class _NewEntryPageState extends State { if (payloadRaw is! Map) return; final payload = Map.from(payloadRaw); final mode = data['mode'] as String?; - final useManual = mode == 'manual' || - (payload.containsKey('leg_distance') && !payload.containsKey('leg_route')); + final useManual = + mode == 'manual' || + (payload.containsKey('leg_distance') && + !payload.containsKey('leg_route')); final beginStr = payload['leg_begin_time'] as String?; - final beginTime = - beginStr == null ? DateTime.now() : DateTime.tryParse(beginStr) ?? DateTime.now(); + final beginTime = beginStr == null + ? DateTime.now() + : DateTime.tryParse(beginStr) ?? DateTime.now(); final tripRaw = payload['leg_trip']; final tripId = tripRaw is num ? tripRaw.toInt() : null; @@ -894,7 +887,9 @@ class _NewEntryPageState extends State { RouteResult? restoredRouteResult; if (!useManual) { if (payload['leg_route'] is List) { - routeStations = (payload['leg_route'] as List).map((e) => e.toString()).toList(); + routeStations = (payload['leg_route'] as List) + .map((e) => e.toString()) + .toList(); } final rr = data['routeResult']; if (rr is Map) { @@ -903,10 +898,17 @@ class _NewEntryPageState extends State { (rr['input_route'] as List?)?.map((e) => e.toString()).toList() ?? routeStations, calculatedRoute: - (rr['calculated_route'] as List?)?.map((e) => e.toString()).toList() ?? + (rr['calculated_route'] as List?) + ?.map((e) => e.toString()) + .toList() ?? routeStations, - costs: (rr['costs'] as List?)?.map((e) => (e as num).toDouble()).toList() ?? [], - distance: (rr['distance'] as num?)?.toDouble() ?? + costs: + (rr['costs'] as List?) + ?.map((e) => (e as num).toDouble()) + .toList() ?? + [], + distance: + (rr['distance'] as num?)?.toDouble() ?? (payload['leg_mileage'] as num?)?.toDouble() ?? 0, ); @@ -927,21 +929,26 @@ class _NewEntryPageState extends State { _selectedTime = TimeOfDay.fromDateTime(beginTime); _selectedTripId = tripId == null || tripId == 0 ? null : tripId; _routeResult = restoredRouteResult; - _headcodeController.text = - (payload['leg_headcode'] as String? ?? '').toUpperCase(); - _networkController.text = - (payload['leg_network'] as String? ?? '').toUpperCase(); + _headcodeController.text = (payload['leg_headcode'] as String? ?? '') + .toUpperCase(); + _networkController.text = (payload['leg_network'] as String? ?? '') + .toUpperCase(); _notesController.text = payload['leg_notes'] ?? ''; if (useManual) { _startController.text = payload['leg_start'] ?? ''; _endController.text = payload['leg_end'] ?? ''; final miles = (payload['leg_distance'] as num?)?.toDouble(); - _mileageController.text = - miles == null || miles == 0 ? '' : miles.toStringAsFixed(2); + _mileageController.text = miles == null || miles == 0 + ? '' + : miles.toStringAsFixed(2); } else { - _startController.text = routeStations.isNotEmpty ? routeStations.first : ''; - _endController.text = routeStations.isNotEmpty ? routeStations.last : ''; + _startController.text = routeStations.isNotEmpty + ? routeStations.first + : ''; + _endController.text = routeStations.isNotEmpty + ? routeStations.last + : ''; final dist = _routeResult?.distance ?? 0; _mileageController.text = dist == 0 ? '' : dist.toStringAsFixed(2); } @@ -952,9 +959,9 @@ class _NewEntryPageState extends State { List>.from(tractionRaw.cast()), ); } else { - _tractionItems - ..clear() - ..add(_TractionItem.marker()); + _tractionItems + ..clear() + ..add(_TractionItem.marker()); } _lastSubmittedSnapshot = null; final idRaw = data['id']; @@ -964,11 +971,9 @@ class _NewEntryPageState extends State { }); final baselineId = _activeDraftId ?? data['id']?.toString() ?? DateTime.now().toString(); - _loadedDraftSnapshot = _normalizeDraftSnapshot( - _buildDraftSnapshot( - id: baselineId, - includeTimestamp: false, - ), + _loadedDraftSnapshot = _buildDraftSnapshot( + id: baselineId, + includeTimestamp: false, ); _restoringDraft = false; } @@ -1072,8 +1077,9 @@ class _NewEntryPageState extends State { minimumSize: const Size(0, 36), tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), - onPressed: - _submitting ? null : () => _resetFormState(clearDraft: true), + onPressed: _submitting + ? null + : () => _resetFormState(clearDraft: true), icon: const Icon(Icons.clear, size: 16), label: const Text('Clear form'), ), @@ -1128,7 +1134,7 @@ class _NewEntryPageState extends State { ), ), ], - ), + ), TextFormField( controller: _headcodeController, textCapitalization: TextCapitalization.characters, @@ -1444,18 +1450,15 @@ class _StoredDraft { final DateTime savedAt; final Map data; - _StoredDraft({ - required this.id, - required this.savedAt, - required this.data, - }); + _StoredDraft({required this.id, required this.savedAt, required this.data}); factory _StoredDraft.fromJson(Map json) { final savedAt = DateTime.tryParse(json['saved_at'] ?? '') ?? DateTime.now(); final data = Map.from(json['data'] as Map? ?? {}); final embeddedId = data['id']?.toString(); return _StoredDraft( - id: json['id']?.toString() ?? + id: + json['id']?.toString() ?? embeddedId ?? savedAt.microsecondsSinceEpoch.toString(), savedAt: savedAt, @@ -1464,11 +1467,7 @@ class _StoredDraft { } Map toJson() { - return { - "id": id, - "saved_at": savedAt.toIso8601String(), - "data": data, - }; + return {"id": id, "saved_at": savedAt.toIso8601String(), "data": data}; } } @@ -1491,10 +1490,7 @@ class _DraftListPage extends StatelessWidget { if (drafts.isEmpty) { return const Center(child: Text('No drafts saved yet.')); } - return _DraftListBody( - drafts: drafts, - onDelete: onDeleteDraft, - ); + return _DraftListBody(drafts: drafts, onDelete: onDeleteDraft); }, ), ); @@ -1551,16 +1547,16 @@ class _DraftListBodyState extends State<_DraftListBody> { Future _confirmDelete(BuildContext context, _StoredDraft draft) async { final confirmed = await showDialog( context: context, - builder: (_) => AlertDialog( + builder: (dialogCtx) => AlertDialog( title: const Text('Delete draft?'), content: const Text('This draft will be removed permanently.'), actions: [ TextButton( - onPressed: () => Navigator.of(context).pop(false), + onPressed: () => Navigator.of(dialogCtx).pop(false), child: const Text('Cancel'), ), TextButton( - onPressed: () => Navigator.of(context).pop(true), + onPressed: () => Navigator.of(dialogCtx).pop(true), child: const Text('Delete'), ), ], @@ -1607,11 +1603,13 @@ class _DraftListBodyState extends State<_DraftListBody> { if (network.isNotEmpty) parts.add('Network $network'); final notes = (map['leg_notes'] as String? ?? '').trim(); if (notes.isNotEmpty) parts.add('Notes'); - final mileage = (map['leg_distance'] as num?)?.toDouble() ?? + final mileage = + (map['leg_distance'] as num?)?.toDouble() ?? (map['leg_mileage'] as num?)?.toDouble(); if (mileage != null && mileage > 0) { parts.add('${mileage.toStringAsFixed(1)} mi'); - } else if (map['leg_route'] is List && (map['leg_route'] as List).isNotEmpty) { + } else if (map['leg_route'] is List && + (map['leg_route'] as List).isNotEmpty) { parts.add('Route ${(map['leg_route'] as List).length} stops'); } final locos = map['locos']; diff --git a/lib/components/pages/trips.dart b/lib/components/pages/trips.dart index 74eb4b1..638cae4 100644 --- a/lib/components/pages/trips.dart +++ b/lib/components/pages/trips.dart @@ -301,6 +301,25 @@ class _TripsPageState extends State { ], ), const SizedBox(height: 8), + if (!loading && items.isNotEmpty) ...[ + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + Chip( + avatar: const Icon(Icons.train, size: 16), + label: Text('Total had: ${items.length}'), + ), + Chip( + avatar: const Icon(Icons.star, size: 16), + label: Text( + 'Winners: ${items.where((e) => e.won == true).length}', + ), + ), + ], + ), + const SizedBox(height: 8), + ], if (loading) const Center( child: Padding( diff --git a/pubspec.yaml b/pubspec.yaml index 80ce1f0..9a57429 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.1.4+1 +version: 0.1.5+1 environment: sdk: ^3.8.1