Big overhaul of the UI and usability of the app #61
| @@ -140,9 +140,10 @@ class _AnimatedDotsTextState extends State<AnimatedDotsText> { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     String dots = '.' * dotCount; | ||||
|     return Text( | ||||
|     return AutoSizeText( | ||||
|       '${widget.baseText}$dots', | ||||
|       style: widget.style, | ||||
|       maxLines: 2, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -15,8 +15,9 @@ class CurrentTripSummary extends StatefulWidget { | ||||
|  | ||||
| class _CurrentTripSummaryState extends State<CurrentTripSummary> { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Padding( | ||||
|   Widget build(BuildContext context) => ListenableBuilder( | ||||
|     listenable: widget.trip, | ||||
|     builder: (context, child) => Padding( | ||||
|       padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20), | ||||
|       child: Row( | ||||
|         mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
| @@ -25,18 +26,18 @@ class _CurrentTripSummaryState extends State<CurrentTripSummary> { | ||||
|             children: [ | ||||
|               const Icon(Icons.flag, size: 20), | ||||
|               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( | ||||
|             children: [ | ||||
|               const Icon(Icons.hourglass_bottom_rounded, size: 20), | ||||
|               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); | ||||
|     Future<SharedPreferences> preferences = SharedPreferences.getInstance(); | ||||
|  | ||||
|  | ||||
|     return FutureBuilder( | ||||
|       future: preferences, | ||||
|       builder: (context, snapshot) { | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:anyway/structs/agreement.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_markdown/flutter_markdown.dart'; | ||||
|  | ||||
| @@ -26,7 +27,6 @@ class OnboardingAgreementCard extends StatefulWidget { | ||||
| } | ||||
|  | ||||
| class _OnboardingAgreementCardState extends State<OnboardingAgreementCard> { | ||||
|   bool agreed = false; | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Padding( | ||||
| @@ -39,21 +39,42 @@ class _OnboardingAgreementCardState extends State<OnboardingAgreementCard> { | ||||
|           Row( | ||||
|             mainAxisAlignment: MainAxisAlignment.center, | ||||
|             children: [ | ||||
|               Checkbox( | ||||
|               // The checkbox of the agreement | ||||
|               FutureBuilder( | ||||
|                 future: getAgreement(), | ||||
|                 builder: (context, snapshot) { | ||||
|                   bool agreed = false; | ||||
|                   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(() { | ||||
|                     agreed = value!; | ||||
|                     widget.onAgreementChanged(value); | ||||
|                         widget.onAgreementChanged(value!); | ||||
|                       }); | ||||
|                       saveAgreement(value!); | ||||
|                     }, | ||||
|                   ); | ||||
|                 }, | ||||
|               ), | ||||
|                | ||||
|               // The text of the agreement | ||||
|               Text( | ||||
|                 "I agree to the ", | ||||
|                 style: Theme.of(context).textTheme.bodyMedium!.copyWith( | ||||
|                   color: Colors.white, | ||||
|                 ), | ||||
|               ), | ||||
|                | ||||
|               // The clickable text of the agreement that shows the agreement text | ||||
|               GestureDetector( | ||||
|                 onTap: () { | ||||
|                   // show a dialog with the agreement text | ||||
|   | ||||
| @@ -48,7 +48,7 @@ class _StepBetweenLandmarksState extends State<StepBetweenLandmarks> { | ||||
|             children: [ | ||||
|               const Icon(Icons.directions_walk), | ||||
|               Text( | ||||
|                 time == null ? "" : "About $time min", | ||||
|                 time == null ? "" : "$time min", | ||||
|                 style: const TextStyle(fontSize: 10) | ||||
|               ), | ||||
|             ], | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import 'package:anyway/layouts/scaffold.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/trip.dart'; | ||||
| import 'package:flutter/cupertino.dart'; | ||||
| @@ -20,6 +21,14 @@ class _NewTripPreferencesPageState extends State<NewTripPreferencesPage> with Sc | ||||
|  | ||||
|   @override | ||||
|   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( | ||||
|       context, | ||||
|       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/onboarding_card.dart'; | ||||
| import 'package:anyway/pages/new_trip_location.dart'; | ||||
| import 'package:anyway/structs/agreement.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
|  | ||||
| @@ -121,11 +122,7 @@ class _OnboardingPageState extends State<OnboardingPage> { | ||||
|         ); | ||||
|       } else { | ||||
|         // only allow the user to proceed if they have agreed to the terms and conditions | ||||
|         Future<bool> hasAgreed = SharedPreferences.getInstance().then( | ||||
|           (SharedPreferences prefs) { | ||||
|             return prefs.getBool('TC_agree') ?? false; | ||||
|           } | ||||
|         ); | ||||
|         Future<bool> hasAgreed = getAgreement().then((agreement) => agreement.agreed); | ||||
|  | ||||
|         return FutureBuilder( | ||||
|           future: hasAgreed, | ||||
| @@ -157,8 +154,7 @@ class _OnboardingPageState extends State<OnboardingPage> { | ||||
|   ); | ||||
|  | ||||
|   void onAgreementChanged(bool value) async { | ||||
|     SharedPreferences prefs = await SharedPreferences.getInstance(); | ||||
|     await prefs.setBool('TC_agree', value); | ||||
|     saveAgreement(value); | ||||
|     // Update the state of the OnboardingPage | ||||
|     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 { | ||||
|   String uuid; | ||||
|   int totalTime; | ||||
|   Duration totalTime; | ||||
|   LinkedList<Landmark> landmarks; | ||||
|   // could be empty as well | ||||
|   String? errorDescription; | ||||
| @@ -44,7 +44,7 @@ class Trip with ChangeNotifier { | ||||
|  | ||||
|   Trip({ | ||||
|     this.uuid = 'pending', | ||||
|     this.totalTime = 0, | ||||
|     this.totalTime = Duration.zero, | ||||
|     LinkedList<Landmark>? landmarks | ||||
|     // a trip can be created with no landmarks, but the list should be initialized anyway | ||||
|   }) : landmarks = landmarks ?? LinkedList<Landmark>(); | ||||
| @@ -53,7 +53,7 @@ class Trip with ChangeNotifier { | ||||
|   factory Trip.fromJson(Map<String, dynamic> json) { | ||||
|     Trip trip = Trip( | ||||
|       uuid: json['uuid'], | ||||
|       totalTime: json['total_time'], | ||||
|       totalTime: Duration(minutes: json['total_time']), | ||||
|     ); | ||||
|  | ||||
|     return trip; | ||||
| @@ -61,7 +61,7 @@ class Trip with ChangeNotifier { | ||||
|  | ||||
|   void loadFromJson(Map<String, dynamic> json) { | ||||
|     uuid = json['uuid']; | ||||
|     totalTime = json['total_time']; | ||||
|     totalTime = Duration(minutes: json['total_time']); | ||||
|     notifyListeners(); | ||||
|   } | ||||
|  | ||||
| @@ -82,9 +82,12 @@ class Trip with ChangeNotifier { | ||||
|     // removing the landmark means we need to recompute the time between the two adjoined landmarks | ||||
|     if (previous != null && next != null) { | ||||
|       // previous.next = next happens automatically since we are using a LinkedList | ||||
|       this.totalTime -= previous.tripTime ?? Duration.zero; | ||||
|       previous.tripTime = null; | ||||
|       // TODO | ||||
|     } | ||||
|     this.totalTime -= landmark.tripTime ?? Duration.zero; | ||||
|  | ||||
|     notifyListeners(); | ||||
|   } | ||||
|  | ||||
| @@ -111,7 +114,7 @@ class Trip with ChangeNotifier { | ||||
|  | ||||
|   Map<String, dynamic> toJson() => { | ||||
|     'uuid': uuid, | ||||
|     'total_time': totalTime, | ||||
|     'total_time': totalTime.inMinutes, | ||||
|     'first_landmark_uuid': landmarks.first.uuid | ||||
|   }; | ||||
|  | ||||
|   | ||||
| @@ -1,44 +1,50 @@ | ||||
| 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:anyway/structs/trip.dart'; | ||||
| import 'package:anyway/utils/load_trips.dart'; | ||||
| import 'package:anyway/pages/current_trip.dart'; | ||||
| import 'package:anyway/pages/onboarding.dart'; | ||||
|  | ||||
|  | ||||
| Widget getFirstPage() { | ||||
|   SavedTrips trips = savedTrips; | ||||
|   trips.loadTrips(); | ||||
|  | ||||
|   return ListenableBuilder( | ||||
|     listenable: trips, | ||||
|     builder: (BuildContext context, Widget? child) { | ||||
|       List<Trip> items = trips.trips; | ||||
|       if (items.isNotEmpty) { | ||||
|         return TripPage(trip: items[0]); | ||||
|   // check if the user has already seen the onboarding and agreed to the terms of service | ||||
|   return FutureBuilder( | ||||
|     future: getAgreement(), | ||||
|     builder: (context, snapshot) { | ||||
|       if (snapshot.connectionState == ConnectionState.done) { | ||||
|         if (snapshot.hasData) { | ||||
|           Agreement agrement = snapshot.data!; | ||||
|           if (agrement.agreed) { | ||||
|             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 const OnboardingPage(); | ||||
|                       return NoTripsPage(); | ||||
|                     } | ||||
|                   } else { | ||||
|                     return Center(child: CircularProgressIndicator()); | ||||
|                   } | ||||
|                 } else { | ||||
|                   return Center(child: CircularProgressIndicator()); | ||||
|                 } | ||||
|               }, | ||||
|             ); | ||||
|           } else { | ||||
|             return OnboardingPage(); | ||||
|           } | ||||
|         } else { | ||||
|           return OnboardingPage(); | ||||
|         } | ||||
|       } else { | ||||
|         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; | ||||
|  | ||||
|   void loadTrips() async { | ||||
|   Future<List<Trip>> loadTrips() async { | ||||
|     SharedPreferences prefs = await SharedPreferences.getInstance(); | ||||
|  | ||||
|     List<Trip> trips = []; | ||||
| @@ -21,6 +21,7 @@ class SavedTrips extends ChangeNotifier { | ||||
|     } | ||||
|     _trips = trips; | ||||
|     notifyListeners(); | ||||
|     return trips; | ||||
|   } | ||||
|  | ||||
|   void addTrip(Trip trip) async { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user