diff --git a/lib/components/login/login.dart b/lib/components/login/login.dart index 4ce91b9..5e3e691 100644 --- a/lib/components/login/login.dart +++ b/lib/components/login/login.dart @@ -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 { 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( diff --git a/lib/main.dart b/lib/main.dart index 721f8a5..c497338 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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( - create: (_) => - ApiService(baseUrl: 'https://dev.mileograph.co.uk/api/v1'), + create: (_) { + api = ApiService(baseUrl: 'https://dev.mileograph.co.uk/api/v1'); + return api; + }, ), - ChangeNotifierProvider( + ChangeNotifierProxyProvider( create: (context) => AuthService(api: context.read()), + update: (_, api, previous) { + return previous ?? AuthService(api: api); + }, + ), + ProxyProvider( + update: (_, auth, __) { + api.setTokenProvider(() => auth.token); + }, + ), + ChangeNotifierProxyProvider( + create: (context) => DataService(api: context.read()), + 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( + 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(); - - 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 { int pageIndex = 0; + final List 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(); + if (data.homepageStats == null) { + data.fetchHomepageStats(); + } + } + @override Widget build(BuildContext context) { + Widget currentPage; + + final data = context.watch(); + + 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 { // 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 { }, 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', diff --git a/lib/objects/objects.dart b/lib/objects/objects.dart index 02d523e..26d2317 100644 --- a/lib/objects/objects.dart +++ b/lib/objects/objects.dart @@ -34,3 +34,120 @@ class AuthenticatedUserData extends UserData { final String access_token; } + +class HomepageStats { + final double totalMileage; + final List yearlyMileage; + final List topLocos; + final List leaderboard; + final List trips; + + HomepageStats({ + required this.totalMileage, + required this.yearlyMileage, + required this.topLocos, + required this.leaderboard, + required this.trips, + }); + + factory HomepageStats.fromJson(Map 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 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 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 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 json) => TripSummary( + tripId: json['trip_id'], + tripName: json['trip_name'], + tripMileage: (json['trip_mileage'] as num).toDouble(), + ); +} diff --git a/lib/services/apiService.dart b/lib/services/apiService.dart index 6de75ff..933fabb 100644 --- a/lib/services/apiService.dart +++ b/lib/services/apiService.dart @@ -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 _buildHeaders(Map? extra) { + final token = _getToken?.call(); + final headers = {'accept': 'application/json', ...?extra}; + if (token != null && token.isNotEmpty) { + headers['Authorization'] = 'Bearer $token'; + } + return headers; + } + Future get(String endpoint, {Map? 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 postForm(String endpoint, Map 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 ); 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); } diff --git a/pubspec.yaml b/pubspec.yaml index ce423e8..0425c51 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 diff --git a/test/widget_test.dart b/test/widget_test.dart index 355889a..19b1d7f 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -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 {