more pleasant progress handling, although somewhat flawed

This commit is contained in:
Remy Moll 2025-02-14 12:23:41 +01:00
parent 1d5553f7f2
commit b82f9997a4
8 changed files with 287 additions and 172 deletions

View File

@ -7,14 +7,11 @@ import 'package:anyway/structs/landmark.dart';
import 'package:anyway/structs/trip.dart'; import 'package:anyway/structs/trip.dart';
// Returns a list of widgets that represent the landmarks matching the given selector
List<Widget> landmarksList(Trip trip) { List<Widget> landmarksList(Trip trip, {required bool Function(Landmark) selector}) {
log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks");
List<Widget> children = []; List<Widget> children = [];
log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks");
if (trip.landmarks.isEmpty || trip.landmarks.length <= 1 && trip.landmarks.first.type == typeStart ) { if (trip.landmarks.isEmpty || trip.landmarks.length <= 1 && trip.landmarks.first.type == typeStart ) {
children.add( children.add(
const Text("No landmarks in this trip"), const Text("No landmarks in this trip"),
@ -23,14 +20,16 @@ List<Widget> landmarksList(Trip trip) {
} }
for (Landmark landmark in trip.landmarks) { for (Landmark landmark in trip.landmarks) {
children.add( if (selector(landmark)) {
LandmarkCard(landmark, trip),
);
if (landmark.next != null) {
children.add( children.add(
StepBetweenLandmarks(current: landmark, next: landmark.next!) LandmarkCard(landmark, trip),
); );
if (!landmark.visited && landmark.next != null) {
children.add(
StepBetweenLandmarks(current: landmark, next: landmark.next!)
);
}
} }
} }

View File

@ -9,14 +9,10 @@ import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:widget_to_marker/widget_to_marker.dart'; import 'package:widget_to_marker/widget_to_marker.dart';
class CurrentTripMap extends StatefulWidget { class CurrentTripMap extends StatefulWidget {
final Trip? trip; final Trip? trip;
CurrentTripMap({ CurrentTripMap({this.trip});
this.trip
});
@override @override
State<CurrentTripMap> createState() => _CurrentTripMapState(); State<CurrentTripMap> createState() => _CurrentTripMapState();
@ -30,7 +26,23 @@ class _CurrentTripMapState extends State<CurrentTripMap> {
zoom: 11.0, zoom: 11.0,
); );
Set<Marker> mapMarkers = <Marker>{}; Set<Marker> mapMarkers = <Marker>{};
Set<Polyline> mapPolylines = <Polyline>{};
@override
void initState() {
super.initState();
widget.trip?.addListener(setMapMarkers);
widget.trip?.addListener(setMapRoute);
}
@override
void dispose() {
widget.trip?.removeListener(setMapMarkers);
widget.trip?.removeListener(setMapRoute);
super.dispose();
}
void _onMapCreated(GoogleMapController controller) async { void _onMapCreated(GoogleMapController controller) async {
mapController = controller; mapController = controller;
@ -40,16 +52,17 @@ class _CurrentTripMapState extends State<CurrentTripMap> {
controller.moveCamera(update); controller.moveCamera(update);
} }
setMapMarkers(); setMapMarkers();
setMapRoute();
} }
void _onCameraIdle() { void _onCameraIdle() {
// print(mapController.getLatLng(ScreenCoordinate(x: 0, y: 0))); // print(mapController.getLatLng(ScreenCoordinate(x: 0, y: 0)));
} }
void setMapMarkers() async { void setMapMarkers() async {
List<Landmark> landmarks = widget.trip?.landmarks.toList() ?? []; List<Landmark> landmarks = widget.trip?.landmarks.toList() ?? [];
Set<Marker> newMarkers = <Marker>{}; Set<Marker> markers = <Marker>{};
for (int i = 0; i < landmarks.length; i++) { for (int i = 0; i < landmarks.length; i++) {
Landmark landmark = landmarks[i]; Landmark landmark = landmarks[i];
List<double> location = landmark.location; List<double> location = landmark.location;
@ -58,20 +71,47 @@ class _CurrentTripMapState extends State<CurrentTripMap> {
position: LatLng(location[0], location[1]), position: LatLng(location[0], location[1]),
icon: await ThemedMarker(landmark: landmark, position: i).toBitmapDescriptor( icon: await ThemedMarker(landmark: landmark, position: i).toBitmapDescriptor(
logicalSize: const Size(150, 150), logicalSize: const Size(150, 150),
imageSize: const Size(150, 150) imageSize: const Size(150, 150),
), ),
); );
newMarkers.add(marker); markers.add(marker);
} }
setState(() { setState(() {
mapMarkers = newMarkers; mapMarkers = markers;
}); });
} }
void setMapRoute() async {
List<Landmark> landmarks = widget.trip?.landmarks.toList() ?? [];
Set<Polyline> polyLines = <Polyline>{};
if (landmarks.length < 2) {
return;
}
for (Landmark landmark in landmarks) {
if (landmark.next != null) {
List<LatLng> step = [
LatLng(landmark.location[0], landmark.location[1]),
LatLng(landmark.next!.location[0], landmark.next!.location[1])
];
Polyline stepLine = Polyline(
polylineId: PolylineId('step-${landmark.uuid}'),
points: step,
color: landmark.visited ? Colors.grey : PRIMARY_COLOR,
width: 5,
);
polyLines.add(stepLine);
}
}
setState(() {
mapPolylines = polyLines;
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
widget.trip?.addListener(setMapMarkers);
Future<SharedPreferences> preferences = SharedPreferences.getInstance(); Future<SharedPreferences> preferences = SharedPreferences.getInstance();
return FutureBuilder( return FutureBuilder(
@ -84,7 +124,7 @@ class _CurrentTripMapState extends State<CurrentTripMap> {
} else { } else {
return const CircularProgressIndicator(); return const CircularProgressIndicator();
} }
} },
); );
} }
@ -93,8 +133,8 @@ class _CurrentTripMapState extends State<CurrentTripMap> {
onMapCreated: _onMapCreated, onMapCreated: _onMapCreated,
initialCameraPosition: _cameraPosition, initialCameraPosition: _cameraPosition,
onCameraIdle: _onCameraIdle, onCameraIdle: _onCameraIdle,
// onLongPress: ,
markers: mapMarkers, markers: mapMarkers,
polylines: mapPolylines,
cloudMapId: MAP_ID, cloudMapId: MAP_ID,
mapToolbarEnabled: false, mapToolbarEnabled: false,
zoomControlsEnabled: false, zoomControlsEnabled: false,
@ -102,5 +142,4 @@ class _CurrentTripMapState extends State<CurrentTripMap> {
myLocationButtonEnabled: false, myLocationButtonEnabled: false,
); );
} }
} }

