From 985eddb35ac701b9a65c30957ac6b32e34b49f5f Mon Sep 17 00:00:00 2001 From: Pete Gregory Date: Fri, 25 Jul 2025 02:03:10 +0100 Subject: [PATCH] Add display of legs and top traction, adjust colour based on device theme --- android/app/build.gradle.kts | 2 +- .../dashboard/topTractionPanel.dart | 59 +++++++++++++ lib/components/pages/dashboard.dart | 83 +++++++++++++------ lib/components/pages/legs.dart | 31 +++++++ lib/main.dart | 81 ++++++++++-------- lib/objects/objects.dart | 82 +++++++++++++++++- lib/services/dataService.dart | 20 +++++ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 8 ++ pubspec.yaml | 1 + test/widget_test.dart | 2 +- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 15 files changed, 316 insertions(+), 64 deletions(-) create mode 100644 lib/components/dashboard/topTractionPanel.dart create mode 100644 lib/components/pages/legs.dart diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 8111223..c5c908d 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -8,7 +8,7 @@ plugins { android { namespace = "com.example.mileograph_sbb_flutter" compileSdk = flutter.compileSdkVersion - ndkVersion = flutter.ndkVersion + ndkVersion = "27.0.12077973" compileOptions { sourceCompatibility = JavaVersion.VERSION_11 diff --git a/lib/components/dashboard/topTractionPanel.dart b/lib/components/dashboard/topTractionPanel.dart new file mode 100644 index 0000000..ee4de84 --- /dev/null +++ b/lib/components/dashboard/topTractionPanel.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:mileograph_flutter/services/dataService.dart'; + +import 'package:provider/provider.dart'; + +class TopTractionPanel extends StatelessWidget { + Widget build(BuildContext context) { + final data = context.watch(); + return Padding( + padding: const EdgeInsets.all(10.0), + child: Card( child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("Top Traction", style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, decoration: TextDecoration.underline)), + Column( + children: List.generate( + data.homepageStats?.topLocos.length ?? 0, + (index) { + final loco = data.homepageStats!.topLocos[index]; + return Container( + width: double.infinity, + child: Card( + margin: EdgeInsets.symmetric(horizontal: 0, vertical: 8), + child: Padding( + padding: EdgeInsets.all(16), + + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text.rich( + TextSpan( + children: [ + TextSpan(text: '${index + 1}. ', style: TextStyle(fontWeight: FontWeight.bold)), + TextSpan(text: '${loco.locoClass} ${loco.locoNumber}'), + ], + ), + ), + Text('${loco.locoName}', style: TextStyle(fontStyle: FontStyle.italic)), + + ], + ), + Text('${loco.locoMileage?.toStringAsFixed(1)} mi'), + ], + ), + ), + ), + ); + }, + ), +) + + ], + )), + ); + } +} \ No newline at end of file diff --git a/lib/components/pages/dashboard.dart b/lib/components/pages/dashboard.dart index 8924fa4..416fcd1 100644 --- a/lib/components/pages/dashboard.dart +++ b/lib/components/pages/dashboard.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:mileograph_flutter/services/authservice.dart'; import 'package:mileograph_flutter/services/dataService.dart'; +import 'package:mileograph_flutter/components/dashboard/topTractionPanel.dart'; import 'package:provider/provider.dart'; @@ -22,36 +23,70 @@ class DashboardHeader extends StatelessWidget { @override Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, + return Column( children: [ - Column( + Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - auth.fullName ?? "Unknown", - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), - ), - Text.rich( - TextSpan( - children: [ - TextSpan(text: "Total Mileage: "), - TextSpan( - text: data.homepageStats?.totalMileage.toString() ?? "0", + Row( + children: [ + Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Text.rich( + TextSpan( + children: [ + TextSpan(text: "Total Mileage: "), + TextSpan( + text: + data.homepageStats?.totalMileage + .toString() ?? + "0", + ), + ], + ), + ), + Text.rich( + TextSpan( + children: [ + TextSpan(text: DateTime.now().year.toString()), + TextSpan(text: " Mileage: "), + TextSpan( + text: data + .getMileageForCurrentYear() + .toString(), + ), + ], + ), + ), + ], + ), ), - ], - ), - ), - Text.rich( - TextSpan( - children: [ - TextSpan(text: DateTime.now().year.toString()), - TextSpan(text: " Mileage: "), - TextSpan(text: data.getMileageForCurrentYear().toString()), - ], - ), + ), + Card( + child: Padding( + padding: EdgeInsets.all(8), + child: Column( + children: [ + Text("Total Winners: 123"), + Text("Average mileage: 45.6"), + ], + ), + ), + ), + ], ), ], ), + + Expanded( + child: ListView( + scrollDirection: Axis.vertical, + children: [TopTractionPanel()], + ), + ), ], ); } diff --git a/lib/components/pages/legs.dart b/lib/components/pages/legs.dart new file mode 100644 index 0000000..81e1570 --- /dev/null +++ b/lib/components/pages/legs.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:mileograph_flutter/services/dataService.dart'; + + +class LegsPage extends StatelessWidget { + Widget build(BuildContext context){ + final data = context.watch(); + return ListView.builder( + itemCount: data.legs.length, + itemBuilder: (context, index) { + final leg = data.legs[index]; + return Card( + margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${leg.start} → ${leg.end}', style: TextStyle(fontSize: 16)), + Text('Mileage: ${leg.mileage.toStringAsFixed(2)} km'), + Text('Headcode: ${leg.headcode}'), + Text('Begin: ${leg.beginTime}'), + ], + ), + ), + ); + }, + ); + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 911ae18..f06b37b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,11 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:dynamic_color/dynamic_color.dart'; +import 'package:provider/provider.dart'; + +import 'package:mileograph_flutter/components/pages/legs.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'; import 'components/pages/dashboard.dart'; +import 'components/dashboard/topTractionPanel.dart'; late ApiService api; @@ -54,41 +58,47 @@ class AppRoot extends StatelessWidget { } class MyApp extends StatelessWidget { - const MyApp({super.key}); + MyApp({super.key}); + final ColorScheme defaultLight = ColorScheme.fromSeed(seedColor: Colors.red); + final ColorScheme defaultDark = ColorScheme.fromSeed( + seedColor: Colors.red, + brightness: Brightness.dark, + ); // This widget is the root of your application. @override Widget build(BuildContext context) { - 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 - // the application has a purple toolbar. Then, without quitting the app, - // try changing the seedColor in the colorScheme below to Colors.green - // 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. - // - // This works for code too, not just values: Most code changes can be - // tested with just a hot reload. - colorScheme: ColorScheme.fromSeed(seedColor: Colors.blueGrey), - ), - darkTheme: ThemeData( - useMaterial3: true, - colorScheme: ColorScheme.fromSeed( - seedColor: Colors.blueGrey, - brightness: Brightness.dark, - ), - ), - themeMode: ThemeMode.system, - home: AppRoot(), + return DynamicColorBuilder( + builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) { + 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 + // the application has a purple toolbar. Then, without quitting the app, + // try changing the seedColor in the colorScheme below to Colors.green + // 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. + // + // This works for code too, not just values: Most code changes can be + // tested with just a hot reload. + colorScheme: lightDynamic ?? defaultLight, + ), + darkTheme: ThemeData( + useMaterial3: true, + colorScheme: darkDynamic ?? defaultDark, + ), + themeMode: ThemeMode.system, + home: AppRoot(), + ); + }, ); } } @@ -114,7 +124,7 @@ class _MyHomePageState extends State { final List contentPages = [ Dashboard(), Center(child: Text("Calculator Page")), - Center(child: Text("Entries Page")), + LegsPage(), Center(child: Text("Traction Page")), Center(child: Text("Trips Page")), ]; @@ -135,6 +145,9 @@ class _MyHomePageState extends State { if (data.homepageStats == null) { data.fetchHomepageStats(); } + if (data.legs.isEmpty) { + data.fetchLegs(); + } }); } } @@ -152,7 +165,7 @@ class _MyHomePageState extends State { currentPage = Center( child: FilledButton( onPressed: data.fetchHomepageStats, - child: Text("Fetch"), + child: CircularProgressIndicator(), ), ); } diff --git a/lib/objects/objects.dart b/lib/objects/objects.dart index 26d2317..7c53d58 100644 --- a/lib/objects/objects.dart +++ b/lib/objects/objects.dart @@ -84,8 +84,9 @@ class YearlyMileage { class LocoSummary { final String locoType, locoClass, locoNumber, locoName, locoOperator; final String? locoNotes, locoEvn; - final int locoId, locoJourneys; - final double locoMileage; + final int locoId; + final int? locoJourneys; + final double? locoMileage; LocoSummary({ required this.locoType, @@ -96,8 +97,8 @@ class LocoSummary { this.locoNotes, this.locoEvn, required this.locoId, - required this.locoMileage, - required this.locoJourneys, + this.locoMileage, + this.locoJourneys, }); factory LocoSummary.fromJson(Map json) => LocoSummary( @@ -151,3 +152,76 @@ class TripSummary { tripMileage: (json['trip_mileage'] as num).toDouble(), ); } + +class Loco { + final int id; + final String type, number, name, locoClass, operator; + final String? notes, evn; + + Loco({ + required this.id, + required this.type, + required this.number, + required this.name, + required this.locoClass, + required this.operator, + this.notes, + this.evn, + }); + + factory Loco.fromJson(Map json) => Loco( + id: json['loco_id'], + type: json['loco_type'], + number: json['loco_number'], + name: json['loco_name'] ?? "", + locoClass: json['loco_class'], + operator: json['loco_operator'], + notes: json['loco_notes'], + evn: json['loco_evn'], + ); +} + +class Leg { + final int id, tripId, timezone, driving; + final String start, end, route, network, notes, headcode, user; + final DateTime beginTime; + final double mileage; + final List locos; + + Leg({ + required this.id, + required this.tripId, + required this.start, + required this.end, + required this.beginTime, + required this.timezone, + required this.network, + required this.route, + required this.mileage, + required this.notes, + required this.headcode, + required this.driving, + required this.user, + required this.locos, + }); + + factory Leg.fromJson(Map json) => Leg( + id: json['leg_id'], + tripId: json['leg_trip'] ?? 0, + start: json['leg_start'], + end: json['leg_end'], + beginTime: DateTime.parse(json['leg_begin_time']), + timezone: (json['leg_timezone'] as num).toInt(), + network: json['leg_network'] ?? "", + route: json['leg_route'], + mileage: (json['leg_mileage'] as num).toDouble(), + notes: json['leg_notes'] ?? "", + headcode: json['leg_headcode'] ?? "", + driving: json['leg_driving'], + user: json['leg_user'], + locos: (json['locos'] as List) + .map((e) => Loco.fromJson(e as Map)) + .toList(), + ); +} + diff --git a/lib/services/dataService.dart b/lib/services/dataService.dart index 6a07cd0..92ca63c 100644 --- a/lib/services/dataService.dart +++ b/lib/services/dataService.dart @@ -10,6 +10,9 @@ class DataService extends ChangeNotifier { HomepageStats? _homepageStats; HomepageStats? get homepageStats => _homepageStats; + List _legs = []; + List get legs => _legs; + bool _isHomepageLoading = false; bool get isHomepageLoading => _isHomepageLoading; @@ -28,6 +31,23 @@ class DataService extends ChangeNotifier { notifyListeners(); } } + + Future fetchLegs({ + int offset = 0, + int limit = 100, + String sortBy = 'date', + int sortDirection = 0, +}) async { + final query = '?sort_direction=$sortDirection&sort_by=$sortBy&offset=$offset&limit=$limit'; + final json = await api.get('/user/legs$query'); + + if (json is List) { + _legs = json.map((e) => Leg.fromJson(e)).toList(); + notifyListeners(); + } else { + throw Exception('Unexpected legs response: $json'); + } +} void clear() { _homepageStats = null; diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d..675b719 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) dynamic_color_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); + dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87..3e303c1 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + dynamic_color ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..507ed46 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,8 @@ import FlutterMacOS import Foundation +import dynamic_color func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 128d9be..45b5fb8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dynamic_color: + dependency: "direct main" + description: + name: dynamic_color + sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d + url: "https://pub.dev" + source: hosted + version: "1.7.0" fake_async: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0425c51..cfe70d4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,6 +32,7 @@ dependencies: sdk: flutter http: ^1.4.0 provider: ^6.1.5 + dynamic_color: ^1.6.6 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. diff --git a/test/widget_test.dart b/test/widget_test.dart index 19b1d7f..0b175c3 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -13,7 +13,7 @@ import 'package:mileograph_flutter/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); + await tester.pumpWidget(MyApp()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget); diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d468..e4899a6 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + DynamicColorPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c3..841e8c4 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + dynamic_color ) list(APPEND FLUTTER_FFI_PLUGIN_LIST