add timeline edit/delete
All checks were successful
All checks were successful
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user