diff --git a/frontend/lib/modules/current_trip_loading_indicator.dart b/frontend/lib/modules/current_trip_loading_indicator.dart index 83a216a..60cc98c 100644 --- a/frontend/lib/modules/current_trip_loading_indicator.dart +++ b/frontend/lib/modules/current_trip_loading_indicator.dart @@ -140,9 +140,10 @@ class _AnimatedDotsTextState extends State { @override Widget build(BuildContext context) { String dots = '.' * dotCount; - return Text( + return AutoSizeText( '${widget.baseText}$dots', style: widget.style, + maxLines: 2, ); } } diff --git a/frontend/lib/modules/current_trip_summary.dart b/frontend/lib/modules/current_trip_summary.dart index e7aa4a5..b7c5654 100644 --- a/frontend/lib/modules/current_trip_summary.dart +++ b/frontend/lib/modules/current_trip_summary.dart @@ -15,8 +15,9 @@ class CurrentTripSummary extends StatefulWidget { class _CurrentTripSummaryState extends State { @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 { 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), ] ), ], ) - ); - } + ) + ); } diff --git a/frontend/lib/modules/new_trip_map.dart b/frontend/lib/modules/new_trip_map.dart index e4e5400..b033b16 100644 --- a/frontend/lib/modules/new_trip_map.dart +++ b/frontend/lib/modules/new_trip_map.dart @@ -78,6 +78,7 @@ class _NewTripMapState extends State { widget.trip.addListener(updateTripDetails); Future preferences = SharedPreferences.getInstance(); + return FutureBuilder( future: preferences, builder: (context, snapshot) { diff --git a/frontend/lib/modules/onbarding_agreement_card.dart b/frontend/lib/modules/onbarding_agreement_card.dart index 8f3d540..bc7a293 100644 --- a/frontend/lib/modules/onbarding_agreement_card.dart +++ b/frontend/lib/modules/onbarding_agreement_card.dart @@ -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 { - bool agreed = false; @override Widget build(BuildContext context) { return Padding( @@ -39,21 +39,42 @@ class _OnboardingAgreementCardState extends State { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Checkbox( - value: agreed, - onChanged: (value) { - setState(() { - agreed = value!; - widget.onAgreementChanged(value); - }); + // 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(() { + 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 diff --git a/frontend/lib/modules/step_between_landmarks.dart b/frontend/lib/modules/step_between_landmarks.dart index 129763f..6dbcf4b 100644 --- a/frontend/lib/modules/step_between_landmarks.dart +++ b/frontend/lib/modules/step_between_landmarks.dart @@ -48,7 +48,7 @@ class _StepBetweenLandmarksState extends State { children: [ const Icon(Icons.directions_walk), Text( - time == null ? "" : "About $time min", + time == null ? "" : "$time min", style: const TextStyle(fontSize: 10) ), ], diff --git a/frontend/lib/pages/new_trip_preferences.dart b/frontend/lib/pages/new_trip_preferences.dart index 76aaf10..ab6146a 100644 --- a/frontend/lib/pages/new_trip_preferences.dart +++ b/frontend/lib/pages/new_trip_preferences.dart @@ -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 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( diff --git a/frontend/lib/pages/no_trips_page.dart b/frontend/lib/pages/no_trips_page.dart new file mode 100644 index 0000000..9c46525 --- /dev/null +++ b/frontend/lib/pages/no_trips_page.dart @@ -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 createState() => _NoTripsPageState(); +} + +class _NoTripsPageState extends State 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) + ], + ) + ) + ) + ); +} \ No newline at end of file diff --git a/frontend/lib/pages/onboarding.dart b/frontend/lib/pages/onboarding.dart index 2de42b6..847ee3a 100644 --- a/frontend/lib/pages/onboarding.dart +++ b/frontend/lib/pages/onboarding.dart @@ -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 { ); } else { // only allow the user to proceed if they have agreed to the terms and conditions - Future hasAgreed = SharedPreferences.getInstance().then( - (SharedPreferences prefs) { - return prefs.getBool('TC_agree') ?? false; - } - ); + Future hasAgreed = getAgreement().then((agreement) => agreement.agreed); return FutureBuilder( future: hasAgreed, @@ -157,8 +154,7 @@ class _OnboardingPageState extends State { ); 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(() {}); } diff --git a/frontend/lib/structs/agreement.dart b/frontend/lib/structs/agreement.dart new file mode 100644 index 0000000..321ca74 --- /dev/null +++ b/frontend/lib/structs/agreement.dart @@ -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 getAgreement() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + return Agreement(agreed: prefs.getBool('agreed_to_terms_and_conditions') ?? false); +} diff --git a/frontend/lib/structs/trip.dart b/frontend/lib/structs/trip.dart index 820749e..ef09e4e 100644 --- a/frontend/lib/structs/trip.dart +++ b/frontend/lib/structs/trip.dart @@ -12,7 +12,7 @@ import 'package:shared_preferences/shared_preferences.dart'; class Trip with ChangeNotifier { String uuid; - int totalTime; + Duration totalTime; LinkedList 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? landmarks // a trip can be created with no landmarks, but the list should be initialized anyway }) : landmarks = landmarks ?? LinkedList(); @@ -53,7 +53,7 @@ class Trip with ChangeNotifier { factory Trip.fromJson(Map 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 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 toJson() => { 'uuid': uuid, - 'total_time': totalTime, + 'total_time': totalTime.inMinutes, 'first_landmark_uuid': landmarks.first.uuid }; diff --git a/frontend/lib/utils/get_first_page.dart b/frontend/lib/utils/get_first_page.dart index 26f2787..bc54b79 100644 --- a/frontend/lib/utils/get_first_page.dart +++ b/frontend/lib/utils/get_first_page.dart @@ -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 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 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 { - return const OnboardingPage(); + return OnboardingPage(); } - } + }, ); - // Future> 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 availableTrips = snapshot.data!; - // if (availableTrips.isNotEmpty) { - // return TripPage(trip: availableTrips[0]); - // } else { - // return OnboardingPage(); - // } - // } else { - // return CircularProgressIndicator(); - // } - // } - // ); } \ No newline at end of file diff --git a/frontend/lib/utils/load_trips.dart b/frontend/lib/utils/load_trips.dart index dbf7aa7..6506e77 100644 --- a/frontend/lib/utils/load_trips.dart +++ b/frontend/lib/utils/load_trips.dart @@ -8,7 +8,7 @@ class SavedTrips extends ChangeNotifier { List get trips => _trips; - void loadTrips() async { + Future> loadTrips() async { SharedPreferences prefs = await SharedPreferences.getInstance(); List trips = []; @@ -21,6 +21,7 @@ class SavedTrips extends ChangeNotifier { } _trips = trips; notifyListeners(); + return trips; } void addTrip(Trip trip) async {