View File

@ -1,6 +1,7 @@
import 'package:anyway/constants.dart'; import 'package:anyway/constants.dart';
import 'package:anyway/modules/current_trip_error_message.dart'; import 'package:anyway/modules/current_trip_error_message.dart';
import 'package:anyway/modules/current_trip_loading_indicator.dart'; import 'package:anyway/modules/current_trip_loading_indicator.dart';
import 'package:anyway/structs/landmark.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:anyway/structs/trip.dart'; import 'package:anyway/structs/trip.dart';
@ -63,13 +64,40 @@ class _CurrentTripPanelState extends State<CurrentTripPanel> {
child: CurrentTripGreeter(trip: widget.trip), child: CurrentTripGreeter(trip: widget.trip),
), ),
Padding(
padding: EdgeInsets.all(10),
child: Container(
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(10),
),
child: Column(
children: [
CurrentTripSummary(trip: widget.trip),
ExpansionTile(
leading: Icon(Icons.location_on),
title: Text('Visited Landmarks (tap to expand)'),
children: [
...landmarksList(widget.trip, selector: (Landmark landmark) => landmark.visited),
],
visualDensity: VisualDensity.compact,
collapsedShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
],
),
),
),
const Padding(padding: EdgeInsets.only(top: 10)), const Padding(padding: EdgeInsets.only(top: 10)),
// CurrentTripSummary(trip: widget.trip), // upcoming landmarks
...landmarksList(widget.trip, selector: (Landmark landmark) => landmark.visited == false),
// const Divider(),
...landmarksList(widget.trip),
const Padding(padding: EdgeInsets.only(top: 10)), const Padding(padding: EdgeInsets.only(top: 10)),

