Add accepted leg edit notification and class leaderboard
All checks were successful
Release / meta (push) Successful in 12s
Release / linux-build (push) Successful in 1m0s
Release / web-build (push) Successful in 2m6s
Release / android-build (push) Successful in 6m8s
Release / release-master (push) Successful in 16s
Release / release-dev (push) Successful in 19s

This commit is contained in:
2026-01-05 01:09:43 +00:00
parent 42ac7a97e1
commit 8ab3f53c0d
11 changed files with 1114 additions and 132 deletions

View File

@@ -55,26 +55,19 @@ class _StationAutocompleteState extends State<StationAutocomplete> {
},
fieldViewBuilder:
(context, textEditingController, focusNode, onFieldSubmitted) {
textEditingController.value = _controller.value;
textEditingController.value = _controller.value;
return TextField(
controller: textEditingController,
focusNode: focusNode,
textInputAction: TextInputAction.done,
onSubmitted: (_) {
final matches = _findTopMatches(textEditingController.text);
final firstMatch = matches.isEmpty ? null : matches.first;
if (firstMatch == null) return;
_controller.text = firstMatch;
widget.onChanged(firstMatch);
focusNode.unfocus(); // optionally close keyboard
},
decoration: const InputDecoration(
labelText: 'Select station',
border: OutlineInputBorder(),
),
);
},
return TextField(
controller: textEditingController,
focusNode: focusNode,
textInputAction: TextInputAction.done,
onSubmitted: (_) => onFieldSubmitted(),
decoration: const InputDecoration(
labelText: 'Select station',
border: OutlineInputBorder(),
),
);
},
);
}
@@ -181,6 +174,14 @@ class _RouteCalculatorState extends State<RouteCalculator> {
}
Future<void> _calculateRoute(List<String> stations) async {
final cleaned = stations.where((s) => s.trim().isNotEmpty).toList();
if (cleaned.length < 2) {
setState(() {
_routeResult = null;
_errorMessage = 'Add at least two stations before calculating.';
});
return;
}
setState(() {
_errorMessage = null;
_routeResult = null;
@@ -188,7 +189,7 @@ class _RouteCalculatorState extends State<RouteCalculator> {
final api = context.read<ApiService>(); // context is valid here
try {
final res = await api.post('/route/distance2', {
'route': stations.where((s) => s.trim().isNotEmpty).toList(),
'route': cleaned,
});
if (res is Map && res['error'] == false) {
@@ -232,11 +233,28 @@ class _RouteCalculatorState extends State<RouteCalculator> {
});
}
void _clearCalculator() {
final data = context.read<DataService>();
setState(() {
data.stations = [''];
_routeResult = null;
_errorMessage = null;
});
}
@override
Widget build(BuildContext context) {
final data = context.watch<DataService>();
return Column(
children: [
Align(
alignment: Alignment.centerRight,
child: IconButton(
tooltip: 'Clear calculator',
icon: const Icon(Icons.clear_all),
onPressed: _clearCalculator,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
child: Wrap(
@@ -366,28 +384,34 @@ class _RouteCalculatorState extends State<RouteCalculator> {
spacing: 12,
runSpacing: 8,
children: [
ElevatedButton.icon(
icon: const Icon(Icons.swap_horiz),
label: const Text('Reverse route'),
onPressed: () async {
setState(() {
data.stations = data.stations.reversed.toList();
});
await _calculateRoute(data.stations);
},
),
ElevatedButton.icon(
icon: const Icon(Icons.add),
label: const Text('Add Station'),
onPressed: _addStation,
),
ElevatedButton.icon(
icon: const Icon(Icons.route),
label: const Text('Calculate Route'),
onPressed: () async {
await _calculateRoute(data.stations);
},
),
...(() {
final reverseButton = ElevatedButton.icon(
icon: const Icon(Icons.swap_horiz),
label: const Text('Reverse route'),
onPressed: () async {
setState(() {
data.stations = data.stations.reversed.toList();
});
await _calculateRoute(data.stations);
},
);
final addButton = ElevatedButton.icon(
icon: const Icon(Icons.add),
label: const Text('Add Station'),
onPressed: _addStation,
);
final calculateButton = ElevatedButton.icon(
icon: const Icon(Icons.route),
label: const Text('Calculate Route'),
onPressed: () async {
await _calculateRoute(data.stations);
},
);
final isMobile = MediaQuery.of(context).size.width < 600;
return isMobile
? [addButton, reverseButton, calculateButton]
: [reverseButton, addButton, calculateButton];
})(),
],
),
),