commit from livecd

This commit is contained in:
2025-07-24 13:46:46 +01:00
parent 9fbdd97b15
commit e6ed9d01c2
6 changed files with 251 additions and 38 deletions

View File

@@ -2,15 +2,41 @@ import 'package:flutter/material.dart';
import 'package:mileograph_flutter/services/authservice.dart';
import 'package:provider/provider.dart';
class LoginPage extends StatelessWidget {
const LoginPage({super.key});
class LoginScreen extends StatelessWidget {
const LoginScreen({super.key});
@override
Widget build(BuildContext context) {
return Container(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [LoginPanel()],
return Scaffold(
resizeToAvoidBottomInset: true,
body: Container(
color: Theme.of(context).scaffoldBackgroundColor,
child: Center(
child: Column(
spacing: 50,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text.rich(
TextSpan(
children: [
TextSpan(text: "Mile"),
TextSpan(
text: "O",
style: TextStyle(color: Colors.red),
),
TextSpan(text: "graph"),
],
style: TextStyle(
decoration: TextDecoration.none,
color: Colors.white,
fontFamily: "Tomatoes",
fontSize: 50,
),
),
),
LoginPanel(),
],
),
),
),
);
@@ -99,6 +125,7 @@ class _LoginPanelContentState extends State<LoginPanelContent> {
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
spacing: 8,
children: [
Padding(
@@ -149,6 +176,7 @@ class RegisterPanelContent extends StatelessWidget {
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
spacing: 8,
children: [
Row(

View File

@@ -1,19 +1,36 @@
import 'package:flutter/material.dart';
import 'package:mileograph_flutter/services/apiService.dart';
import 'package:mileograph_flutter/services/authservice.dart';
import 'package:mileograph_flutter/services/dataService.dart';
import 'package:provider/provider.dart';
import 'components/login/login.dart';
late ApiService api;
void main() {
runApp(
MultiProvider(
providers: [
Provider<ApiService>(
create: (_) =>
ApiService(baseUrl: 'https://dev.mileograph.co.uk/api/v1'),
create: (_) {
api = ApiService(baseUrl: 'https://dev.mileograph.co.uk/api/v1');
return api;
},
),
ChangeNotifierProvider<AuthService>(
ChangeNotifierProxyProvider<ApiService, AuthService>(
create: (context) => AuthService(api: context.read<ApiService>()),
update: (_, api, previous) {
return previous ?? AuthService(api: api);
},
),
ProxyProvider<AuthService, void>(
update: (_, auth, __) {
api.setTokenProvider(() => auth.token);
},
),
ChangeNotifierProxyProvider<ApiService, DataService>(
create: (context) => DataService(api: context.read<ApiService>()),
update: (_, api, previous) => previous ?? DataService(api: api),
),
],
child: MyApp(),
@@ -21,25 +38,29 @@ void main() {
);
}
class AppRoot extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<AuthService>(
builder: (context, auth, child) {
return auth.isLoggedIn
? MyHomePage(title: "Mileograph")
: LoginScreen();
},
);
}
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
final auth = context.read<AuthService>();
Widget fullPage;
Widget subPage;
if (!auth.isLoggedIn) {
fullPage = LoginPage();
} else {
fullPage = MyHomePage(title: "Mileograph");
}
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
useMaterial3: true,
// This is the theme of your application.
//
// TRY THIS: Try running your application with "flutter run". You'll see
@@ -48,7 +69,7 @@ class MyApp extends StatelessWidget {
// and then invoke "hot reload" (save your changes or press the "hot
// reload" button in a Flutter-supported IDE, or press "r" if you used
// the command line to start the app).
//
//fullPage
// Notice that the counter didn't reset back to zero; the application
// state is not lost during the reload. To reset the state, use hot
// restart instead.
@@ -58,13 +79,14 @@ class MyApp extends StatelessWidget {
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blueGrey),
),
darkTheme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blueGrey,
brightness: Brightness.dark,
),
),
themeMode: ThemeMode.system,
home: const MyHomePage(title: 'Mile-O-Graph'),
home: AppRoot(),
);
}
}
@@ -89,10 +111,41 @@ class MyHomePage extends StatefulWidget {
class _MyHomePageState extends State<MyHomePage> {
int pageIndex = 0;
final List<Widget> contentPages = [
Dashboard(),
Center(child: Text("Calculator Page")),
Center(child: Text("Entries Page")),
Center(child: Text("Traction Page")),
Center(child: Text("Trips Page")),
];
bool loggedIn = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final data = context.read<DataService>();
if (data.homepageStats == null) {
data.fetchHomepageStats();
}
}
@override
Widget build(BuildContext context) {
Widget currentPage;
final data = context.watch<DataService>();
if (data.homepageStats != null) {
currentPage = contentPages[pageIndex];
} else {
currentPage = Center(
child: FilledButton(
onPressed: data.fetchHomepageStats,
child: Text("Fetch"),
),
);
}
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
@@ -108,10 +161,8 @@ class _MyHomePageState extends State<MyHomePage> {
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
leading: IconButton(onPressed: null, icon: Icon(Icons.menu)),
actions: [
IconButton(onPressed: null, icon: Icon(Icons.account_circle)),
IconButton(onPressed: null, icon: Icon(Icons.settings)),
],
),
bottomNavigationBar: NavigationBar(
@@ -123,16 +174,13 @@ class _MyHomePageState extends State<MyHomePage> {
},
destinations: [
NavigationDestination(icon: Icon(Icons.home), label: "Home"),
NavigationDestination(icon: Icon(Icons.route), label: "Calculator"),
NavigationDestination(icon: Icon(Icons.list), label: "Entries"),
NavigationDestination(icon: Icon(Icons.train), label: "Traction"),
NavigationDestination(icon: Icon(Icons.route), label: "Trips"),
NavigationDestination(
icon: Icon(Icons.account_circle),
label: "User",
),
NavigationDestination(icon: Icon(Icons.book), label: "Trips"),
],
),
body: Dashboard(),
body: currentPage,
floatingActionButton: FloatingActionButton(
onPressed: null,
tooltip: 'New Entry',

View File

@@ -34,3 +34,120 @@ class AuthenticatedUserData extends UserData {
final String access_token;
}
class HomepageStats {
final double totalMileage;
final List<YearlyMileage> yearlyMileage;
final List<LocoSummary> topLocos;
final List<LeaderboardEntry> leaderboard;
final List<TripSummary> trips;
HomepageStats({
required this.totalMileage,
required this.yearlyMileage,
required this.topLocos,
required this.leaderboard,
required this.trips,
});
factory HomepageStats.fromJson(Map<String, dynamic> json) {
return HomepageStats(
totalMileage: (json['milage_data']['mileage'] as num).toDouble(),
yearlyMileage: (json['yearly_mileage'] as List)
.map((e) => YearlyMileage.fromJson(e))
.toList(),
topLocos: (json['top_locos'] as List)
.map((e) => LocoSummary.fromJson(e))
.toList(),
leaderboard: (json['leaderboard_data'] as List)
.map((e) => LeaderboardEntry.fromJson(e))
.toList(),
trips: (json['trip_data'] as List)
.map((e) => TripSummary.fromJson(e))
.toList(),
);
}
}
class YearlyMileage {
final int? year;
final double mileage;
YearlyMileage({this.year, required this.mileage});
factory YearlyMileage.fromJson(Map<String, dynamic> json) => YearlyMileage(
year: json['year'],
mileage: (json['mileage'] as num).toDouble(),
);
}
class LocoSummary {
final String locoType, locoClass, locoNumber, locoName, locoOperator;
final String? locoNotes, locoEvn;
final int locoId, locoJourneys;
final double locoMileage;
LocoSummary({
required this.locoType,
required this.locoClass,
required this.locoNumber,
required this.locoName,
required this.locoOperator,
this.locoNotes,
this.locoEvn,
required this.locoId,
required this.locoMileage,
required this.locoJourneys,
});
factory LocoSummary.fromJson(Map<String, dynamic> json) => LocoSummary(
locoType: json['loco_type'],
locoClass: json['loco_class'],
locoNumber: json['loco_number'],
locoName: json['loco_name'],
locoOperator: json['loco_operator'],
locoNotes: json['loco_notes'],
locoEvn: json['loco_evn'],
locoId: json['loco_id'],
locoMileage: (json['loco_mileage'] as num).toDouble(),
locoJourneys: json['loco_journeys'],
);
}
class LeaderboardEntry {
final String userId, username, userFullName;
final double mileage;
LeaderboardEntry({
required this.userId,
required this.username,
required this.userFullName,
required this.mileage,
});
factory LeaderboardEntry.fromJson(Map<String, dynamic> json) =>
LeaderboardEntry(
userId: json['user_id'],
username: json['username'],
userFullName: json['user_full_name'],
mileage: (json['mileage'] as num).toDouble(),
);
}
class TripSummary {
final int tripId;
final String tripName;
final double tripMileage;
TripSummary({
required this.tripId,
required this.tripName,
required this.tripMileage,
});
factory TripSummary.fromJson(Map<String, dynamic> json) => TripSummary(
tripId: json['trip_id'],
tripName: json['trip_name'],
tripMileage: (json['trip_mileage'] as num).toDouble(),
);
}

View File

@@ -1,15 +1,32 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:mileograph_flutter/services/authservice.dart';
typedef TokenProvider = String? Function();
class ApiService {
final String baseUrl;
TokenProvider? _getToken;
ApiService({required this.baseUrl});
void setTokenProvider(TokenProvider provider) {
_getToken = provider;
}
Map<String, String> _buildHeaders(Map<String, String>? extra) {
final token = _getToken?.call();
final headers = {'accept': 'application/json', ...?extra};
if (token != null && token.isNotEmpty) {
headers['Authorization'] = 'Bearer $token';
}
return headers;
}
Future<dynamic> get(String endpoint, {Map<String, String>? headers}) async {
final response = await http.get(
Uri.parse('$baseUrl$endpoint'),
headers: headers,
headers: _buildHeaders(headers),
);
return _processResponse(response);
}
@@ -21,7 +38,7 @@ class ApiService {
}) async {
final response = await http.post(
Uri.parse('$baseUrl$endpoint'),
headers: _jsonHeaders(headers),
headers: _buildHeaders(_jsonHeaders(headers)),
body: jsonEncode(data),
);
return _processResponse(response);
@@ -30,10 +47,10 @@ class ApiService {
Future<dynamic> postForm(String endpoint, Map<String, String> data) async {
final response = await http.post(
Uri.parse('$baseUrl$endpoint'),
headers: {
headers: _buildHeaders({
'Content-Type': 'application/x-www-form-urlencoded',
'accept': 'application/json',
},
}),
body: data, // http package handles form-encoding for Map<String, String>
);
return _processResponse(response);
@@ -46,7 +63,7 @@ class ApiService {
}) async {
final response = await http.put(
Uri.parse('$baseUrl$endpoint'),
headers: _jsonHeaders(headers),
headers: _buildHeaders(_jsonHeaders(headers)),
body: jsonEncode(data),
);
return _processResponse(response);
@@ -58,7 +75,7 @@ class ApiService {
}) async {
final response = await http.delete(
Uri.parse('$baseUrl$endpoint'),
headers: headers,
headers: _buildHeaders(headers),
);
return _processResponse(response);
}

View File

@@ -57,7 +57,10 @@ flutter:
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
fonts:
- family: Tomatoes
fonts:
- asset: lib/assets/fonts/Tomatoes-O8L8.ttf
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg

View File

@@ -8,7 +8,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mileograph_sbb_flutter/main.dart';
import 'package:mileograph_flutter/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {