quite a few UX improvements
This commit is contained in:
		| @@ -140,9 +140,10 @@ class _AnimatedDotsTextState extends State<AnimatedDotsText> { | |||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     String dots = '.' * dotCount; |     String dots = '.' * dotCount; | ||||||
|     return Text( |     return AutoSizeText( | ||||||
|       '${widget.baseText}$dots', |       '${widget.baseText}$dots', | ||||||
|       style: widget.style, |       style: widget.style, | ||||||
|  |       maxLines: 2, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -15,8 +15,9 @@ class CurrentTripSummary extends StatefulWidget { | |||||||
|  |  | ||||||
| class _CurrentTripSummaryState extends State<CurrentTripSummary> { | class _CurrentTripSummaryState extends State<CurrentTripSummary> { | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) => ListenableBuilder( | ||||||
|     return Padding( |     listenable: widget.trip, | ||||||
|  |     builder: (context, child) => Padding( | ||||||
|       padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20), |       padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20), | ||||||
|       child: Row( |       child: Row( | ||||||
|         mainAxisAlignment: MainAxisAlignment.spaceBetween, |         mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||||
| @@ -25,18 +26,18 @@ class _CurrentTripSummaryState extends State<CurrentTripSummary> { | |||||||
|             children: [ |             children: [ | ||||||
|               const Icon(Icons.flag, size: 20), |               const Icon(Icons.flag, size: 20), | ||||||
|               const Padding(padding: EdgeInsets.only(right: 10)), |               const Padding(padding: EdgeInsets.only(right: 10)), | ||||||
|               Text('Stops: ${widget.trip.landmarks.length}', style: Theme.of(context).textTheme.bodyLarge), |               Text('${widget.trip.landmarks.length} stops', style: Theme.of(context).textTheme.bodyLarge), | ||||||
|             ] |             ] | ||||||
|           ), |           ), | ||||||
|           Row( |           Row( | ||||||
|             children: [ |             children: [ | ||||||
|               const Icon(Icons.hourglass_bottom_rounded, size: 20), |               const Icon(Icons.hourglass_bottom_rounded, size: 20), | ||||||
|               const Padding(padding: EdgeInsets.only(right: 10)), |               const Padding(padding: EdgeInsets.only(right: 10)), | ||||||
|               Text('Duration: ${widget.trip.totalTime} minutes', style: Theme.of(context).textTheme.bodyLarge), |               Text('${widget.trip.totalTime.inHours}h ${widget.trip.totalTime.inMinutes.remainder(60)}min', style: Theme.of(context).textTheme.bodyLarge), | ||||||
|             ] |             ] | ||||||
|           ), |           ), | ||||||
|         ], |         ], | ||||||
|       ) |       ) | ||||||
|     ); |     ) | ||||||
|   } |   ); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -78,6 +78,7 @@ class _NewTripMapState extends State<NewTripMap> { | |||||||
|     widget.trip.addListener(updateTripDetails); |     widget.trip.addListener(updateTripDetails); | ||||||
|     Future<SharedPreferences> preferences = SharedPreferences.getInstance(); |     Future<SharedPreferences> preferences = SharedPreferences.getInstance(); | ||||||
|  |  | ||||||
|  |  | ||||||
|     return FutureBuilder( |     return FutureBuilder( | ||||||
|       future: preferences, |       future: preferences, | ||||||
|       builder: (context, snapshot) { |       builder: (context, snapshot) { | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | import 'package:anyway/structs/agreement.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:flutter_markdown/flutter_markdown.dart'; | import 'package:flutter_markdown/flutter_markdown.dart'; | ||||||
|  |  | ||||||
| @@ -26,7 +27,6 @@ class OnboardingAgreementCard extends StatefulWidget { | |||||||
| } | } | ||||||
|  |  | ||||||
| class _OnboardingAgreementCardState extends State<OnboardingAgreementCard> { | class _OnboardingAgreementCardState extends State<OnboardingAgreementCard> { | ||||||
|   bool agreed = false; |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     return Padding( |     return Padding( | ||||||
| @@ -39,21 +39,42 @@ class _OnboardingAgreementCardState extends State<OnboardingAgreementCard> { | |||||||
|           Row( |           Row( | ||||||
|             mainAxisAlignment: MainAxisAlignment.center, |             mainAxisAlignment: MainAxisAlignment.center, | ||||||
|             children: [ |             children: [ | ||||||
|               Checkbox( |               // The checkbox of the agreement | ||||||
|                 value: agreed, |               FutureBuilder( | ||||||
|                 onChanged: (value) { |                 future: getAgreement(), | ||||||
|                   setState(() { |                 builder: (context, snapshot) { | ||||||
|                     agreed = value!; |                   bool agreed = false; | ||||||
|                     widget.onAgreementChanged(value); |                   if (snapshot.connectionState == ConnectionState.done) { | ||||||
|                   }); |                     if (snapshot.hasData) { | ||||||
|  |                       Agreement agreement = snapshot.data!; | ||||||
|  |                       agreed = agreement.agreed; | ||||||
|  |                     } else { | ||||||
|  |                       agreed = false; | ||||||
|  |                     } | ||||||
|  |                   } else { | ||||||
|  |                     agreed = false; | ||||||
|  |                   } | ||||||
|  |                   return Checkbox( | ||||||
|  |                     value: agreed, | ||||||
|  |                     onChanged: (value) { | ||||||
|  |                       setState(() { | ||||||
|  |                         widget.onAgreementChanged(value!); | ||||||
|  |                       }); | ||||||
|  |                       saveAgreement(value!); | ||||||
|  |                     }, | ||||||
|  |                   ); | ||||||
|                 }, |                 }, | ||||||
|               ), |               ), | ||||||
|  |                | ||||||
|  |               // The text of the agreement | ||||||
|               Text( |               Text( | ||||||
|                 "I agree to the ", |                 "I agree to the ", | ||||||
|                 style: Theme.of(context).textTheme.bodyMedium!.copyWith( |                 style: Theme.of(context).textTheme.bodyMedium!.copyWith( | ||||||
|                   color: Colors.white, |                   color: Colors.white, | ||||||
|                 ), |                 ), | ||||||
|               ), |               ), | ||||||
|  |                | ||||||
|  |               // The clickable text of the agreement that shows the agreement text | ||||||
|               GestureDetector( |               GestureDetector( | ||||||
|                 onTap: () { |                 onTap: () { | ||||||
|                   // show a dialog with the agreement text |                   // show a dialog with the agreement text | ||||||
|   | |||||||
| @@ -48,7 +48,7 @@ class _StepBetweenLandmarksState extends State<StepBetweenLandmarks> { | |||||||
|             children: [ |             children: [ | ||||||
|               const Icon(Icons.directions_walk), |               const Icon(Icons.directions_walk), | ||||||
|               Text( |               Text( | ||||||
|                 time == null ? "" : "About $time min", |                 time == null ? "" : "$time min", | ||||||
|                 style: const TextStyle(fontSize: 10) |                 style: const TextStyle(fontSize: 10) | ||||||
|               ), |               ), | ||||||
|             ], |             ], | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| import 'package:anyway/layouts/scaffold.dart'; | import 'package:anyway/layouts/scaffold.dart'; | ||||||
| import 'package:anyway/modules/new_trip_button.dart'; | import 'package:anyway/modules/new_trip_button.dart'; | ||||||
|  | import 'package:anyway/structs/landmark.dart'; | ||||||
| import 'package:anyway/structs/preferences.dart'; | import 'package:anyway/structs/preferences.dart'; | ||||||
| import 'package:anyway/structs/trip.dart'; | import 'package:anyway/structs/trip.dart'; | ||||||
| import 'package:flutter/cupertino.dart'; | import 'package:flutter/cupertino.dart'; | ||||||
| @@ -20,6 +21,14 @@ class _NewTripPreferencesPageState extends State<NewTripPreferencesPage> with Sc | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|  |     // Ensure that the trip is "empty" save for the start landmark | ||||||
|  |     // This is necessary because users can swipe back to this page even after the trip has been created | ||||||
|  |     if (widget.trip.landmarks.length > 1) { | ||||||
|  |       Landmark start = widget.trip.landmarks.first; | ||||||
|  |       widget.trip.landmarks.clear(); | ||||||
|  |       widget.trip.addLandmark(start); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     return mainScaffold( |     return mainScaffold( | ||||||
|       context, |       context, | ||||||
|       child: Scaffold( |       child: Scaffold( | ||||||
|   | |||||||
							
								
								
									
										50
									
								
								frontend/lib/pages/no_trips_page.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								frontend/lib/pages/no_trips_page.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | |||||||
|  | import 'package:anyway/pages/new_trip_location.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  |  | ||||||
|  | import 'package:anyway/layouts/scaffold.dart'; | ||||||
|  | class NoTripsPage extends StatefulWidget { | ||||||
|  |   const NoTripsPage({super.key}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   State<NoTripsPage> createState() => _NoTripsPageState(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _NoTripsPageState extends State<NoTripsPage> with ScaffoldLayout { | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) => mainScaffold( | ||||||
|  |     context, | ||||||
|  |     child: Scaffold( | ||||||
|  |       body: Center( | ||||||
|  |         child: Column( | ||||||
|  |           mainAxisAlignment: MainAxisAlignment.center, | ||||||
|  |           children: [ | ||||||
|  |             Text( | ||||||
|  |               "No trips yet", | ||||||
|  |               style: Theme.of(context).textTheme.headlineMedium, | ||||||
|  |             ), | ||||||
|  |             Text( | ||||||
|  |               "You can start a new trip by clicking the button below", | ||||||
|  |               style: Theme.of(context).textTheme.bodyMedium, | ||||||
|  |             ), | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |       floatingActionButton: FloatingActionButton.extended( | ||||||
|  |         onPressed: () { | ||||||
|  |           Navigator.of(context).push( | ||||||
|  |             MaterialPageRoute( | ||||||
|  |               builder: (context) => const NewTripPage() | ||||||
|  |             ) | ||||||
|  |           ); | ||||||
|  |         }, | ||||||
|  |         label: const Row( | ||||||
|  |           children: [ | ||||||
|  |             Text("Start planning!"), | ||||||
|  |             Padding(padding: EdgeInsets.only(right: 8.0)), | ||||||
|  |             Icon(Icons.map_outlined) | ||||||
|  |           ], | ||||||
|  |         ) | ||||||
|  |       ) | ||||||
|  |     ) | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @@ -4,6 +4,7 @@ import 'package:anyway/constants.dart'; | |||||||
| import 'package:anyway/modules/onbarding_agreement_card.dart'; | import 'package:anyway/modules/onbarding_agreement_card.dart'; | ||||||
| import 'package:anyway/modules/onboarding_card.dart'; | import 'package:anyway/modules/onboarding_card.dart'; | ||||||
| import 'package:anyway/pages/new_trip_location.dart'; | import 'package:anyway/pages/new_trip_location.dart'; | ||||||
|  | import 'package:anyway/structs/agreement.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:shared_preferences/shared_preferences.dart'; | import 'package:shared_preferences/shared_preferences.dart'; | ||||||
|  |  | ||||||
| @@ -121,11 +122,7 @@ class _OnboardingPageState extends State<OnboardingPage> { | |||||||
|         ); |         ); | ||||||
|       } else { |       } else { | ||||||
|         // only allow the user to proceed if they have agreed to the terms and conditions |         // only allow the user to proceed if they have agreed to the terms and conditions | ||||||
|         Future<bool> hasAgreed = SharedPreferences.getInstance().then( |         Future<bool> hasAgreed = getAgreement().then((agreement) => agreement.agreed); | ||||||
|           (SharedPreferences prefs) { |  | ||||||
|             return prefs.getBool('TC_agree') ?? false; |  | ||||||
|           } |  | ||||||
|         ); |  | ||||||
|  |  | ||||||
|         return FutureBuilder( |         return FutureBuilder( | ||||||
|           future: hasAgreed, |           future: hasAgreed, | ||||||
| @@ -157,8 +154,7 @@ class _OnboardingPageState extends State<OnboardingPage> { | |||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   void onAgreementChanged(bool value) async { |   void onAgreementChanged(bool value) async { | ||||||
|     SharedPreferences prefs = await SharedPreferences.getInstance(); |     saveAgreement(value); | ||||||
|     await prefs.setBool('TC_agree', value); |  | ||||||
|     // Update the state of the OnboardingPage |     // Update the state of the OnboardingPage | ||||||
|     setState(() {}); |     setState(() {}); | ||||||
|   } |   } | ||||||
|   | |||||||
							
								
								
									
										17
									
								
								frontend/lib/structs/agreement.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								frontend/lib/structs/agreement.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | import 'package:shared_preferences/shared_preferences.dart'; | ||||||
|  |  | ||||||
|  | final class Agreement{ | ||||||
|  |   bool agreed; | ||||||
|  |  | ||||||
|  |   Agreement({required this.agreed}); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void saveAgreement(bool agreed) async { | ||||||
|  |   SharedPreferences prefs = await SharedPreferences.getInstance(); | ||||||
|  |   prefs.setBool('agreed_to_terms_and_conditions', agreed); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Future<Agreement> getAgreement() async { | ||||||
|  |   SharedPreferences prefs = await SharedPreferences.getInstance(); | ||||||
|  |   return Agreement(agreed: prefs.getBool('agreed_to_terms_and_conditions') ?? false); | ||||||
|  | } | ||||||
| @@ -12,7 +12,7 @@ import 'package:shared_preferences/shared_preferences.dart'; | |||||||
|  |  | ||||||
| class Trip with ChangeNotifier { | class Trip with ChangeNotifier { | ||||||
|   String uuid; |   String uuid; | ||||||
|   int totalTime; |   Duration totalTime; | ||||||
|   LinkedList<Landmark> landmarks; |   LinkedList<Landmark> landmarks; | ||||||
|   // could be empty as well |   // could be empty as well | ||||||
|   String? errorDescription; |   String? errorDescription; | ||||||
| @@ -44,7 +44,7 @@ class Trip with ChangeNotifier { | |||||||
|  |  | ||||||
|   Trip({ |   Trip({ | ||||||
|     this.uuid = 'pending', |     this.uuid = 'pending', | ||||||
|     this.totalTime = 0, |     this.totalTime = Duration.zero, | ||||||
|     LinkedList<Landmark>? landmarks |     LinkedList<Landmark>? landmarks | ||||||
|     // a trip can be created with no landmarks, but the list should be initialized anyway |     // a trip can be created with no landmarks, but the list should be initialized anyway | ||||||
|   }) : landmarks = landmarks ?? LinkedList<Landmark>(); |   }) : landmarks = landmarks ?? LinkedList<Landmark>(); | ||||||
| @@ -53,7 +53,7 @@ class Trip with ChangeNotifier { | |||||||
|   factory Trip.fromJson(Map<String, dynamic> json) { |   factory Trip.fromJson(Map<String, dynamic> json) { | ||||||
|     Trip trip = Trip( |     Trip trip = Trip( | ||||||
|       uuid: json['uuid'], |       uuid: json['uuid'], | ||||||
|       totalTime: json['total_time'], |       totalTime: Duration(minutes: json['total_time']), | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     return trip; |     return trip; | ||||||
| @@ -61,7 +61,7 @@ class Trip with ChangeNotifier { | |||||||
|  |  | ||||||
|   void loadFromJson(Map<String, dynamic> json) { |   void loadFromJson(Map<String, dynamic> json) { | ||||||
|     uuid = json['uuid']; |     uuid = json['uuid']; | ||||||
|     totalTime = json['total_time']; |     totalTime = Duration(minutes: json['total_time']); | ||||||
|     notifyListeners(); |     notifyListeners(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -82,9 +82,12 @@ class Trip with ChangeNotifier { | |||||||
|     // removing the landmark means we need to recompute the time between the two adjoined landmarks |     // removing the landmark means we need to recompute the time between the two adjoined landmarks | ||||||
|     if (previous != null && next != null) { |     if (previous != null && next != null) { | ||||||
|       // previous.next = next happens automatically since we are using a LinkedList |       // previous.next = next happens automatically since we are using a LinkedList | ||||||
|  |       this.totalTime -= previous.tripTime ?? Duration.zero; | ||||||
|       previous.tripTime = null; |       previous.tripTime = null; | ||||||
|       // TODO |       // TODO | ||||||
|     } |     } | ||||||
|  |     this.totalTime -= landmark.tripTime ?? Duration.zero; | ||||||
|  |  | ||||||
|     notifyListeners(); |     notifyListeners(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -111,7 +114,7 @@ class Trip with ChangeNotifier { | |||||||
|  |  | ||||||
|   Map<String, dynamic> toJson() => { |   Map<String, dynamic> toJson() => { | ||||||
|     'uuid': uuid, |     'uuid': uuid, | ||||||
|     'total_time': totalTime, |     'total_time': totalTime.inMinutes, | ||||||
|     'first_landmark_uuid': landmarks.first.uuid |     'first_landmark_uuid': landmarks.first.uuid | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,44 +1,50 @@ | |||||||
| import 'package:anyway/main.dart'; | import 'package:anyway/main.dart'; | ||||||
|  | import 'package:anyway/pages/no_trips_page.dart'; | ||||||
|  | import 'package:anyway/structs/agreement.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  |  | ||||||
| import 'package:anyway/structs/trip.dart'; | import 'package:anyway/structs/trip.dart'; | ||||||
| import 'package:anyway/utils/load_trips.dart'; |  | ||||||
| import 'package:anyway/pages/current_trip.dart'; | import 'package:anyway/pages/current_trip.dart'; | ||||||
| import 'package:anyway/pages/onboarding.dart'; | import 'package:anyway/pages/onboarding.dart'; | ||||||
|  |  | ||||||
|  |  | ||||||
| Widget getFirstPage() { | Widget getFirstPage() { | ||||||
|   SavedTrips trips = savedTrips; |   // check if the user has already seen the onboarding and agreed to the terms of service | ||||||
|   trips.loadTrips(); |   return FutureBuilder( | ||||||
|  |     future: getAgreement(), | ||||||
|   return ListenableBuilder( |     builder: (context, snapshot) { | ||||||
|     listenable: trips, |       if (snapshot.connectionState == ConnectionState.done) { | ||||||
|     builder: (BuildContext context, Widget? child) { |         if (snapshot.hasData) { | ||||||
|       List<Trip> items = trips.trips; |           Agreement agrement = snapshot.data!; | ||||||
|       if (items.isNotEmpty) { |           if (agrement.agreed) { | ||||||
|         return TripPage(trip: items[0]); |             return FutureBuilder( | ||||||
|  |               future: savedTrips.loadTrips(), | ||||||
|  |               builder: (context, snapshot) { | ||||||
|  |                 if (snapshot.connectionState == ConnectionState.done) { | ||||||
|  |                   if (snapshot.hasData) { | ||||||
|  |                     List<Trip> trips = snapshot.data!; | ||||||
|  |                     if (trips.length > 0) { | ||||||
|  |                       return TripPage(trip: trips[0]); | ||||||
|  |                     } else { | ||||||
|  |                       return NoTripsPage(); | ||||||
|  |                     } | ||||||
|  |                   } else { | ||||||
|  |                     return Center(child: CircularProgressIndicator()); | ||||||
|  |                   } | ||||||
|  |                 } else { | ||||||
|  |                   return Center(child: CircularProgressIndicator()); | ||||||
|  |                 } | ||||||
|  |               }, | ||||||
|  |             ); | ||||||
|  |           } else { | ||||||
|  |             return OnboardingPage(); | ||||||
|  |           } | ||||||
|  |         } else { | ||||||
|  |           return OnboardingPage(); | ||||||
|  |         } | ||||||
|       } else { |       } else { | ||||||
|         return const OnboardingPage(); |         return OnboardingPage(); | ||||||
|       } |       } | ||||||
|     } |     }, | ||||||
|   ); |   ); | ||||||
|   // Future<List<Trip>> trips = loadTrips(); |  | ||||||
|   // // test if there are any active trips |  | ||||||
|   // // if there are, return the trip list |  | ||||||
|   // // if there are not, return the onboarding page |  | ||||||
|   // return FutureBuilder( |  | ||||||
|   //   future: trips, |  | ||||||
|   //   builder: (context, snapshot) { |  | ||||||
|   //     if (snapshot.hasData) { |  | ||||||
|   //       List<Trip> availableTrips = snapshot.data!; |  | ||||||
|   //       if (availableTrips.isNotEmpty) { |  | ||||||
|   //         return TripPage(trip: availableTrips[0]); |  | ||||||
|   //       } else { |  | ||||||
|   //         return OnboardingPage(); |  | ||||||
|   //       } |  | ||||||
|   //     } else { |  | ||||||
|   //       return CircularProgressIndicator(); |  | ||||||
|   //     } |  | ||||||
|   //   } |  | ||||||
|   // ); |  | ||||||
| } | } | ||||||
| @@ -8,7 +8,7 @@ class SavedTrips extends ChangeNotifier { | |||||||
|  |  | ||||||
|   List<Trip> get trips => _trips; |   List<Trip> get trips => _trips; | ||||||
|  |  | ||||||
|   void loadTrips() async { |   Future<List<Trip>> loadTrips() async { | ||||||
|     SharedPreferences prefs = await SharedPreferences.getInstance(); |     SharedPreferences prefs = await SharedPreferences.getInstance(); | ||||||
|  |  | ||||||
|     List<Trip> trips = []; |     List<Trip> trips = []; | ||||||
| @@ -21,6 +21,7 @@ class SavedTrips extends ChangeNotifier { | |||||||
|     } |     } | ||||||
|     _trips = trips; |     _trips = trips; | ||||||
|     notifyListeners(); |     notifyListeners(); | ||||||
|  |     return trips; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void addTrip(Trip trip) async { |   void addTrip(Trip trip) async { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user