4 Commits

Author SHA1 Message Date
8116cfe7b1 fix secure storage and release pipeline
All checks were successful
Release / meta (push) Successful in 19s
Release / android-build (push) Successful in 5m53s
Release / linux-build (push) Successful in 8m3s
Release / release-master (push) Successful in 2s
Release / release-dev (push) Successful in 32s
2025-12-14 08:39:22 +00:00
4d483495fc add secure storage (not working)
Some checks failed
Release / meta (push) Successful in 2s
Release / android-build (push) Successful in 6m25s
Release / release-dev (push) Has been skipped
Release / release-master (push) Has been skipped
Release / linux-build (push) Failing after 2m16s
2025-12-12 09:58:52 +00:00
292163bda6 Fix trips on entry page, correct order of trips on dashboard and trip page, move to prod api
Some checks failed
Release / meta (push) Successful in 17s
Release / linux-build (push) Successful in 1m33s
Release / android-build (push) Successful in 15m28s
Release / release-dev (push) Failing after 4s
Release / release-master (push) Successful in 29s
2025-12-12 09:17:18 +00:00
53eaf0b4af Update .gitea/workflows/release.yml
Some checks failed
Release / meta (push) Successful in 17s
Release / linux-build (push) Successful in 1m35s
Release / android-build (push) Successful in 15m21s
Release / release-dev (push) Failing after 4s
Release / release-master (push) Successful in 24s
2025-12-12 08:17:59 +00:00
16 changed files with 353 additions and 164 deletions

View File

@@ -107,7 +107,7 @@ jobs:
SUDO="" SUDO=""
fi fi
$SUDO apt-get update $SUDO apt-get update
$SUDO apt-get install -y unzip xz-utils zip libstdc++6 libglu1-mesa clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev curl jq $SUDO apt-get install -y unzip xz-utils zip libstdc++6 libglu1-mesa clang cmake ninja-build pkg-config libgtk-3-dev libsecret-1-dev liblzma-dev curl jq
- name: Setup Flutter - name: Setup Flutter
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
@@ -134,46 +134,6 @@ jobs:
name: linux-bundle name: linux-bundle
path: app-linux-x64.tar.gz path: app-linux-x64.tar.gz
windows-build:
runs-on: windows-latest
needs: meta
# Job always runs; steps are conditional so it won't block anything when Windows is disabled.
steps:
- name: Checkout
if: ${{ env.BUILD_WINDOWS == 'true' }}
uses: actions/checkout@v4
- name: Setup Flutter
if: ${{ env.BUILD_WINDOWS == 'true' }}
uses: subosito/flutter-action@v2
with:
channel: ${{ env.FLUTTER_CHANNEL }}
- name: Allow all git directories (CI)
if: ${{ env.BUILD_WINDOWS == 'true' }}
run: git config --global --add safe.directory '*'
- name: Flutter dependencies
if: ${{ env.BUILD_WINDOWS == 'true' }}
run: flutter pub get
- name: Enable Windows desktop
if: ${{ env.BUILD_WINDOWS == 'true' }}
run: flutter config --enable-windows-desktop
- name: Build Windows binary (release)
if: ${{ env.BUILD_WINDOWS == 'true' }}
run: |
flutter build windows --release
powershell -Command "Compress-Archive -Path build/windows/x64/runner/Release/* -DestinationPath app-windows-x64.zip"
- name: Upload Windows artifact
if: ${{ env.BUILD_WINDOWS == 'true' }}
uses: actions/upload-artifact@v3
with:
name: windows-zip
path: app-windows-x64.zip
release-dev: release-dev:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
@@ -181,6 +141,15 @@ jobs:
- android-build - android-build
- linux-build - linux-build
steps: steps:
- name: Install jq
run: |
if command -v sudo >/dev/null 2>&1; then
SUDO="sudo"
else
SUDO=""
fi
$SUDO apt-get update
$SUDO apt-get install -y jq
- name: Download Android APK - name: Download Android APK
if: ${{ github.ref == 'refs/heads/dev' }} if: ${{ github.ref == 'refs/heads/dev' }}
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
@@ -260,7 +229,6 @@ jobs:
>/dev/null >/dev/null
done done
release-master: release-master:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:

