add timeline edit/delete
All checks were successful
Release / meta (push) Successful in 8s
Release / linux-build (push) Successful in 6m36s
Release / android-build (push) Successful in 17m20s
Release / release-dev (push) Successful in 37s
Release / release-master (push) Successful in 35s

This commit is contained in:
2025-12-17 12:17:41 +00:00
parent 80be797322
commit fa9773bcd1
9 changed files with 454 additions and 38 deletions

View File

@@ -1,9 +1,12 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:mileograph_flutter/components/traction/traction_card.dart';
import 'package:mileograph_flutter/objects/objects.dart';
import 'package:mileograph_flutter/services/data_service.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
class TractionPage extends StatefulWidget {
const TractionPage({
@@ -22,6 +25,7 @@ class TractionPage extends StatefulWidget {
}
class _TractionPageState extends State<TractionPage> {
static const String _prefsKey = 'traction_search_state_v1';
final _classController = TextEditingController();
final _classFocusNode = FocusNode();
final _numberController = TextEditingController();
@@ -34,6 +38,7 @@ class _TractionPageState extends State<TractionPage> {
final Map<String, TextEditingController> _dynamicControllers = {};
final Map<String, String?> _enumSelections = {};
bool _restoredFromPrefs = false;
@override
void initState() {
@@ -48,17 +53,107 @@ class _TractionPageState extends State<TractionPage> {
_initialised = true;
_selectedKeys = {...widget.selectedKeys};
WidgetsBinding.instance.addPostFrameCallback((_) {
final data = context.read<DataService>();
data.fetchClassList();
data.fetchEventFields();
_refreshTraction();
_initialLoad();
});
}
}
Future<void> _initialLoad() async {
final data = context.read<DataService>();
await _restoreSearchState();
data.fetchClassList();
data.fetchEventFields();
await _refreshTraction();
}
Future<void> _restoreSearchState() async {
if (widget.selectionMode) return;
if (_restoredFromPrefs) return;
_restoredFromPrefs = true;
try {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString(_prefsKey);
if (raw == null || raw.trim().isEmpty) return;
final decoded = jsonDecode(raw);
if (decoded is! Map) return;
final classText = decoded['classText']?.toString();
final numberText = decoded['number']?.toString();
final nameText = decoded['name']?.toString();
final selectedClass = decoded['selectedClass']?.toString();
final mileageFirst = decoded['mileageFirst'];
final showAdvanced = decoded['showAdvancedFilters'];
if (classText != null) _classController.text = classText;
if (numberText != null) _numberController.text = numberText;
if (nameText != null) _nameController.text = nameText;
final dynamicValues = <String, String>{};
final enumValues = <String, String?>{};
final dynamicRaw = decoded['dynamic'];
if (dynamicRaw is Map) {
for (final entry in dynamicRaw.entries) {
final key = entry.key.toString();
final val = entry.value?.toString() ?? '';
dynamicValues[key] = val;
}
}
final enumRaw = decoded['enum'];
if (enumRaw is Map) {
for (final entry in enumRaw.entries) {
enumValues[entry.key.toString()] = entry.value?.toString();
}
}
for (final entry in dynamicValues.entries) {
_dynamicControllers.putIfAbsent(
entry.key,
() => TextEditingController(text: entry.value),
);
_dynamicControllers[entry.key]?.text = entry.value;
}
for (final entry in enumValues.entries) {
_enumSelections[entry.key] = entry.value;
}
if (!mounted) return;
setState(() {
_selectedClass = (selectedClass != null && selectedClass.trim().isNotEmpty)
? selectedClass
: null;
if (mileageFirst is bool) _mileageFirst = mileageFirst;
if (showAdvanced is bool) _showAdvancedFilters = showAdvanced;
});
} catch (_) {
// Ignore preference restore failures.
}
}
Future<void> _persistSearchState() async {
if (widget.selectionMode) return;
final payload = <String, dynamic>{
'classText': _classController.text,
'number': _numberController.text,
'name': _nameController.text,
'selectedClass': _selectedClass,
'mileageFirst': _mileageFirst,
'showAdvancedFilters': _showAdvancedFilters,
'dynamic': _dynamicControllers.map((k, v) => MapEntry(k, v.text)),
'enum': _enumSelections,
};
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_prefsKey, jsonEncode(payload));
} catch (_) {
// Ignore persistence failures.
}
}
@override
void dispose() {
_classController.removeListener(_onClassTextChanged);
_persistSearchState();
_classController.dispose();
_classFocusNode.dispose();
_numberController.dispose();
@@ -111,6 +206,7 @@ class _TractionPageState extends State<TractionPage> {
filters: filters,
mileageFirst: _mileageFirst,
);
await _persistSearchState();
}
void _clearFilters() {
@@ -547,12 +643,14 @@ class _TractionPageState extends State<TractionPage> {
return _selectedKeys.contains(keyVal);
}
void _openTimeline(LocoSummary loco) {
Future<void> _openTimeline(LocoSummary loco) async {
final label = '${loco.locoClass} ${loco.number}'.trim();
context.push(
await context.push(
'/traction/${loco.id}/timeline',
extra: {'label': label},
);
if (!mounted) return;
await _refreshTraction();
}
Widget _buildFilterInput(