View File

@ -15,17 +15,27 @@ class CurrentTripSummary extends StatefulWidget {
class _CurrentTripSummaryState extends State<CurrentTripSummary> { class _CurrentTripSummaryState extends State<CurrentTripSummary> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Padding(
children: [ padding: EdgeInsets.symmetric(vertical: 10, horizontal: 20),
Text('Summary'), child: Row(
// Text('Start: ${widget.trip.start}'), mainAxisAlignment: MainAxisAlignment.spaceBetween,
// Text('End: ${widget.trip.end}'), children: [
Text('Total duration: ${widget.trip.totalTime}'), Row(
Text('Total distance: ${widget.trip.totalTime}'), children: [
// Text('Fuel: ${widget.trip.fuel}'), Icon(Icons.flag, size: 20),
// Text('Cost: ${widget.trip.cost}'), Padding(padding: EdgeInsets.only(right: 10)),
], Text('Stops: ${widget.trip.landmarks.length}', style: Theme.of(context).textTheme.bodyLarge,),
]
),
Row(
children: [
Icon(Icons.hourglass_bottom_rounded, size: 20),
Padding(padding: EdgeInsets.only(right: 10)),
Text('Duration: ${widget.trip.totalTime} minutes', style: Theme.of(context).textTheme.bodyLarge,),
]
),
],
)
); );
} }
} }

View File