View File

@@ -39,11 +39,7 @@ class Dashboard extends StatelessWidget {
children: [ children: [
_buildHeader(context, auth, stats, data.isHomepageLoading), _buildHeader(context, auth, stats, data.isHomepageLoading),
const SizedBox(height: 12), const SizedBox(height: 12),
Wrap( Wrap(spacing: 12, runSpacing: 12, children: metricChips),
spacing: 12,
runSpacing: 12,
children: metricChips,
),
const SizedBox(height: 16), const SizedBox(height: 16),
isWide isWide
? Row( ? Row(
@@ -71,8 +67,12 @@ class Dashboard extends StatelessWidget {
); );
} }
Widget _buildHeader(BuildContext context, AuthService auth, Widget _buildHeader(
HomepageStats? stats, bool loading) { BuildContext context,
AuthService auth,
HomepageStats? stats,
bool loading,
) {
final greetingName = final greetingName =
stats?.user?.full_name ?? auth.fullName ?? auth.username ?? 'there'; stats?.user?.full_name ?? auth.fullName ?? auth.username ?? 'there';
return Row( return Row(
@@ -82,10 +82,7 @@ class Dashboard extends StatelessWidget {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text('Dashboard', style: Theme.of(context).textTheme.labelMedium),
'Dashboard',
style: Theme.of(context).textTheme.labelMedium,
),
const SizedBox(height: 2), const SizedBox(height: 2),
Text( Text(
'Welcome back, $greetingName', 'Welcome back, $greetingName',
@@ -93,7 +90,8 @@ class Dashboard extends StatelessWidget {
), ),
], ],
), ),
if (loading) const Padding( if (loading)
const Padding(
padding: EdgeInsets.only(right: 8.0), padding: EdgeInsets.only(right: 8.0),
child: SizedBox( child: SizedBox(
height: 24, height: 24,
@@ -121,11 +119,13 @@ class Dashboard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text(label.toUpperCase(), Text(
label.toUpperCase(),
style: textTheme.labelSmall?.copyWith( style: textTheme.labelSmall?.copyWith(
letterSpacing: 0.7, letterSpacing: 0.7,
color: textTheme.bodySmall?.color?.withOpacity(0.7), color: textTheme.bodySmall?.color?.withOpacity(0.7),
)), ),
),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
value, value,
@@ -203,10 +203,9 @@ class Dashboard extends StatelessWidget {
children: [ children: [
Text( Text(
title, title,
style: Theme.of(context) style: Theme.of(context).textTheme.titleMedium?.copyWith(
.textTheme fontWeight: FontWeight.w700,
.titleMedium ),
?.copyWith(fontWeight: FontWeight.w700),
), ),
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -234,10 +233,7 @@ class Dashboard extends StatelessWidget {
required String emptyMessage, required String emptyMessage,
}) { }) {
if (legs.isEmpty) { if (legs.isEmpty) {
return Text( return Text(emptyMessage, style: Theme.of(context).textTheme.bodyMedium);
emptyMessage,
style: Theme.of(context).textTheme.bodyMedium,
);
} }
return Column( return Column(
children: legs.take(5).map((leg) { children: legs.take(5).map((leg) {
@@ -257,10 +253,9 @@ class Dashboard extends StatelessWidget {
if (leg.headcode.isNotEmpty) if (leg.headcode.isNotEmpty)
Text( Text(
leg.headcode, leg.headcode,
style: Theme.of(context) style: Theme.of(context).textTheme.labelSmall?.copyWith(
.textTheme color: Theme.of(context).hintColor,
.labelSmall ),
?.copyWith(color: Theme.of(context).hintColor),
), ),
], ],
), ),
@@ -286,7 +281,11 @@ class Dashboard extends StatelessWidget {
} }
Widget _buildTripsCard(BuildContext context, DataService data) { Widget _buildTripsCard(BuildContext context, DataService data) {
final trips = data.trips; final trips_unsorted = data.trips;
List trips = [];
if (trips_unsorted.isNotEmpty) {
trips = [...trips_unsorted]..sort((a, b) => b.tripId.compareTo(a.tripId));
}
return _buildCard( return _buildCard(
context, context,
title: 'Trips', title: 'Trips',

View File

@@ -38,6 +38,8 @@ class _TractionPageState extends State<TractionPage> {
final _domainController = TextEditingController(); final _domainController = TextEditingController();
final _typeController = TextEditingController(); final _typeController = TextEditingController();
int offset = 0;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -239,8 +241,7 @@ class _TractionPageState extends State<TractionPage> {
onSubmitted: (_) => _refreshTraction(), onSubmitted: (_) => _refreshTraction(),
); );
}, },
optionsViewBuilder: optionsViewBuilder: (context, onSelected, options) {
(context, onSelected, options) {
final optionList = options.toList(); final optionList = options.toList();
if (optionList.isEmpty) { if (optionList.isEmpty) {
return const SizedBox.shrink(); return const SizedBox.shrink();
@@ -306,7 +307,7 @@ class _TractionPageState extends State<TractionPage> {
), ),
FilterChip( FilterChip(
label: Text( label: Text(
_mileageFirst ? 'Mileage first' : 'Had first', _mileageFirst ? 'Mileage first' : 'Number order',
), ),
selected: _mileageFirst, selected: _mileageFirst,
onSelected: (v) { onSelected: (v) {

View File

@@ -46,11 +46,15 @@ class _TripsPageState extends State<TripsPage> {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('Journeys', Text(
style: Theme.of(context).textTheme.labelMedium), 'Journeys',
style: Theme.of(context).textTheme.labelMedium,
),
const SizedBox(height: 2), const SizedBox(height: 2),
Text('Trips', Text(
style: Theme.of(context).textTheme.headlineSmall), 'Trips',
style: Theme.of(context).textTheme.headlineSmall,
),
], ],
), ),
Row( Row(
@@ -81,10 +85,9 @@ class _TripsPageState extends State<TripsPage> {
children: [ children: [
Text( Text(
'No trips yet', 'No trips yet',
style: Theme.of(context) style: Theme.of(context).textTheme.titleMedium?.copyWith(
.textTheme fontWeight: FontWeight.w700,
.titleMedium ),
?.copyWith(fontWeight: FontWeight.w700),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
const Text( const Text(
@@ -101,8 +104,9 @@ class _TripsPageState extends State<TripsPage> {
(trip) => Card( (trip) => Card(
child: ListTile( child: ListTile(
title: Text(trip.tripName), title: Text(trip.tripName),
subtitle: subtitle: Text(
Text('${trip.tripMileage.toStringAsFixed(1)} mi'), '${trip.tripMileage.toStringAsFixed(1)} mi',
),
), ),
), ),
) )
@@ -110,8 +114,9 @@ class _TripsPageState extends State<TripsPage> {
) )
else else
Column( Column(
children: children: tripDetails
tripDetails.map((trip) => _buildTripCard(context, trip, isMobile)).toList(), .map((trip) => _buildTripCard(context, trip, isMobile))
.toList(),
), ),
], ],
), ),
@@ -134,10 +139,9 @@ class _TripsPageState extends State<TripsPage> {
children: [ children: [
Text( Text(
trip.name, trip.name,
style: Theme.of(context) style: Theme.of(context).textTheme.titleMedium?.copyWith(
.textTheme fontWeight: FontWeight.w700,
.titleMedium ),
?.copyWith(fontWeight: FontWeight.w700),
), ),
Text( Text(
'${trip.mileage.toStringAsFixed(1)} mi · ${trip.legCount} legs', '${trip.mileage.toStringAsFixed(1)} mi · ${trip.legCount} legs',
@@ -145,6 +149,14 @@ class _TripsPageState extends State<TripsPage> {
), ),
], ],
), ),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
IconButton(
icon: const Icon(Icons.train),
tooltip: 'Traction',
onPressed: () => _showTripWinners(context, trip),
),
IconButton( IconButton(
icon: const Icon(Icons.open_in_new), icon: const Icon(Icons.open_in_new),
tooltip: 'Details', tooltip: 'Details',
@@ -152,6 +164,8 @@ class _TripsPageState extends State<TripsPage> {
), ),
], ],
), ),
],
),
const SizedBox(height: 8), const SizedBox(height: 8),
if (legs.isNotEmpty) if (legs.isNotEmpty)
Column( Column(
@@ -168,10 +182,9 @@ class _TripsPageState extends State<TripsPage> {
), ),
trailing: Text( trailing: Text(
leg.mileage?.toStringAsFixed(1) ?? '-', leg.mileage?.toStringAsFixed(1) ?? '-',
style: Theme.of(context) style: Theme.of(context).textTheme.labelLarge?.copyWith(
.textTheme fontWeight: FontWeight.bold,
.labelLarge ),
?.copyWith(fontWeight: FontWeight.bold),
), ),
); );
}).toList(), }).toList(),
@@ -215,10 +228,9 @@ class _TripsPageState extends State<TripsPage> {
), ),
Text( Text(
trip.name, trip.name,
style: Theme.of(context) style: Theme.of(context).textTheme.titleMedium?.copyWith(
.textTheme fontWeight: FontWeight.bold,
.titleMedium ),
?.copyWith(fontWeight: FontWeight.bold),
), ),
const Spacer(), const Spacer(),
Text('${trip.mileage.toStringAsFixed(1)} mi'), Text('${trip.mileage.toStringAsFixed(1)} mi'),
@@ -237,9 +249,63 @@ class _TripsPageState extends State<TripsPage> {
subtitle: Text(_formatDate(leg.beginTime)), subtitle: Text(_formatDate(leg.beginTime)),
trailing: Text( trailing: Text(
leg.mileage?.toStringAsFixed(1) ?? '-', leg.mileage?.toStringAsFixed(1) ?? '-',
style: Theme.of(context) style: Theme.of(context).textTheme.labelLarge
.textTheme ?.copyWith(fontWeight: FontWeight.bold),
.labelLarge ),
);
},
),
),
],
),
),
);
},
);
}
void _showTripWinners(BuildContext context, TripDetail trip) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (_) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).pop(),
),
Text(
trip.name,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const Spacer(),
Text('${trip.mileage.toStringAsFixed(1)} mi'),
],
),
const SizedBox(height: 8),
SizedBox(
height: MediaQuery.of(context).size.height * 0.6,
child: ListView.builder(
itemCount: trip.legs.length,
itemBuilder: (context, index) {
final leg = trip.legs[index];
return ListTile(
leading: const Icon(Icons.train),
title: Text('${leg.start}${leg.end}'),
subtitle: Text(_formatDate(leg.beginTime)),
trailing: Text(
leg.mileage?.toStringAsFixed(1) ?? '-',
style: Theme.of(context).textTheme.labelLarge
?.copyWith(fontWeight: FontWeight.bold), ?.copyWith(fontWeight: FontWeight.bold),
), ),
); );

View File

@@ -24,7 +24,7 @@ void main() {
providers: [ providers: [
Provider<ApiService>( Provider<ApiService>(
create: (_) { create: (_) {
api = ApiService(baseUrl: 'https://dev.mileograph.co.uk/api/v1'); api = ApiService(baseUrl: 'https://mileograph.co.uk/api/v1');
return api; return api;
}, },
), ),
@@ -37,6 +37,7 @@ void main() {
ProxyProvider<AuthService, void>( ProxyProvider<AuthService, void>(
update: (_, auth, __) { update: (_, auth, __) {
api.setTokenProvider(() => auth.token); api.setTokenProvider(() => auth.token);
api.setUnauthorizedHandler(() => auth.handleTokenExpired());
}, },
), ),
ChangeNotifierProxyProvider<ApiService, DataService>( ChangeNotifierProxyProvider<ApiService, DataService>(

View File

@@ -2,10 +2,12 @@ import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
typedef TokenProvider = String? Function(); typedef TokenProvider = String? Function();
typedef UnauthorizedHandler = Future<void> Function();
class ApiService { class ApiService {
final String baseUrl; final String baseUrl;
TokenProvider? _getToken; TokenProvider? _getToken;
UnauthorizedHandler? _onUnauthorized;
ApiService({required this.baseUrl}); ApiService({required this.baseUrl});
@@ -13,6 +15,10 @@ class ApiService {
_getToken = provider; _getToken = provider;
} }
void setUnauthorizedHandler(UnauthorizedHandler handler) {
_onUnauthorized = handler;
}
Map<String, String> _buildHeaders(Map<String, String>? extra) { Map<String, String> _buildHeaders(Map<String, String>? extra) {
final token = _getToken?.call(); final token = _getToken?.call();
final headers = {'accept': 'application/json', ...?extra}; final headers = {'accept': 'application/json', ...?extra};
@@ -85,12 +91,19 @@ class ApiService {
return {'Content-Type': 'application/json', if (extra != null) ...extra}; return {'Content-Type': 'application/json', if (extra != null) ...extra};
} }
dynamic _processResponse(http.Response res) { Future<dynamic> _processResponse(http.Response res) async {
final body = res.body.isNotEmpty ? jsonDecode(res.body) : null; final body = res.body.isNotEmpty ? jsonDecode(res.body) : null;
if (res.statusCode >= 200 && res.statusCode < 300) { if (res.statusCode >= 200 && res.statusCode < 300) {
return body; return body;
} else { }
if (res.statusCode == 401 &&
body is Map<String, dynamic> &&
body['detail'] == 'Not authenticated' &&
_onUnauthorized != null) {
await _onUnauthorized!();
}
throw Exception('API error ${res.statusCode}: $body'); throw Exception('API error ${res.statusCode}: $body');
} }
} }
}

View File

@@ -1,13 +1,16 @@
import 'package:flutter/foundation.dart'; 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/objects/objects.dart';
import 'package:mileograph_flutter/services/apiService.dart'; import 'package:mileograph_flutter/services/apiService.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AuthService extends ChangeNotifier { class AuthService extends ChangeNotifier {
final ApiService api; final ApiService api;
static const _tokenKey = 'auth_token'; static const _tokenKey = 'auth_token';
bool _restoring = false; bool _restoring = false;
// secure storage instance
final FlutterSecureStorage _storage = const FlutterSecureStorage();
AuthService({required this.api}); AuthService({required this.api});
AuthenticatedUserData? _user; AuthenticatedUserData? _user;
@@ -73,9 +76,10 @@ class AuthService extends ChangeNotifier {
if (_restoring || _user != null) return; if (_restoring || _user != null) return;
_restoring = true; _restoring = true;
try { try {
final prefs = await SharedPreferences.getInstance(); // read token from secure storage
final token = prefs.getString(_tokenKey); final token = await _storage.read(key: _tokenKey);
if (token == null || token.isEmpty) return; if (token == null || token.isEmpty) return;
final userResponse = await api.get( final userResponse = await api.get(
'/users/me', '/users/me',
headers: { headers: {
@@ -83,6 +87,7 @@ class AuthService extends ChangeNotifier {
'accept': 'application/json', 'accept': 'application/json',
}, },
); );
setLoginData( setLoginData(
userId: userResponse['user_id'], userId: userResponse['user_id'],
username: userResponse['username'], username: userResponse['username'],
@@ -98,13 +103,11 @@ class AuthService extends ChangeNotifier {
} }
Future<void> _persistToken(String token) async { Future<void> _persistToken(String token) async {
final prefs = await SharedPreferences.getInstance(); await _storage.write(key: _tokenKey, value: token);
await prefs.setString(_tokenKey, token);
} }
Future<void> _clearToken() async { Future<void> _clearToken() async {
final prefs = await SharedPreferences.getInstance(); await _storage.delete(key: _tokenKey);
await prefs.remove(_tokenKey);
} }
Future<void> register({ Future<void> register({
@@ -126,9 +129,13 @@ class AuthService extends ChangeNotifier {
await api.postForm('/register', formData); await api.postForm('/register', formData);
} }
void logout() { Future<void> handleTokenExpired() async {
_user = null; _user = null;
_clearToken(); await _clearToken();
notifyListeners(); notifyListeners();
} }
void logout() {
handleTokenExpired(); // reuse
}
} }

View File

@@ -117,7 +117,8 @@ class DataService extends ChangeNotifier {
); );
} }
final buffer = StringBuffer( final buffer = StringBuffer(
'?sort_direction=$sortDirection&sort_by=$sortBy&offset=$offset&limit=$limit'); '?sort_direction=$sortDirection&sort_by=$sortBy&offset=$offset&limit=$limit',
);
if (dateRangeStart != null && dateRangeStart.isNotEmpty) { if (dateRangeStart != null && dateRangeStart.isNotEmpty) {
buffer.write('&date_range_start=$dateRangeStart'); buffer.write('&date_range_start=$dateRangeStart');
} }
@@ -178,7 +179,7 @@ class DataService extends ChangeNotifier {
try { try {
final params = StringBuffer('?limit=$limit&offset=$offset'); final params = StringBuffer('?limit=$limit&offset=$offset');
if (hadOnly) params.write('&had_only=true'); if (hadOnly) params.write('&had_only=true');
if (mileageFirst) params.write('&mileage_first=true'); if (!mileageFirst) params.write('&mileage_first=false');
final payload = <String, dynamic>{}; final payload = <String, dynamic>{};
if (locoClass != null && locoClass.isNotEmpty) { if (locoClass != null && locoClass.isNotEmpty) {
@@ -203,7 +204,7 @@ class DataService extends ChangeNotifier {
if (json is List) { if (json is List) {
final newItems = json.map((e) => LocoSummary.fromJson(e)).toList(); final newItems = json.map((e) => LocoSummary.fromJson(e)).toList();
_traction = append ? [..._traction, ...newItems] : newItems; _traction = append ? [..._traction, ...newItems] : newItems;
_tractionHasMore = newItems.length >= limit; _tractionHasMore = newItems.length >= limit - 1;
} else { } else {
throw Exception('Unexpected traction response: $json'); throw Exception('Unexpected traction response: $json');
} }
@@ -245,12 +246,13 @@ class DataService extends ChangeNotifier {
try { try {
final json = await api.get('/trips/legs-and-stats'); final json = await api.get('/trips/legs-and-stats');
if (json is List) { if (json is List) {
_tripDetails = json.map((e) => TripDetail.fromJson(e)).toList(); final trip_map = json.map((e) => TripDetail.fromJson(e)).toList();
_tripDetails = [...trip_map]..sort((a, b) => b.id.compareTo(a.id));
} else { } else {
_tripDetails = []; _tripDetails = [];
} }
} catch (e) { } catch (e) {
debugPrint('Failed to fetch trips: $e'); debugPrint('Failed to fetch trip_map: $e');
_tripDetails = []; _tripDetails = [];
} finally { } finally {
_isTripDetailsLoading = false; _isTripDetailsLoading = false;
@@ -260,7 +262,7 @@ class DataService extends ChangeNotifier {
Future<void> fetchTrips() async { Future<void> fetchTrips() async {
try { try {
final json = await api.get('/trips'); final json = await api.get('/trips/mileage');
Iterable<dynamic>? raw; Iterable<dynamic>? raw;
if (json is List) { if (json is List) {
raw = json; raw = json;
@@ -274,10 +276,12 @@ class DataService extends ChangeNotifier {
} }
} }
if (raw != null) { if (raw != null) {
_tripList = raw final trip_map = raw
.whereType<Map<String, dynamic>>() .whereType<Map<String, dynamic>>()
.map((e) => TripSummary.fromJson(e)) .map((e) => TripSummary.fromJson(e))
.toList(); .toList();
_tripList = [...trip_map]..sort((a, b) => b.tripId.compareTo(a.tripId));
} else { } else {
debugPrint('Unexpected trip list response: $json'); debugPrint('Unexpected trip list response: $json');
_tripList = []; _tripList = [];

View File

@@ -0,0 +1,36 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class TokenStorageService {
// Singleton pattern (optional but usually handy for services)
TokenStorageService._internal();
static final TokenStorageService _instance = TokenStorageService._internal();
factory TokenStorageService() => _instance;
static const _tokenKey = 'auth_token';
// Use const constructor for secure storage
final FlutterSecureStorage _storage = const FlutterSecureStorage();
/// Save or update the token
Future<void> setToken(String token) async {
await _storage.write(key: _tokenKey, value: token);
}
/// Retrieve the stored token (null if none)
Future<String?> getToken() async {
return _storage.read(key: _tokenKey);
}
/// Delete the token
Future<void> clearToken() async {
await _storage.delete(key: _tokenKey);
}
/// Optional: check quickly if a token exists
Future<bool> hasToken() async {
final token = await getToken();
return token != null && token.isNotEmpty;
}
}

View File

@@ -7,9 +7,13 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <dynamic_color/dynamic_color_plugin.h> #include <dynamic_color/dynamic_color_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) dynamic_color_registrar = g_autoptr(FlPluginRegistrar) dynamic_color_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin");
dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); dynamic_color_plugin_register_with_registrar(dynamic_color_registrar);
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
} }

View File

@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
dynamic_color dynamic_color
flutter_secure_storage_linux
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@@ -6,9 +6,13 @@ import FlutterMacOS
import Foundation import Foundation
import dynamic_color import dynamic_color
import flutter_secure_storage_darwin
import path_provider_foundation
import shared_preferences_foundation import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
} }

View File

@@ -94,6 +94,54 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.0" version: "6.0.0"
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_secure_storage
sha256: da922f2aab2d733db7e011a6bcc4a825b844892d4edd6df83ff156b09a9b2e40
url: "https://pub.dev"
source: hosted
version: "10.0.0"
flutter_secure_storage_darwin:
dependency: transitive
description:
name: flutter_secure_storage_darwin
sha256: "8878c25136a79def1668c75985e8e193d9d7d095453ec28730da0315dc69aee3"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
flutter_secure_storage_linux:
dependency: transitive
description:
name: flutter_secure_storage_linux
sha256: "2b5c76dce569ab752d55a1cee6a2242bcc11fdba927078fb88c503f150767cda"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
name: flutter_secure_storage_platform_interface
sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
flutter_secure_storage_web:
dependency: transitive
description:
name: flutter_secure_storage_web
sha256: "6a1137df62b84b54261dca582c1c09ea72f4f9a4b2fcee21b025964132d5d0c3"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
flutter_secure_storage_windows:
dependency: transitive
description:
name: flutter_secure_storage_windows
sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@@ -216,6 +264,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" version: "1.9.1"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "3b4c1fc3aa55ddc9cd4aa6759984330d5c8e66aa7702a6223c61540dc6380c37"
url: "https://pub.dev"
source: hosted
version: "2.2.19"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
@@ -405,6 +477,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" version: "1.1.1"
win32:
dependency: transitive
description:
name: win32
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
url: "https://pub.dev"
source: hosted
version: "5.15.0"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:

View File

@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 0.1.1+1 version: 0.1.2+1
environment: environment:
sdk: ^3.8.1 sdk: ^3.8.1
@@ -35,6 +35,7 @@ dependencies:
http: ^1.4.0 http: ^1.4.0
provider: ^6.1.5 provider: ^6.1.5
dynamic_color: ^1.6.6 dynamic_color: ^1.6.6
flutter_secure_storage: ^10.0.0
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.

View File

@@ -7,8 +7,11 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <dynamic_color/dynamic_color_plugin_c_api.h> #include <dynamic_color/dynamic_color_plugin_c_api.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
DynamicColorPluginCApiRegisterWithRegistrar( DynamicColorPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
} }

View File

@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
dynamic_color dynamic_color
flutter_secure_storage_windows
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST