quite a few UX improvements

This commit is contained in:
Remy Moll 2025-03-23 20:00:24 +01:00
parent 4ad867e609
commit e148c851e1
12 changed files with 166 additions and 60 deletions

View File

@ -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,
); );
} }
} }

View File

@ -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),
] ]
), ),
], ],
) )
); )
} );
} }

View File

@ -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) {

View File

@ -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

View File

@ -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)
), ),
], ],

View File

@ -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(

View 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)
],
)
)
)
);
}

View File

@ -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(() {});
} }

View 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);
}

View File

@ -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
}; };

View File

@ -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();
// }
// }
// );
} }

View File

@ -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 {