drafts minor changes, edit minor changes
All checks were successful
Release / meta (push) Successful in 15s
Release / linux-build (push) Successful in 9m20s
Release / android-build (push) Successful in 25m33s
Release / release-master (push) Successful in 43s
Release / release-dev (push) Successful in 45s

This commit is contained in:
2025-12-15 00:33:18 +00:00
parent 603e117af8
commit da70dce369
4 changed files with 112 additions and 86 deletions

View File

@@ -97,10 +97,16 @@ class _StationAutocompleteState extends State<StationAutocomplete> {
} }
class RouteCalculator extends StatefulWidget { class RouteCalculator extends StatefulWidget {
const RouteCalculator({super.key, this.onDistanceComputed, this.onApplyRoute}); const RouteCalculator({
super.key,
this.onDistanceComputed,
this.onApplyRoute,
this.initialStations,
});
final ValueChanged<double>? onDistanceComputed; final ValueChanged<double>? onDistanceComputed;
final ValueChanged<RouteResult>? onApplyRoute; final ValueChanged<RouteResult>? onApplyRoute;
final List<String>? initialStations;
@override @override
State<RouteCalculator> createState() => _RouteCalculatorState(); State<RouteCalculator> createState() => _RouteCalculatorState();
@@ -122,6 +128,9 @@ class _RouteCalculatorState extends State<RouteCalculator> {
super.didChangeDependencies(); super.didChangeDependencies();
if (!_fetched) { if (!_fetched) {
_fetched = true; _fetched = true;
if (widget.initialStations != null && widget.initialStations!.isNotEmpty) {
context.read<DataService>().stations = List.from(widget.initialStations!);
}
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
final data = context.read<DataService>(); final data = context.read<DataService>();
final result = await data.fetchStations(); final result = await data.fetchStations();

View File

@@ -51,7 +51,8 @@ class _NewEntryPageState extends State<NewEntryPage> {
String? _activeDraftId; String? _activeDraftId;
bool get _isEditing => widget.editLegId != null; 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 @override
void initState() { void initState() {
@@ -266,8 +267,8 @@ class _NewEntryPageState extends State<NewEntryPage> {
if (json is! Map<String, dynamic>) { if (json is! Map<String, dynamic>) {
throw Exception('Unexpected response for leg $legId'); throw Exception('Unexpected response for leg $legId');
} }
final beginTime = DateTime.tryParse(json['leg_begin_time'] ?? '') ?? final beginTime =
_selectedDate; DateTime.tryParse(json['leg_begin_time'] ?? '') ?? _selectedDate;
final routeStations = _parseRouteStations(json['leg_route']); final routeStations = _parseRouteStations(json['leg_route']);
final mileageVal = (json['leg_mileage'] as num?)?.toDouble() ?? 0.0; final mileageVal = (json['leg_mileage'] as num?)?.toDouble() ?? 0.0;
final useManual = routeStations.isEmpty; final useManual = routeStations.isEmpty;
@@ -297,13 +298,14 @@ class _NewEntryPageState extends State<NewEntryPage> {
_routeResult = routeResult; _routeResult = routeResult;
_startController.text = json['leg_start'] ?? ''; _startController.text = json['leg_start'] ?? '';
_endController.text = json['leg_end'] ?? ''; _endController.text = json['leg_end'] ?? '';
_headcodeController.text = _headcodeController.text = (json['leg_headcode'] as String? ?? '')
(json['leg_headcode'] as String? ?? '').toUpperCase(); .toUpperCase();
_notesController.text = json['leg_notes'] ?? ''; _notesController.text = json['leg_notes'] ?? '';
_networkController.text = _networkController.text = (json['leg_network'] as String? ?? '')
(json['leg_network'] as String? ?? '').toUpperCase(); .toUpperCase();
_mileageController.text = _mileageController.text = mileageVal == 0
mileageVal == 0 ? '' : mileageVal.toStringAsFixed(2); ? ''
: mileageVal.toStringAsFixed(2);
_tractionItems _tractionItems
..clear() ..clear()
..addAll(tractionItems); ..addAll(tractionItems);
@@ -317,9 +319,9 @@ class _NewEntryPageState extends State<NewEntryPage> {
setState(() { setState(() {
_loadError = 'Failed to load entry: $e'; _loadError = 'Failed to load entry: $e';
}); });
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
SnackBar(content: Text('Failed to load entry: $e')), context,
); ).showSnackBar(SnackBar(content: Text('Failed to load entry: $e')));
} finally { } finally {
_restoringDraft = false; _restoringDraft = false;
if (mounted) { if (mounted) {
@@ -394,10 +396,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
_TractionItem _mapLocoToTractionItem(Map<String, dynamic> loco) { _TractionItem _mapLocoToTractionItem(Map<String, dynamic> loco) {
final poweringRaw = loco['alloc_powering']; final poweringRaw = loco['alloc_powering'];
final powering = poweringRaw == true || poweringRaw == 1; final powering = poweringRaw == true || poweringRaw == 1;
return _TractionItem( return _TractionItem(loco: LocoSummary.fromJson(loco), powering: powering);
loco: LocoSummary.fromJson(loco),
powering: powering,
);
} }
DateTime get _legDateTime => DateTime( DateTime get _legDateTime => DateTime(
@@ -452,20 +451,11 @@ class _NewEntryPageState extends State<NewEntryPage> {
bool _draftChangedFromBaseline() { bool _draftChangedFromBaseline() {
if (_loadedDraftSnapshot == null) return true; if (_loadedDraftSnapshot == null) return true;
final current = _normalizeDraftSnapshot( final current = _buildDraftSnapshot(
_buildDraftSnapshot(
id: _activeDraftId ?? 'temp', id: _activeDraftId ?? 'temp',
includeTimestamp: false, includeTimestamp: false,
),
); );
final baseline = _normalizeDraftSnapshot(_loadedDraftSnapshot!); return !_snapshotEquality.equals(_loadedDraftSnapshot, current);
return !_snapshotEquality.equals(baseline, current);
}
Map<String, dynamic> _normalizeDraftSnapshot(Map<String, dynamic> snapshot) {
final normalized = Map<String, dynamic>.from(snapshot);
normalized.remove('saved_at');
return normalized;
} }
bool _formIsEmpty() { bool _formIsEmpty() {
@@ -487,8 +477,9 @@ class _NewEntryPageState extends State<NewEntryPage> {
useRootNavigator: false, useRootNavigator: false,
builder: (_) => AlertDialog( builder: (_) => AlertDialog(
title: const Text('Save draft?'), title: const Text('Save draft?'),
content: content: const Text(
const Text('Do you want to save this entry as a draft before leaving?'), 'Do you want to save this entry as a draft before leaving?',
),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(_ExitChoice.discard), onPressed: () => Navigator.of(context).pop(_ExitChoice.discard),
@@ -637,11 +628,11 @@ class _NewEntryPageState extends State<NewEntryPage> {
if (!mounted) return; if (!mounted) return;
context.read<DataService>().refreshLegs(); context.read<DataService>().refreshLegs();
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of( ScaffoldMessenger.of(context).showSnackBar(
context,
).showSnackBar(
SnackBar( SnackBar(
content: Text(isEditingExisting ? 'Entry updated' : 'Entry submitted'), content: Text(
isEditingExisting ? 'Entry updated' : 'Entry submitted',
),
), ),
); );
_lastSubmittedSnapshot = snapshot; _lastSubmittedSnapshot = snapshot;
@@ -816,8 +807,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
jsonEncode(drafts.map((e) => e.toJson()).toList()), jsonEncode(drafts.map((e) => e.toJson()).toList()),
); );
_activeDraftId = id; _activeDraftId = id;
_loadedDraftSnapshot = _loadedDraftSnapshot = _buildDraftSnapshot(id: id, includeTimestamp: false);
_normalizeDraftSnapshot(_buildDraftSnapshot(id: id, includeTimestamp: false));
return id; return id;
} }
@@ -882,11 +872,14 @@ class _NewEntryPageState extends State<NewEntryPage> {
if (payloadRaw is! Map) return; if (payloadRaw is! Map) return;
final payload = Map<String, dynamic>.from(payloadRaw); final payload = Map<String, dynamic>.from(payloadRaw);
final mode = data['mode'] as String?; final mode = data['mode'] as String?;
final useManual = mode == 'manual' || final useManual =
(payload.containsKey('leg_distance') && !payload.containsKey('leg_route')); mode == 'manual' ||
(payload.containsKey('leg_distance') &&
!payload.containsKey('leg_route'));
final beginStr = payload['leg_begin_time'] as String?; final beginStr = payload['leg_begin_time'] as String?;
final beginTime = final beginTime = beginStr == null
beginStr == null ? DateTime.now() : DateTime.tryParse(beginStr) ?? DateTime.now(); ? DateTime.now()
: DateTime.tryParse(beginStr) ?? DateTime.now();
final tripRaw = payload['leg_trip']; final tripRaw = payload['leg_trip'];
final tripId = tripRaw is num ? tripRaw.toInt() : null; final tripId = tripRaw is num ? tripRaw.toInt() : null;
@@ -894,7 +887,9 @@ class _NewEntryPageState extends State<NewEntryPage> {
RouteResult? restoredRouteResult; RouteResult? restoredRouteResult;
if (!useManual) { if (!useManual) {
if (payload['leg_route'] is List) { 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']; final rr = data['routeResult'];
if (rr is Map<String, dynamic>) { if (rr is Map<String, dynamic>) {
@@ -903,10 +898,17 @@ class _NewEntryPageState extends State<NewEntryPage> {
(rr['input_route'] as List?)?.map((e) => e.toString()).toList() ?? (rr['input_route'] as List?)?.map((e) => e.toString()).toList() ??
routeStations, routeStations,
calculatedRoute: calculatedRoute:
(rr['calculated_route'] as List?)?.map((e) => e.toString()).toList() ?? (rr['calculated_route'] as List?)
?.map((e) => e.toString())
.toList() ??
routeStations, routeStations,
costs: (rr['costs'] as List?)?.map((e) => (e as num).toDouble()).toList() ?? [], costs:
distance: (rr['distance'] as num?)?.toDouble() ?? (rr['costs'] as List?)
?.map((e) => (e as num).toDouble())
.toList() ??
[],
distance:
(rr['distance'] as num?)?.toDouble() ??
(payload['leg_mileage'] as num?)?.toDouble() ?? (payload['leg_mileage'] as num?)?.toDouble() ??
0, 0,
); );
@@ -927,21 +929,26 @@ class _NewEntryPageState extends State<NewEntryPage> {
_selectedTime = TimeOfDay.fromDateTime(beginTime); _selectedTime = TimeOfDay.fromDateTime(beginTime);
_selectedTripId = tripId == null || tripId == 0 ? null : tripId; _selectedTripId = tripId == null || tripId == 0 ? null : tripId;
_routeResult = restoredRouteResult; _routeResult = restoredRouteResult;
_headcodeController.text = _headcodeController.text = (payload['leg_headcode'] as String? ?? '')
(payload['leg_headcode'] as String? ?? '').toUpperCase(); .toUpperCase();
_networkController.text = _networkController.text = (payload['leg_network'] as String? ?? '')
(payload['leg_network'] as String? ?? '').toUpperCase(); .toUpperCase();
_notesController.text = payload['leg_notes'] ?? ''; _notesController.text = payload['leg_notes'] ?? '';
if (useManual) { if (useManual) {
_startController.text = payload['leg_start'] ?? ''; _startController.text = payload['leg_start'] ?? '';
_endController.text = payload['leg_end'] ?? ''; _endController.text = payload['leg_end'] ?? '';
final miles = (payload['leg_distance'] as num?)?.toDouble(); final miles = (payload['leg_distance'] as num?)?.toDouble();
_mileageController.text = _mileageController.text = miles == null || miles == 0
miles == null || miles == 0 ? '' : miles.toStringAsFixed(2); ? ''
: miles.toStringAsFixed(2);
} else { } else {
_startController.text = routeStations.isNotEmpty ? routeStations.first : ''; _startController.text = routeStations.isNotEmpty
_endController.text = routeStations.isNotEmpty ? routeStations.last : ''; ? routeStations.first
: '';
_endController.text = routeStations.isNotEmpty
? routeStations.last
: '';
final dist = _routeResult?.distance ?? 0; final dist = _routeResult?.distance ?? 0;
_mileageController.text = dist == 0 ? '' : dist.toStringAsFixed(2); _mileageController.text = dist == 0 ? '' : dist.toStringAsFixed(2);
} }
@@ -964,11 +971,9 @@ class _NewEntryPageState extends State<NewEntryPage> {
}); });
final baselineId = final baselineId =
_activeDraftId ?? data['id']?.toString() ?? DateTime.now().toString(); _activeDraftId ?? data['id']?.toString() ?? DateTime.now().toString();
_loadedDraftSnapshot = _normalizeDraftSnapshot( _loadedDraftSnapshot = _buildDraftSnapshot(
_buildDraftSnapshot(
id: baselineId, id: baselineId,
includeTimestamp: false, includeTimestamp: false,
),
); );
_restoringDraft = false; _restoringDraft = false;
} }
@@ -1072,8 +1077,9 @@ class _NewEntryPageState extends State<NewEntryPage> {
minimumSize: const Size(0, 36), minimumSize: const Size(0, 36),
tapTargetSize: MaterialTapTargetSize.shrinkWrap, tapTargetSize: MaterialTapTargetSize.shrinkWrap,
), ),
onPressed: onPressed: _submitting
_submitting ? null : () => _resetFormState(clearDraft: true), ? null
: () => _resetFormState(clearDraft: true),
icon: const Icon(Icons.clear, size: 16), icon: const Icon(Icons.clear, size: 16),
label: const Text('Clear form'), label: const Text('Clear form'),
), ),
@@ -1444,18 +1450,15 @@ class _StoredDraft {
final DateTime savedAt; final DateTime savedAt;
final Map<String, dynamic> data; final Map<String, dynamic> data;
_StoredDraft({ _StoredDraft({required this.id, required this.savedAt, required this.data});
required this.id,
required this.savedAt,
required this.data,
});
factory _StoredDraft.fromJson(Map<String, dynamic> json) { factory _StoredDraft.fromJson(Map<String, dynamic> json) {
final savedAt = DateTime.tryParse(json['saved_at'] ?? '') ?? DateTime.now(); final savedAt = DateTime.tryParse(json['saved_at'] ?? '') ?? DateTime.now();
final data = Map<String, dynamic>.from(json['data'] as Map? ?? {}); final data = Map<String, dynamic>.from(json['data'] as Map? ?? {});
final embeddedId = data['id']?.toString(); final embeddedId = data['id']?.toString();
return _StoredDraft( return _StoredDraft(
id: json['id']?.toString() ?? id:
json['id']?.toString() ??
embeddedId ?? embeddedId ??
savedAt.microsecondsSinceEpoch.toString(), savedAt.microsecondsSinceEpoch.toString(),
savedAt: savedAt, savedAt: savedAt,
@@ -1464,11 +1467,7 @@ class _StoredDraft {
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {"id": id, "saved_at": savedAt.toIso8601String(), "data": data};
"id": id,
"saved_at": savedAt.toIso8601String(),
"data": data,
};
} }
} }
@@ -1491,10 +1490,7 @@ class _DraftListPage extends StatelessWidget {
if (drafts.isEmpty) { if (drafts.isEmpty) {
return const Center(child: Text('No drafts saved yet.')); return const Center(child: Text('No drafts saved yet.'));
} }
return _DraftListBody( return _DraftListBody(drafts: drafts, onDelete: onDeleteDraft);
drafts: drafts,
onDelete: onDeleteDraft,
);
}, },
), ),
); );
@@ -1551,16 +1547,16 @@ class _DraftListBodyState extends State<_DraftListBody> {
Future<void> _confirmDelete(BuildContext context, _StoredDraft draft) async { Future<void> _confirmDelete(BuildContext context, _StoredDraft draft) async {
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,
builder: (_) => AlertDialog( builder: (dialogCtx) => AlertDialog(
title: const Text('Delete draft?'), title: const Text('Delete draft?'),
content: const Text('This draft will be removed permanently.'), content: const Text('This draft will be removed permanently.'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(false), onPressed: () => Navigator.of(dialogCtx).pop(false),
child: const Text('Cancel'), child: const Text('Cancel'),
), ),
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(true), onPressed: () => Navigator.of(dialogCtx).pop(true),
child: const Text('Delete'), child: const Text('Delete'),
), ),
], ],
@@ -1607,11 +1603,13 @@ class _DraftListBodyState extends State<_DraftListBody> {
if (network.isNotEmpty) parts.add('Network $network'); if (network.isNotEmpty) parts.add('Network $network');
final notes = (map['leg_notes'] as String? ?? '').trim(); final notes = (map['leg_notes'] as String? ?? '').trim();
if (notes.isNotEmpty) parts.add('Notes'); 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(); (map['leg_mileage'] as num?)?.toDouble();
if (mileage != null && mileage > 0) { if (mileage != null && mileage > 0) {
parts.add('${mileage.toStringAsFixed(1)} mi'); 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'); parts.add('Route ${(map['leg_route'] as List).length} stops');
} }
final locos = map['locos']; final locos = map['locos'];

View File

@@ -301,6 +301,25 @@ class _TripsPageState extends State<TripsPage> {
], ],
), ),
const SizedBox(height: 8), 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) if (loading)
const Center( const Center(
child: Padding( child: Padding(

View File

@@ -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 # 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 # 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. # 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: environment:
sdk: ^3.8.1 sdk: ^3.8.1