major refactor
Some checks failed
Release / meta (push) Successful in 9s
Release / android-build (push) Failing after 4m3s
Release / linux-build (push) Successful in 5m38s
Release / release-dev (push) Has been skipped
Release / release-master (push) Has been skipped

This commit is contained in:
2025-12-17 16:32:53 +00:00
parent 1239a9dc85
commit 334d6e3e18
29 changed files with 3614 additions and 3501 deletions

View File

@@ -25,9 +25,6 @@ class StationAutocomplete extends StatefulWidget {
class _StationAutocompleteState extends State<StationAutocomplete> {
late final TextEditingController _controller;
// Simulated list of over 10,000 stations
final List<String> stations = List.generate(10000, (i) => 'Station $i');
@override
void initState() {
super.initState();
@@ -50,15 +47,7 @@ class _StationAutocompleteState extends State<StationAutocomplete> {
if (textEditingValue.text.isEmpty) {
return const Iterable<String>.empty();
}
final query = textEditingValue.text.toLowerCase();
final matches = widget.allStations
.map((s) => s.name)
.where((name) => name.toLowerCase().contains(query))
.toList();
matches.sort((a, b) => a.length.compareTo(b.length));
return matches.take(10);
return _findTopMatches(textEditingValue.text);
},
onSelected: (String selection) {
_controller.text = selection;
@@ -73,19 +62,12 @@ class _StationAutocompleteState extends State<StationAutocomplete> {
focusNode: focusNode,
textInputAction: TextInputAction.done,
onSubmitted: (_) {
final query = textEditingController.text.toLowerCase();
final matches = widget.allStations
.map((s) => s.name)
.where((name) => name.toLowerCase().contains(query))
.toList();
if (matches.isNotEmpty) {
matches.sort((a, b) => a.length.compareTo(b.length));
final firstMatch = matches.first;
_controller.text = firstMatch;
widget.onChanged(firstMatch);
focusNode.unfocus(); // optionally close keyboard
}
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',
@@ -95,6 +77,42 @@ class _StationAutocompleteState extends State<StationAutocomplete> {
},
);
}
Iterable<String> _findTopMatches(String rawQuery) {
final query = rawQuery.trim().toLowerCase();
if (query.isEmpty) return const <String>[];
// Keep a bounded, sorted list (by shortest name, then alpha) without
// sorting the entire match set.
final best = <String>[];
for (final station in widget.allStations) {
final name = station.name;
if (name.isEmpty) continue;
if (!name.toLowerCase().contains(query)) continue;
_insertCandidate(best, name, max: 10);
}
return best;
}
void _insertCandidate(List<String> best, String candidate, {required int max}) {
final existingIndex = best.indexOf(candidate);
if (existingIndex >= 0) return;
int insertAt = 0;
while (insertAt < best.length &&
_candidateCompare(best[insertAt], candidate) <= 0) {
insertAt++;
}
best.insert(insertAt, candidate);
if (best.length > max) best.removeLast();
}
int _candidateCompare(String a, String b) {
final byLength = a.length.compareTo(b.length);
if (byLength != 0) return byLength;
return a.compareTo(b);
}
}
class RouteCalculator extends StatefulWidget {
@@ -146,20 +164,28 @@ class _RouteCalculatorState extends State<RouteCalculator> {
_routeResult = null;
});
final api = context.read<ApiService>(); // context is valid here
final res = await api.post('/route/distance2', {
'route': stations.where((s) => s.trim().isNotEmpty).toList(),
});
try {
final res = await api.post('/route/distance2', {
'route': stations.where((s) => s.trim().isNotEmpty).toList(),
});
if (res['error'] == false) {
setState(() {
_routeResult = RouteResult.fromJson(res);
});
final distance = (_routeResult?.distance ?? 0);
widget.onDistanceComputed?.call(distance);
} else {
setState(() {
_errorMessage = RouteError.fromJson(res["error_obj"][0]).msg;
});
if (res is Map && res['error'] == false) {
setState(() {
_routeResult = RouteResult.fromJson(Map<String, dynamic>.from(res));
});
final distance = (_routeResult?.distance ?? 0);
widget.onDistanceComputed?.call(distance);
} else if (res is Map && res['error_obj'] is List && res['error_obj'].isNotEmpty) {
setState(() {
_errorMessage = RouteError.fromJson(
Map<String, dynamic>.from(res['error_obj'][0] as Map),
).msg;
});
} else {
setState(() => _errorMessage = 'Failed to calculate route.');
}
} catch (e) {
setState(() => _errorMessage = 'Failed to calculate route: $e');
}
}