initial codex commit
This commit is contained in:
@@ -35,10 +35,11 @@ class ApiService {
|
||||
dynamic data, {
|
||||
Map<String, String>? headers,
|
||||
}) async {
|
||||
final hasBody = data != null;
|
||||
final response = await http.post(
|
||||
Uri.parse('$baseUrl$endpoint'),
|
||||
headers: _buildHeaders(_jsonHeaders(headers)),
|
||||
body: jsonEncode(data),
|
||||
headers: _buildHeaders(hasBody ? _jsonHeaders(headers) : headers),
|
||||
body: hasBody ? jsonEncode(data) : null,
|
||||
);
|
||||
return _processResponse(response);
|
||||
}
|
||||
@@ -60,10 +61,11 @@ class ApiService {
|
||||
dynamic data, {
|
||||
Map<String, String>? headers,
|
||||
}) async {
|
||||
final hasBody = data != null;
|
||||
final response = await http.put(
|
||||
Uri.parse('$baseUrl$endpoint'),
|
||||
headers: _buildHeaders(_jsonHeaders(headers)),
|
||||
body: jsonEncode(data),
|
||||
headers: _buildHeaders(hasBody ? _jsonHeaders(headers) : headers),
|
||||
body: hasBody ? jsonEncode(data) : null,
|
||||
);
|
||||
return _processResponse(response);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:mileograph_flutter/objects/objects.dart';
|
||||
import 'package:mileograph_flutter/services/apiService.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class AuthService extends ChangeNotifier {
|
||||
final ApiService api;
|
||||
static const _tokenKey = 'auth_token';
|
||||
bool _restoring = false;
|
||||
|
||||
AuthService({required this.api});
|
||||
|
||||
@@ -29,6 +32,7 @@ class AuthService extends ChangeNotifier {
|
||||
access_token: accessToken,
|
||||
email: email,
|
||||
);
|
||||
_persistToken(accessToken);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -65,8 +69,66 @@ class AuthService extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> tryRestoreSession() async {
|
||||
if (_restoring || _user != null) return;
|
||||
_restoring = true;
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final token = prefs.getString(_tokenKey);
|
||||
if (token == null || token.isEmpty) return;
|
||||
final userResponse = await api.get(
|
||||
'/users/me',
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
'accept': 'application/json',
|
||||
},
|
||||
);
|
||||
setLoginData(
|
||||
userId: userResponse['user_id'],
|
||||
username: userResponse['username'],
|
||||
fullName: userResponse['full_name'],
|
||||
accessToken: token,
|
||||
email: userResponse['email'],
|
||||
);
|
||||
} catch (_) {
|
||||
await _clearToken();
|
||||
} finally {
|
||||
_restoring = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _persistToken(String token) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_tokenKey, token);
|
||||
}
|
||||
|
||||
Future<void> _clearToken() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_tokenKey);
|
||||
}
|
||||
|
||||
Future<void> register({
|
||||
required String username,
|
||||
required String email,
|
||||
required String fullName,
|
||||
required String password,
|
||||
String inviteCode = '',
|
||||
}) async {
|
||||
final formData = {
|
||||
'user_name': username,
|
||||
'email': email,
|
||||
'full_name': fullName,
|
||||
'password': password,
|
||||
'invitation_code': inviteCode,
|
||||
'empty': '',
|
||||
'empty2': '',
|
||||
};
|
||||
await api.postForm('/register', formData);
|
||||
}
|
||||
|
||||
void logout() {
|
||||
_user = null;
|
||||
_clearToken();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
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
|
||||
|
||||
@@ -14,10 +17,32 @@ class DataService extends ChangeNotifier {
|
||||
// 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;
|
||||
@@ -27,20 +52,30 @@ class DataService extends ChangeNotifier {
|
||||
|
||||
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;
|
||||
notifyListeners();
|
||||
|
||||
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;
|
||||
notifyListeners();
|
||||
_notifyAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,34 +84,178 @@ class DataService extends ChangeNotifier {
|
||||
int limit = 100,
|
||||
String sortBy = 'date',
|
||||
int sortDirection = 0,
|
||||
String? dateRangeStart,
|
||||
String? dateRangeEnd,
|
||||
bool append = false,
|
||||
}) async {
|
||||
final query =
|
||||
'?sort_direction=$sortDirection&sort_by=$sortBy&offset=$offset&limit=$limit';
|
||||
final json = await api.get('/user/legs$query');
|
||||
_isLegsLoading = true;
|
||||
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) {
|
||||
_legs = json.map((e) => Leg.fromJson(e)).toList();
|
||||
notifyListeners();
|
||||
} else {
|
||||
throw Exception('Unexpected legs response: $json');
|
||||
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> fetchHadTraction({int offset = 0, int limit = 100}) async {
|
||||
final query = '?offset=$offset&limit=$limit';
|
||||
final json = await api.get('/loco/mileage$query');
|
||||
await fetchTraction(
|
||||
hadOnly: true,
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
append: offset > 0,
|
||||
);
|
||||
}
|
||||
|
||||
if (json is List) {
|
||||
_traction = json.map((e) => LocoSummary.fromJson(e)).toList();
|
||||
notifyListeners();
|
||||
} else {
|
||||
throw Exception('Unexpected traction response: $json');
|
||||
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');
|
||||
if (json is List) {
|
||||
_tripList = json.map((e) => TripSummary.fromJson(e)).toList();
|
||||
}
|
||||
} 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;
|
||||
notifyListeners();
|
||||
_legs = [];
|
||||
_onThisDay = [];
|
||||
_trips = [];
|
||||
_tripDetails = [];
|
||||
_notifyAsync();
|
||||
}
|
||||
|
||||
double getMileageForCurrentYear() {
|
||||
|
||||
Reference in New Issue
Block a user