@ -1,3 +1,4 @@
import 'package:anyway/constants.dart';
import 'package:anyway/main.dart'; import 'package:anyway/main.dart';
import 'package:anyway/structs/trip.dart'; import 'package:anyway/structs/trip.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -13,7 +14,7 @@ class LandmarkCard extends StatefulWidget {
LandmarkCard( LandmarkCard(
this.landmark, this.landmark,
this.parentTrip, this.parentTrip,
); );
@override @override
_LandmarkCardState createState() => _LandmarkCardState(); _LandmarkCardState createState() => _LandmarkCardState();
@ -31,142 +32,176 @@ class _LandmarkCardState extends State<LandmarkCard> {
); );
} }
// else:
return Container( return Container(
constraints: BoxConstraints(
minHeight: 50,
maxHeight: 200,
),
child: Card( child: Card(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0), borderRadius: BorderRadius.circular(15.0),
), ),
elevation: 5, elevation: 5,
clipBehavior: Clip.antiAliasWithSaveLayer, clipBehavior: Clip.antiAliasWithSaveLayer,
// if the image is available, display it on the left side of the card, otherwise only display the text // if the image is available, display it on the left side of the card, otherwise only display the text
child: widget.landmark.imageURL != null ? splitLayout() : textLayout(), child: Row(
), crossAxisAlignment: CrossAxisAlignment.start,
); children: [
} // Image and landmark "type" on the left
AspectRatio(
Widget splitLayout() { aspectRatio: 3 / 4,
// If an image is available, display it on the left side of the card child: Column(
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
// the image on the left
width: 160,
height: 160,
child: CachedNetworkImage(
imageUrl: widget.landmark.imageURL ?? '',
placeholder: (context, url) => Center(child: CircularProgressIndicator()),
errorWidget: (context, error, stackTrace) => Icon(Icons.question_mark_outlined),
fit: BoxFit.cover,
),
),
Flexible(
child: textLayout(),
),
],
);
}
Widget textLayout() {
return Padding(
padding: EdgeInsets.all(10),
child: Column(
children: [
Row(
children: [
Flexible(
child: Text(
widget.landmark.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
maxLines: 2,
),
)
],
),
if (widget.landmark.nameEN != null)
Row(
children: [
Flexible(
child: Text(
widget.landmark.nameEN!,
style: const TextStyle(
fontSize: 16,
),
maxLines: 1,
),
)
],
),
Padding(padding: EdgeInsets.only(top: 10)),
Align(
alignment: Alignment.centerLeft,
child: SingleChildScrollView(
// allows the buttons to be scrolled
scrollDirection: Axis.horizontal,
child: Wrap(
spacing: 10,
// show the type, the website, and the wikipedia link as buttons/labels in a row
children: [ children: [
TextButton.icon( if (widget.landmark.imageURL != null && widget.landmark.imageURL!.isNotEmpty)
onPressed: () {}, Expanded(
icon: widget.landmark.type.icon, child: CachedNetworkImage(
label: Text(widget.landmark.type.name), imageUrl: widget.landmark.imageURL!,
), placeholder: (context, url) => Center(child: CircularProgressIndicator()),
if (widget.landmark.duration != null && widget.landmark.duration!.inMinutes > 0) errorWidget: (context, error, stackTrace) => Icon(Icons.question_mark_outlined),
TextButton.icon( fit: BoxFit.cover,
onPressed: () {}, )
icon: Icon(Icons.hourglass_bottom), )
label: Text('${widget.landmark.duration!.inMinutes} minutes'), else
), Expanded(
if (widget.landmark.websiteURL != null) child:
TextButton.icon( Container(
onPressed: () async { decoration: BoxDecoration(
// open a browser with the website link gradient: LinearGradient(
await launchUrl(Uri.parse(widget.landmark.websiteURL!)); begin: Alignment.topLeft,
}, end: Alignment.bottomRight,
icon: Icon(Icons.link), colors: [GRADIENT_START, GRADIENT_END],
label: Text('Website'), ),
), ),
PopupMenuButton( child: Center(
icon: Icon(Icons.settings), child: Icon(widget.landmark.type.icon.icon, size: 50),
style: TextButtonTheme.of(context).style,
itemBuilder: (context) => [
PopupMenuItem(
child: ListTile(
leading: Icon(Icons.delete),
title: Text('Delete'),
onTap: () async {
widget.parentTrip.removeLandmark(widget.landmark);
rootScaffoldMessengerKey.currentState!.showSnackBar(
SnackBar(content: Text("We won't show ${widget.landmark.name} again"))
);
},
), ),
), ),
PopupMenuItem( ),
child: ListTile(
leading: Icon(Icons.star), Container(
title: Text('Favorite'), color: PRIMARY_COLOR,
onTap: () async { child: Center(
// delete the landmark child: Padding(
// await deleteLandmark(widget.landmark); padding: EdgeInsets.all(5),
}, child: Row(
), mainAxisAlignment: MainAxisAlignment.center,
), spacing: 5,
], children: [
Icon(widget.landmark.type.icon.icon, size: 16),
Text(widget.landmark.type.name, style: TextStyle(fontWeight: FontWeight.bold)),
],
)
)
),
) )
], ],
), )
), ),
),
], // Main information, useful buttons on the right
), Expanded(
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.landmark.name,
style: Theme.of(context).textTheme.titleMedium,
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
if (widget.landmark.nameEN != null)
Text(
widget.landmark.nameEN!,
style: Theme.of(context).textTheme.bodyMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
// fill the vspace
const Spacer(),
SingleChildScrollView(
// allows the buttons to be scrolled
scrollDirection: Axis.horizontal,
child: Wrap(
spacing: 10,
// show the type, the website, and the wikipedia link as buttons/labels in a row
children: [
doneToggleButton(),
// if (widget.landmark.duration != null && widget.landmark.duration!.inMinutes > 0)
// TextButton.icon(
// onPressed: () {},
// icon: Icon(Icons.hourglass_bottom),
// label: Text('${widget.landmark.duration!.inMinutes} minutes'),
// ),
if (widget.landmark.websiteURL != null)
websiteButton(),
optionsButton()
],
),
),
],
),
),
)
],
)
)
); );
} }
Widget doneToggleButton() {
return TextButton.icon(
onPressed: () async {
widget.landmark.visited = !widget.landmark.visited;
widget.parentTrip.notifyUpdate();
},
icon: Icon(widget.landmark.visited ? Icons.undo_sharp : Icons.check),
label: Text(widget.landmark.visited ? "Add to overview" : "Done!"),
);
}
Widget websiteButton () => TextButton.icon(
onPressed: () async {
// open a browser with the website link
await launchUrl(Uri.parse(widget.landmark.websiteURL!));
},
icon: Icon(Icons.link),
label: Text('Website'),
);
Widget optionsButton () => PopupMenuButton(
icon: Icon(Icons.settings),
style: TextButtonTheme.of(context).style,
itemBuilder: (context) => [
PopupMenuItem(
child: ListTile(
leading: Icon(Icons.delete),
title: Text('Delete'),
onTap: () async {
widget.parentTrip.removeLandmark(widget.landmark);
rootScaffoldMessengerKey.currentState!.showSnackBar(
SnackBar(content: Text("We won't show ${widget.landmark.name} again"))
);
},
),
),
PopupMenuItem(
child: ListTile(
leading: Icon(Icons.star),
title: Text('Favorite'),
onTap: () async {
// delete the landmark
// await deleteLandmark(widget.landmark);
},
),
),
],
);
} }

