QoL changes
All checks were successful
All checks were successful
This commit is contained in:
@@ -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();
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user