add filter by network for legs, add full export for traction
All checks were successful
Release / meta (push) Successful in 1m45s
Release / linux-build (push) Successful in 1m48s
Release / web-build (push) Successful in 1m56s
Release / android-build (push) Successful in 7m33s
Release / release-dev (push) Successful in 30s
Release / release-master (push) Successful in 5s

This commit is contained in:
2026-01-26 15:57:34 +00:00
parent 8340501f37
commit 94adf06726
9 changed files with 419 additions and 217 deletions

View File

@@ -119,6 +119,45 @@ class ApiService {
return _processResponse(response);
}
Future<ApiBinaryResponse> postBytes(
String endpoint,
dynamic data, {
Map<String, String>? headers,
bool includeAuth = true,
bool allowRetry = true,
}) async {
final hasBody = data != null;
final response = await _sendWithRetry(
() => _client.post(
Uri.parse('$baseUrl$endpoint'),
headers: _buildHeaders(
hasBody ? _jsonHeaders(headers) : headers,
includeAuth: includeAuth,
),
body: hasBody ? jsonEncode(data) : null,
),
allowRetry: allowRetry,
);
if (response.statusCode >= 200 && response.statusCode < 300) {
final contentDisposition = response.headers['content-disposition'];
return ApiBinaryResponse(
bytes: response.bodyBytes,
statusCode: response.statusCode,
contentType: response.headers['content-type'],
filename: _extractFilename(contentDisposition),
);
}
final body = _decodeBody(response);
final message = _extractErrorMessage(body);
throw ApiException(
statusCode: response.statusCode,
message: message,
body: body,
);
}
Future<dynamic> postMultipartFile(
String endpoint, {
required List<int> bytes,

View File

@@ -7,6 +7,7 @@ class _LegFetchOptions {
final String? dateRangeStart;
final String? dateRangeEnd;
final bool unallocatedOnly;
final List<String> networkFilter;
const _LegFetchOptions({
this.limit = 100,
@@ -15,6 +16,7 @@ class _LegFetchOptions {
this.dateRangeStart,
this.dateRangeEnd,
this.unallocatedOnly = false,
this.networkFilter = const [],
});
}
@@ -118,6 +120,7 @@ class DataService extends ChangeNotifier {
List<String> _stationNetworks = [];
Map<String, List<String>> _stationCountryNetworks = {};
DateTime? _stationFiltersFetchedAt;
DateTime? _stationNetworksFetchedAt;
List<String> get stationNetworks => _stationNetworks;
Map<String, List<String>> get stationCountryNetworks =>
_stationCountryNetworks;
@@ -391,9 +394,14 @@ class DataService extends ChangeNotifier {
String? dateRangeEnd,
bool append = false,
bool unallocatedOnly = false,
List<String> networkFilter = const [],
}) async {
_isLegsLoading = true;
if (!append) {
final normalizedNetworks = networkFilter
.map((network) => network.trim())
.where((network) => network.isNotEmpty)
.toList();
_lastLegsFetch = _LegFetchOptions(
limit: limit,
sortBy: sortBy,
@@ -401,6 +409,7 @@ class DataService extends ChangeNotifier {
dateRangeStart: dateRangeStart,
dateRangeEnd: dateRangeEnd,
unallocatedOnly: unallocatedOnly,
networkFilter: normalizedNetworks,
);
}
final buffer = StringBuffer(
@@ -415,6 +424,13 @@ class DataService extends ChangeNotifier {
if (unallocatedOnly) {
buffer.write('&unallocated_only=true');
}
final networks = networkFilter
.map((network) => network.trim())
.where((network) => network.isNotEmpty)
.toList();
for (final network in networks) {
buffer.write('&network_filter=${Uri.encodeQueryComponent(network)}');
}
try {
final json = await api.get('/user/legs${buffer.toString()}');
@@ -444,6 +460,7 @@ class DataService extends ChangeNotifier {
dateRangeStart: _lastLegsFetch.dateRangeStart,
dateRangeEnd: _lastLegsFetch.dateRangeEnd,
unallocatedOnly: _lastLegsFetch.unallocatedOnly,
networkFilter: _lastLegsFetch.networkFilter,
);
}
@@ -669,6 +686,7 @@ class DataService extends ChangeNotifier {
_stationNetworks = [];
_stationCountryNetworks = {};
_stationFiltersFetchedAt = null;
_stationNetworksFetchedAt = null;
_notifications = [];
_isNotificationsLoading = false;
_userEntriesVisibility = 'private';
@@ -736,6 +754,9 @@ class DataService extends ChangeNotifier {
final networks = (map['networks'] as List? ?? const [])
.whereType<String>()
.toList();
networks.sort(
(a, b) => a.toLowerCase().compareTo(b.toLowerCase()),
);
final countryNetworksRaw =
map['country_networks'] as Map? ?? const <String, dynamic>{};
final countryNetworks = <String, List<String>>{};
@@ -753,6 +774,31 @@ class DataService extends ChangeNotifier {
}
}
Future<void> fetchStationNetworks() async {
final now = DateTime.now();
final recent = _stationNetworks.isNotEmpty &&
((_stationNetworksFetchedAt != null &&
now.difference(_stationNetworksFetchedAt!) <
const Duration(minutes: 30)) ||
(_stationFiltersFetchedAt != null &&
now.difference(_stationFiltersFetchedAt!) <
const Duration(minutes: 30)));
if (recent) return;
try {
final response = await api.get('/stations/networks');
if (response is List) {
final networks = response.whereType<String>().toList();
networks.sort(
(a, b) => a.toLowerCase().compareTo(b.toLowerCase()),
);
_stationNetworks = networks;
_stationNetworksFetchedAt = now;
}
} catch (e) {
debugPrint('Failed to fetch station networks: $e');
}
}
String _stationKey(List<String> countries, List<String> networks) {
final c = countries..sort();
final n = networks..sort();