View File

@ -40,7 +40,7 @@ class ThemedMarker extends StatelessWidget {
children: [ children: [
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: APP_GRADIENT, gradient: landmark.visited ? LinearGradient(colors: [Colors.grey, Colors.white]) : APP_GRADIENT,
shape: BoxShape.circle, shape: BoxShape.circle,
border: Border.all(color: Colors.black, width: 5), border: Border.all(color: Colors.black, width: 5),
), ),

View File

@ -27,7 +27,7 @@ final class Landmark extends LinkedListEntry<Landmark>{
String? imageURL; // not final because it can be patched String? imageURL; // not final because it can be patched
final String? description; final String? description;
final Duration? duration; final Duration? duration;
final bool? visited; bool visited;
// Next node // Next node
// final Landmark? next; // final Landmark? next;
@ -46,7 +46,7 @@ final class Landmark extends LinkedListEntry<Landmark>{
this.imageURL, this.imageURL,
this.description, this.description,
this.duration, this.duration,
this.visited, this.visited = false,
// this.next, // this.next,
this.tripTime, this.tripTime,
@ -71,7 +71,7 @@ final class Landmark extends LinkedListEntry<Landmark>{
final imageURL = json['image_url'] as String?; final imageURL = json['image_url'] as String?;
final description = json['description'] as String?; final description = json['description'] as String?;
var duration = Duration(minutes: json['duration'] ?? 0) as Duration?; var duration = Duration(minutes: json['duration'] ?? 0) as Duration?;
final visited = json['visited'] as bool?; final visited = json['visited'] ?? false as bool;
var tripTime = Duration(minutes: json['time_to_reach_next'] ?? 0) as Duration?; var tripTime = Duration(minutes: json['time_to_reach_next'] ?? 0) as Duration?;
return Landmark( return Landmark(

View File

@ -52,6 +52,7 @@ class Trip with ChangeNotifier {
totalTime = json['total_time']; totalTime = json['total_time'];
notifyListeners(); notifyListeners();
} }
void addLandmark(Landmark landmark) { void addLandmark(Landmark landmark) {
landmarks.add(landmark); landmarks.add(landmark);
notifyListeners(); notifyListeners();
@ -66,12 +67,16 @@ class Trip with ChangeNotifier {
landmarks.remove(landmark); landmarks.remove(landmark);
notifyListeners(); notifyListeners();
} }
void updateError(String error) { void updateError(String error) {
errorDescription = error; errorDescription = error;
notifyListeners(); notifyListeners();
} }
void notifyUpdate(){
notifyListeners();
}
factory Trip.fromPrefs(SharedPreferences prefs, String uuid) { factory Trip.fromPrefs(SharedPreferences prefs, String uuid) {
String? content = prefs.getString('trip_$uuid'); String? content = prefs.getString('trip_$uuid');
Map<String, dynamic> json = jsonDecode(content!); Map<String, dynamic> json = jsonDecode(content!);
@ -80,7 +85,6 @@ class Trip with ChangeNotifier {
log('Loading trip $uuid with first landmark $firstUUID'); log('Loading trip $uuid with first landmark $firstUUID');
LinkedList<Landmark> landmarks = readLandmarks(prefs, firstUUID); LinkedList<Landmark> landmarks = readLandmarks(prefs, firstUUID);
trip.landmarks = landmarks; trip.landmarks = landmarks;
// notifyListeners();
return trip; return trip;
} }