QoL changes
All checks were successful
Release / meta (push) Successful in 22s
Release / linux-build (push) Successful in 4m32s
Release / android-build (push) Successful in 7m10s
Release / release-dev (push) Successful in 9s
Release / release-master (push) Successful in 9s

This commit is contained in:
2025-12-14 09:45:32 +00:00
parent 8116cfe7b1
commit f0dfbd185b
11 changed files with 887 additions and 321 deletions

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
@@ -8,6 +9,7 @@ import 'package:mileograph_flutter/objects/objects.dart';
import 'package:mileograph_flutter/services/apiService.dart';
import 'package:mileograph_flutter/services/dataService.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
class NewEntryPage extends StatefulWidget {
const NewEntryPage({super.key});
@@ -17,6 +19,8 @@ class NewEntryPage extends StatefulWidget {
}
class _NewEntryPageState extends State<NewEntryPage> {
static const _draftPrefsKey = 'new_entry_draft';
final _formKey = GlobalKey<FormState>();
DateTime _selectedDate = DateTime.now();
TimeOfDay _selectedTime = TimeOfDay.now();
@@ -31,20 +35,42 @@ class _NewEntryPageState extends State<NewEntryPage> {
RouteResult? _routeResult;
final List<_TractionItem> _tractionItems = [_TractionItem.marker()];
int? _selectedTripId;
bool _restoringDraft = false;
@override
void initState() {
super.initState();
for (final controller in [
_startController,
_endController,
_headcodeController,
_notesController,
_mileageController,
_networkController,
]) {
controller.addListener(_saveDraft);
}
Future.microtask(() {
if (!mounted) return;
final data = context.read<DataService>();
data.fetchClassList();
data.fetchTrips();
_loadDraft();
});
}
@override
void dispose() {
for (final controller in [
_startController,
_endController,
_headcodeController,
_notesController,
_mileageController,
_networkController,
]) {
controller.removeListener(_saveDraft);
}
_startController.dispose();
_endController.dispose();
_headcodeController.dispose();
@@ -73,7 +99,10 @@ class _NewEntryPageState extends State<NewEntryPage> {
DropdownMenuItem(value: t.tripId, child: Text(t.tripName)),
),
],
onChanged: (val) => setState(() => _selectedTripId = val),
onChanged: (val) {
setState(() => _selectedTripId = val);
_saveDraft();
},
),
),
const SizedBox(width: 8),
@@ -126,6 +155,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
: TripSummary(tripId: 0, tripName: result, tripMileage: 0),
);
setState(() => _selectedTripId = match.tripId);
_saveDraft();
} catch (e) {
if (!mounted) return;
messenger.showSnackBar(
@@ -149,6 +179,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
_mileageController.text = result.distance.toStringAsFixed(2);
_useManualMileage = false;
});
_saveDraft();
}
}
@@ -183,6 +214,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
);
}
});
_saveDraft();
},
),
),
@@ -197,6 +229,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (picked != null) setState(() => _selectedDate = picked);
_saveDraft();
}
Future<void> _pickTime() async {
@@ -204,7 +237,10 @@ class _NewEntryPageState extends State<NewEntryPage> {
context: context,
initialTime: _selectedTime,
);
if (picked != null) setState(() => _selectedTime = picked);
if (picked != null) {
setState(() => _selectedTime = picked);
_saveDraft();
}
}
DateTime get _legDateTime => DateTime(
@@ -295,7 +331,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Entry submitted')));
_formKey.currentState!.reset();
_resetFormState(clearDraft: true);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(
@@ -306,6 +342,166 @@ class _NewEntryPageState extends State<NewEntryPage> {
}
}
Future<void> _resetFormState({bool clearDraft = false}) async {
_formKey.currentState?.reset();
_startController.clear();
_endController.clear();
_headcodeController.clear();
_notesController.clear();
_mileageController.clear();
_networkController.clear();
setState(() {
_selectedDate = DateTime.now();
_selectedTime = TimeOfDay.now();
_useManualMileage = false;
_routeResult = null;
_tractionItems
..clear()
..add(_TractionItem.marker());
_selectedTripId = null;
_submitting = false;
});
if (clearDraft) {
await _clearDraft();
} else {
_saveDraft();
}
}
Future<void> _saveDraft() async {
if (_restoringDraft) return;
final prefs = await SharedPreferences.getInstance();
final draft = {
"date": _selectedDate.toIso8601String(),
"time": {"hour": _selectedTime.hour, "minute": _selectedTime.minute},
"start": _startController.text,
"end": _endController.text,
"headcode": _headcodeController.text,
"notes": _notesController.text,
"mileage": _mileageController.text,
"network": _networkController.text,
"useManualMileage": _useManualMileage,
"selectedTripId": _selectedTripId,
"routeResult": _routeResult == null
? null
: {
"input_route": _routeResult!.inputRoute,
"calculated_route": _routeResult!.calculatedRoute,
"costs": _routeResult!.costs,
"distance": _routeResult!.distance,
},
"tractionItems": _serializeTractionItems(),
};
await prefs.setString(_draftPrefsKey, jsonEncode(draft));
}
Future<void> _clearDraft() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_draftPrefsKey);
}
Future<void> _loadDraft() async {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString(_draftPrefsKey);
if (raw == null) return;
try {
final data = jsonDecode(raw);
if (data is! Map) return;
_restoringDraft = true;
setState(() {
if (data['date'] is String) {
_selectedDate = DateTime.tryParse(data['date']) ?? _selectedDate;
}
if (data['time'] is Map) {
final time = data['time'] as Map;
final hour = time['hour'] as int?;
final minute = time['minute'] as int?;
if (hour != null && minute != null) {
_selectedTime = TimeOfDay(hour: hour, minute: minute);
}
}
_useManualMileage = data['useManualMileage'] ?? _useManualMileage;
_selectedTripId = data['selectedTripId'];
if (data['routeResult'] is Map<String, dynamic>) {
_routeResult =
RouteResult.fromJson(Map<String, dynamic>.from(data['routeResult']));
_mileageController.text = _routeResult!.distance.toStringAsFixed(2);
}
if (data['tractionItems'] is List) {
_restoreTractionItems(List<Map<String, dynamic>>.from(
data['tractionItems'].cast<Map>(),
));
}
});
_startController.text = data['start'] ?? '';
_endController.text = data['end'] ?? '';
_headcodeController.text = data['headcode'] ?? '';
_notesController.text = data['notes'] ?? '';
_mileageController.text = data['mileage'] ?? '';
_networkController.text = data['network'] ?? '';
} catch (_) {
// Ignore corrupt draft data
} finally {
_restoringDraft = false;
}
}
List<Map<String, dynamic>> _serializeTractionItems() {
return _tractionItems
.map(
(item) => {
"isMarker": item.isMarker,
"powering": item.powering,
"loco": item.loco == null
? null
: {
"id": item.loco!.id,
"type": item.loco!.type,
"number": item.loco!.number,
"class": item.loco!.locoClass,
"name": item.loco!.name,
"operator": item.loco!.operator,
"notes": item.loco!.notes,
"evn": item.loco!.evn,
},
},
)
.toList();
}
void _restoreTractionItems(List<Map<String, dynamic>> items) {
final restored = <_TractionItem>[];
for (final item in items) {
final locoData = item['loco'] as Map<String, dynamic>?;
LocoSummary? loco;
if (locoData != null) {
loco = LocoSummary(
locoId: locoData['id'] ?? 0,
locoType: locoData['type'] ?? '',
locoNumber: locoData['number'] ?? '',
locoName: locoData['name'] ?? '',
locoClass: locoData['class'] ?? '',
locoOperator: locoData['operator'] ?? '',
locoNotes: locoData['notes'],
locoEvn: locoData['evn'],
);
}
restored.add(
_TractionItem(
loco: loco,
powering: item['powering'] ?? true,
isMarker: item['isMarker'] ?? false,
),
);
}
if (restored.where((e) => e.isMarker).isEmpty) {
restored.insert(0, _TractionItem.marker());
}
_tractionItems
..clear()
..addAll(restored);
}
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width < 700;
@@ -413,6 +609,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
setState(() {
_useManualMileage = val;
});
_saveDraft();
},
),
if (_useManualMileage)
@@ -506,6 +703,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
final item = _tractionItems.removeAt(oldIndex);
_tractionItems.insert(newIndex, item);
});
_saveDraft();
},
itemCount: _tractionItems.length,
itemBuilder: (context, index) {
@@ -549,6 +747,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
setState(() {
_tractionItems[index] = item.copyWith(powering: v);
});
_saveDraft();
},
),
IconButton(
@@ -557,6 +756,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
setState(() {
_tractionItems.removeAt(index);
});
_saveDraft();
},
),
],