Some checks failed
Release / meta (push) Successful in 3s
Release / linux-build (push) Successful in 1m35s
Release / android-build (push) Successful in 4m50s
Release / windows-build (push) Has been cancelled
Release / release-dev (push) Has been cancelled
Release / release-master (push) Has been cancelled
350 lines
9.6 KiB
Dart
350 lines
9.6 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/scheduler.dart';
|
|
import 'package:mileograph_flutter/objects/objects.dart';
|
|
import 'package:mileograph_flutter/services/apiService.dart'; // assumes you've moved HomepageStats + submodels to a separate file
|
|
|
|
class _LegFetchOptions {
|
|
final int limit;
|
|
final String sortBy;
|
|
final int sortDirection;
|
|
final String? dateRangeStart;
|
|
final String? dateRangeEnd;
|
|
|
|
const _LegFetchOptions({
|
|
this.limit = 100,
|
|
this.sortBy = 'date',
|
|
this.sortDirection = 0,
|
|
this.dateRangeStart,
|
|
this.dateRangeEnd,
|
|
});
|
|
}
|
|
|
|
class DataService extends ChangeNotifier {
|
|
final ApiService api;
|
|
|
|
DataService({required this.api});
|
|
|
|
_LegFetchOptions _lastLegsFetch = const _LegFetchOptions();
|
|
|
|
// Homepage Data
|
|
HomepageStats? _homepageStats;
|
|
HomepageStats? get homepageStats => _homepageStats;
|
|
|
|
// Legs Data
|
|
List<Leg> _legs = [];
|
|
List<Leg> get legs => _legs;
|
|
List<Leg> _onThisDay = [];
|
|
List<Leg> get onThisDay => _onThisDay;
|
|
bool _isLegsLoading = false;
|
|
bool get isLegsLoading => _isLegsLoading;
|
|
bool _legsHasMore = false;
|
|
bool get legsHasMore => _legsHasMore;
|
|
|
|
// Traction Data
|
|
List<LocoSummary> _traction = [];
|
|
List<LocoSummary> get traction => _traction;
|
|
bool _isTractionLoading = false;
|
|
bool get isTractionLoading => _isTractionLoading;
|
|
bool _tractionHasMore = false;
|
|
bool get tractionHasMore => _tractionHasMore;
|
|
|
|
// Trips
|
|
List<TripSummary> _trips = [];
|
|
List<TripSummary> get trips => _trips;
|
|
List<TripDetail> _tripDetails = [];
|
|
List<TripDetail> get tripDetails => _tripDetails;
|
|
bool _isTripDetailsLoading = false;
|
|
bool get isTripDetailsLoading => _isTripDetailsLoading;
|
|
List<String> _locoClasses = [];
|
|
List<String> get locoClasses => _locoClasses;
|
|
List<TripSummary> _tripList = [];
|
|
List<TripSummary> get tripList => _tripList;
|
|
|
|
// Station Data
|
|
List<Station>? _cachedStations;
|
|
DateTime? _stationsFetchedAt;
|
|
|
|
List<String> stations = [""];
|
|
|
|
bool _isHomepageLoading = false;
|
|
bool get isHomepageLoading => _isHomepageLoading;
|
|
bool _isOnThisDayLoading = false;
|
|
bool get isOnThisDayLoading => _isOnThisDayLoading;
|
|
|
|
void _notifyAsync() {
|
|
// Always defer to the next frame to avoid setState during build.
|
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
|
notifyListeners();
|
|
});
|
|
}
|
|
|
|
Future<void> fetchHomepageStats() async {
|
|
_isHomepageLoading = true;
|
|
|
|
try {
|
|
final json = await api.get('/stats/homepage');
|
|
_homepageStats = HomepageStats.fromJson(json);
|
|
_trips = _homepageStats?.trips ?? [];
|
|
} catch (e) {
|
|
debugPrint('Failed to fetch homepage stats: $e');
|
|
_homepageStats = null;
|
|
_trips = [];
|
|
} finally {
|
|
_isHomepageLoading = false;
|
|
_notifyAsync();
|
|
}
|
|
}
|
|
|
|
Future<void> fetchLegs({
|
|
int offset = 0,
|
|
int limit = 100,
|
|
String sortBy = 'date',
|
|
int sortDirection = 0,
|
|
String? dateRangeStart,
|
|
String? dateRangeEnd,
|
|
bool append = false,
|
|
}) async {
|
|
_isLegsLoading = true;
|
|
if (!append) {
|
|
_lastLegsFetch = _LegFetchOptions(
|
|
limit: limit,
|
|
sortBy: sortBy,
|
|
sortDirection: sortDirection,
|
|
dateRangeStart: dateRangeStart,
|
|
dateRangeEnd: dateRangeEnd,
|
|
);
|
|
}
|
|
final buffer = StringBuffer(
|
|
'?sort_direction=$sortDirection&sort_by=$sortBy&offset=$offset&limit=$limit');
|
|
if (dateRangeStart != null && dateRangeStart.isNotEmpty) {
|
|
buffer.write('&date_range_start=$dateRangeStart');
|
|
}
|
|
if (dateRangeEnd != null && dateRangeEnd.isNotEmpty) {
|
|
buffer.write('&date_range_end=$dateRangeEnd');
|
|
}
|
|
try {
|
|
final json = await api.get('/user/legs${buffer.toString()}');
|
|
|
|
if (json is List) {
|
|
final newLegs = json.map((e) => Leg.fromJson(e)).toList();
|
|
_legs = append ? [..._legs, ...newLegs] : newLegs;
|
|
_legsHasMore = newLegs.length >= limit;
|
|
} else {
|
|
throw Exception('Unexpected legs response: $json');
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Failed to fetch legs: $e');
|
|
if (!append) _legs = [];
|
|
_legsHasMore = false;
|
|
} finally {
|
|
_isLegsLoading = false;
|
|
_notifyAsync();
|
|
}
|
|
}
|
|
|
|
Future<void> refreshLegs() {
|
|
return fetchLegs(
|
|
limit: _lastLegsFetch.limit,
|
|
sortBy: _lastLegsFetch.sortBy,
|
|
sortDirection: _lastLegsFetch.sortDirection,
|
|
dateRangeStart: _lastLegsFetch.dateRangeStart,
|
|
dateRangeEnd: _lastLegsFetch.dateRangeEnd,
|
|
);
|
|
}
|
|
|
|
Future<void> fetchHadTraction({int offset = 0, int limit = 100}) async {
|
|
await fetchTraction(
|
|
hadOnly: true,
|
|
offset: offset,
|
|
limit: limit,
|
|
append: offset > 0,
|
|
);
|
|
}
|
|
|
|
Future<void> fetchTraction({
|
|
bool hadOnly = false,
|
|
int offset = 0,
|
|
int limit = 50,
|
|
String? locoClass,
|
|
String? locoNumber,
|
|
bool mileageFirst = true,
|
|
bool append = false,
|
|
Map<String, dynamic>? filters,
|
|
}) async {
|
|
_isTractionLoading = true;
|
|
|
|
try {
|
|
final params = StringBuffer('?limit=$limit&offset=$offset');
|
|
if (hadOnly) params.write('&had_only=true');
|
|
if (mileageFirst) params.write('&mileage_first=true');
|
|
|
|
final payload = <String, dynamic>{};
|
|
if (locoClass != null && locoClass.isNotEmpty) {
|
|
payload['class'] = locoClass;
|
|
}
|
|
if (locoNumber != null && locoNumber.isNotEmpty) {
|
|
payload['number'] = locoNumber;
|
|
}
|
|
if (filters != null) {
|
|
filters.forEach((key, value) {
|
|
if (value == null) return;
|
|
if (value is String && value.trim().isEmpty) return;
|
|
payload[key] = value;
|
|
});
|
|
}
|
|
|
|
final json = await api.post(
|
|
'/locos/search/v2${params.toString()}',
|
|
payload.isEmpty ? null : payload,
|
|
);
|
|
|
|
if (json is List) {
|
|
final newItems = json.map((e) => LocoSummary.fromJson(e)).toList();
|
|
_traction = append ? [..._traction, ...newItems] : newItems;
|
|
_tractionHasMore = newItems.length >= limit;
|
|
} else {
|
|
throw Exception('Unexpected traction response: $json');
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Failed to fetch traction: $e');
|
|
if (!append) {
|
|
_traction = [];
|
|
}
|
|
_tractionHasMore = false;
|
|
} finally {
|
|
_isTractionLoading = false;
|
|
_notifyAsync();
|
|
}
|
|
}
|
|
|
|
Future<void> fetchOnThisDay({DateTime? date}) async {
|
|
_isOnThisDayLoading = true;
|
|
final target = date ?? DateTime.now();
|
|
final formatted =
|
|
"${target.year.toString().padLeft(4, '0')}-${target.month.toString().padLeft(2, '0')}-${target.day.toString().padLeft(2, '0')}";
|
|
try {
|
|
final json = await api.get('/legs/on-this-day?date=$formatted');
|
|
if (json is List) {
|
|
_onThisDay = json.map((e) => Leg.fromJson(e)).toList();
|
|
} else {
|
|
_onThisDay = [];
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Failed to fetch on-this-day legs: $e');
|
|
_onThisDay = [];
|
|
} finally {
|
|
_isOnThisDayLoading = false;
|
|
_notifyAsync();
|
|
}
|
|
}
|
|
|
|
Future<void> fetchTripDetails() async {
|
|
_isTripDetailsLoading = true;
|
|
try {
|
|
final json = await api.get('/trips/legs-and-stats');
|
|
if (json is List) {
|
|
_tripDetails = json.map((e) => TripDetail.fromJson(e)).toList();
|
|
} else {
|
|
_tripDetails = [];
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Failed to fetch trips: $e');
|
|
_tripDetails = [];
|
|
} finally {
|
|
_isTripDetailsLoading = false;
|
|
_notifyAsync();
|
|
}
|
|
}
|
|
|
|
Future<void> fetchTrips() async {
|
|
try {
|
|
final json = await api.get('/trips');
|
|
Iterable<dynamic>? raw;
|
|
if (json is List) {
|
|
raw = json;
|
|
} else if (json is Map) {
|
|
for (final key in ['trips', 'trip_data', 'data']) {
|
|
final value = json[key];
|
|
if (value is List) {
|
|
raw = value;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (raw != null) {
|
|
_tripList = raw
|
|
.whereType<Map<String, dynamic>>()
|
|
.map((e) => TripSummary.fromJson(e))
|
|
.toList();
|
|
} else {
|
|
debugPrint('Unexpected trip list response: $json');
|
|
_tripList = [];
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Failed to fetch trip list: $e');
|
|
_tripList = [];
|
|
} finally {
|
|
_notifyAsync();
|
|
}
|
|
}
|
|
|
|
Future<List<String>> fetchClassList() async {
|
|
if (_locoClasses.isNotEmpty) return _locoClasses;
|
|
try {
|
|
final json = await api.get('/loco/classlist');
|
|
if (json is List) {
|
|
_locoClasses = json.map((e) => e.toString()).toList();
|
|
_notifyAsync();
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Failed to fetch class list: $e');
|
|
}
|
|
return _locoClasses;
|
|
}
|
|
|
|
void clear() {
|
|
_homepageStats = null;
|
|
_legs = [];
|
|
_onThisDay = [];
|
|
_trips = [];
|
|
_tripDetails = [];
|
|
_notifyAsync();
|
|
}
|
|
|
|
double getMileageForCurrentYear() {
|
|
final currentYear = DateTime.now().year;
|
|
return getMileageForYear(currentYear) ?? 0;
|
|
}
|
|
|
|
double? getMileageForYear(int year) {
|
|
return _homepageStats?.yearlyMileage
|
|
.firstWhere(
|
|
(entry) => entry.year == year,
|
|
orElse: () => YearlyMileage(year: null, mileage: 0),
|
|
)
|
|
.mileage ??
|
|
0;
|
|
}
|
|
|
|
Future<List<Station>> fetchStations() async {
|
|
final now = DateTime.now();
|
|
|
|
// If cache exists and is less than 30 minutes old, return it
|
|
if (_cachedStations != null &&
|
|
_stationsFetchedAt != null &&
|
|
now.difference(_stationsFetchedAt!) < Duration(minutes: 30)) {
|
|
return _cachedStations!;
|
|
}
|
|
|
|
final response = await api.get('/location');
|
|
final parsed = (response as List).map((e) => Station.fromJson(e)).toList();
|
|
|
|
_cachedStations = parsed;
|
|
_stationsFetchedAt = now;
|
|
|
|
return parsed;
|
|
}
|
|
}
|