QoL changes
All checks were successful
Release / meta (push) Successful in 22s
Release / linux-build (push) Successful in 4m32s
Release / android-build (push) Successful in 7m10s
Release / release-dev (push) Successful in 9s
Release / release-master (push) Successful in 9s

This commit is contained in:
2025-12-14 09:45:32 +00:00
parent 8116cfe7b1
commit f0dfbd185b
11 changed files with 887 additions and 321 deletions

View File

@@ -1,15 +1,13 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:mileograph_flutter/objects/objects.dart';
import 'package:mileograph_flutter/services/apiService.dart';
import 'package:mileograph_flutter/services/tokenStorageService.dart';
class AuthService extends ChangeNotifier {
final ApiService api;
static const _tokenKey = 'auth_token';
bool _restoring = false;
// secure storage instance
final FlutterSecureStorage _storage = const FlutterSecureStorage();
final TokenStorageService _tokenStorage = TokenStorageService();
AuthService({required this.api});
@@ -74,10 +72,10 @@ class AuthService extends ChangeNotifier {
Future<void> tryRestoreSession() async {
if (_restoring || _user != null) return;
_restoring = true;
try {
// read token from secure storage
final token = await _storage.read(key: _tokenKey);
_restoring = true;
try {
// read token from secure storage (with fallback)
final token = await _tokenStorage.getToken();
if (token == null || token.isEmpty) return;
final userResponse = await api.get(
@@ -103,11 +101,11 @@ class AuthService extends ChangeNotifier {
}
Future<void> _persistToken(String token) async {
await _storage.write(key: _tokenKey, value: token);
await _tokenStorage.setToken(token);
}
Future<void> _clearToken() async {
await _storage.delete(key: _tokenKey);
await _tokenStorage.clearToken();
}
Future<void> register({

View File

@@ -61,6 +61,10 @@ class DataService extends ChangeNotifier {
List<String> get locoClasses => _locoClasses;
List<TripSummary> _tripList = [];
List<TripSummary> get tripList => _tripList;
List<EventField> _eventFields = [];
List<EventField> get eventFields => _eventFields;
bool _isEventFieldsLoading = false;
bool get isEventFieldsLoading => _isEventFieldsLoading;
// Station Data
List<Station>? _cachedStations;
@@ -73,6 +77,17 @@ class DataService extends ChangeNotifier {
bool _isOnThisDayLoading = false;
bool get isOnThisDayLoading => _isOnThisDayLoading;
static const List<EventField> _fallbackEventFields = [
EventField(name: 'operator', display: 'Operator'),
EventField(name: 'status', display: 'Status'),
EventField(name: 'evn', display: 'EVN'),
EventField(name: 'owner', display: 'Owner'),
EventField(name: 'location', display: 'Location'),
EventField(name: 'livery', display: 'Livery'),
EventField(name: 'domain', display: 'Domain'),
EventField(name: 'type', display: 'Type'),
];
void _notifyAsync() {
// Always defer to the next frame to avoid setState during build.
SchedulerBinding.instance.addPostFrameCallback((_) {
@@ -260,6 +275,75 @@ class DataService extends ChangeNotifier {
}
}
Future<List<TripLocoStat>> fetchTripLocoStats(int tripId) async {
try {
final json = await api.get('/trips/stats?trip_id=$tripId');
if (json is List) {
return json
.whereType<Map<String, dynamic>>()
.map((e) => TripLocoStat.fromJson(e))
.toList();
}
if (json is Map && json['locos'] is List) {
return (json['locos'] as List)
.whereType<Map<String, dynamic>>()
.map((e) => TripLocoStat.fromJson(e))
.toList();
}
return [];
} catch (e) {
debugPrint('Failed to fetch trip loco stats: $e');
return [];
}
}
Future<void> fetchEventFields({bool force = false}) async {
if (_eventFields.isNotEmpty && !force) return;
_isEventFieldsLoading = true;
_notifyAsync();
try {
final json = await api.get('/event/fields');
List<EventField> fields = _parseEventFields(json);
if (fields.isEmpty) {
fields = _fallbackEventFields;
}
_eventFields = fields;
} catch (e) {
debugPrint('Failed to fetch event fields: $e');
_eventFields = _fallbackEventFields;
} finally {
_isEventFieldsLoading = false;
_notifyAsync();
}
}
List<EventField> _parseEventFields(dynamic json) {
if (json is List) {
return json
.whereType<Map<String, dynamic>>()
.map(EventField.fromJson)
.toList();
}
if (json is Map) {
if (json['fields'] is List) {
return (json['fields'] as List)
.whereType<Map<String, dynamic>>()
.map(EventField.fromJson)
.toList();
}
// If map of name -> definition
return json.entries
.where((entry) => entry.value is Map<String, dynamic>)
.map((entry) {
final map = Map<String, dynamic>.from(entry.value);
map['name'] = entry.key;
return EventField.fromJson(map);
})
.toList();
}
return [];
}
Future<void> fetchTrips() async {
try {
final json = await api.get('/trips/mileage');
@@ -314,6 +398,7 @@ class DataService extends ChangeNotifier {
_onThisDay = [];
_trips = [];
_tripDetails = [];
_eventFields = [];
_notifyAsync();
}

View File

@@ -1,7 +1,9 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Stores the auth token in secure storage and falls back to SharedPreferences
/// so debug builds and platforms without a working keyring still persist.
class TokenStorageService {
// Singleton pattern (optional but usually handy for services)
TokenStorageService._internal();
static final TokenStorageService _instance = TokenStorageService._internal();
@@ -9,26 +11,45 @@ class TokenStorageService {
factory TokenStorageService() => _instance;
static const _tokenKey = 'auth_token';
final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();
// Use const constructor for secure storage
final FlutterSecureStorage _storage = const FlutterSecureStorage();
Future<SharedPreferences> get _prefs async =>
await SharedPreferences.getInstance();
/// Save or update the token
Future<void> setToken(String token) async {
await _storage.write(key: _tokenKey, value: token);
try {
await _secureStorage.write(key: _tokenKey, value: token);
} catch (_) {
// ignore secure storage failures in debug/unsupported environments
}
final prefs = await _prefs;
await prefs.setString(_tokenKey, token);
}
/// Retrieve the stored token (null if none)
Future<String?> getToken() async {
return _storage.read(key: _tokenKey);
try {
final secured = await _secureStorage.read(key: _tokenKey);
if (secured != null && secured.isNotEmpty) {
return secured;
}
} catch (_) {
// ignore and fall back
}
final prefs = await _prefs;
final token = prefs.getString(_tokenKey);
return (token == null || token.isEmpty) ? null : token;
}
/// Delete the token
Future<void> clearToken() async {
await _storage.delete(key: _tokenKey);
try {
await _secureStorage.delete(key: _tokenKey);
} catch (_) {
// ignore
}
final prefs = await _prefs;
await prefs.remove(_tokenKey);
}
/// Optional: check quickly if a token exists
Future<bool> hasToken() async {
final token = await getToken();
return token != null && token.isNotEmpty;