From 8f6dfd404d6acc6e5bb854f9337c0b5494e906b8 Mon Sep 17 00:00:00 2001 From: Remy Moll Date: Fri, 14 Feb 2025 12:23:41 +0100 Subject: [PATCH] more pleasant progress handling, although somewhat flawed --- .../modules/current_trip_landmarks_list.dart | 21 +- frontend/lib/modules/current_trip_map.dart | 69 ++++- frontend/lib/modules/current_trip_panel.dart | 38 ++- .../lib/modules/current_trip_summary.dart | 34 ++- frontend/lib/modules/landmark_card.dart | 281 ++++++++++-------- frontend/lib/modules/landmark_map_marker.dart | 2 +- frontend/lib/structs/landmark.dart | 6 +- frontend/lib/structs/trip.dart | 8 +- 8 files changed, 287 insertions(+), 172 deletions(-) diff --git a/frontend/lib/modules/current_trip_landmarks_list.dart b/frontend/lib/modules/current_trip_landmarks_list.dart index 39333f6..c92e920 100644 --- a/frontend/lib/modules/current_trip_landmarks_list.dart +++ b/frontend/lib/modules/current_trip_landmarks_list.dart @@ -7,14 +7,11 @@ import 'package:anyway/structs/landmark.dart'; import 'package:anyway/structs/trip.dart'; - -List landmarksList(Trip trip) { - log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks"); +// Returns a list of widgets that represent the landmarks matching the given selector +List landmarksList(Trip trip, {required bool Function(Landmark) selector}) { List children = []; - log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks"); - if (trip.landmarks.isEmpty || trip.landmarks.length <= 1 && trip.landmarks.first.type == typeStart ) { children.add( const Text("No landmarks in this trip"), @@ -23,14 +20,16 @@ List landmarksList(Trip trip) { } for (Landmark landmark in trip.landmarks) { - children.add( - LandmarkCard(landmark, trip), - ); - - if (landmark.next != null) { + if (selector(landmark)) { 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!) + ); + } } } diff --git a/frontend/lib/modules/current_trip_map.dart b/frontend/lib/modules/current_trip_map.dart index 9efdc4c..1802404 100644 --- a/frontend/lib/modules/current_trip_map.dart +++ b/frontend/lib/modules/current_trip_map.dart @@ -9,14 +9,10 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:widget_to_marker/widget_to_marker.dart'; - class CurrentTripMap extends StatefulWidget { - final Trip? trip; - CurrentTripMap({ - this.trip - }); + CurrentTripMap({this.trip}); @override State createState() => _CurrentTripMapState(); @@ -30,7 +26,23 @@ class _CurrentTripMapState extends State { zoom: 11.0, ); Set mapMarkers = {}; - + Set mapPolylines = {}; + + @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 { mapController = controller; @@ -40,16 +52,17 @@ class _CurrentTripMapState extends State { controller.moveCamera(update); } setMapMarkers(); + setMapRoute(); } void _onCameraIdle() { // print(mapController.getLatLng(ScreenCoordinate(x: 0, y: 0))); } - void setMapMarkers() async { List landmarks = widget.trip?.landmarks.toList() ?? []; - Set newMarkers = {}; + Set markers = {}; + for (int i = 0; i < landmarks.length; i++) { Landmark landmark = landmarks[i]; List location = landmark.location; @@ -58,20 +71,47 @@ class _CurrentTripMapState extends State { position: LatLng(location[0], location[1]), icon: await ThemedMarker(landmark: landmark, position: i).toBitmapDescriptor( logicalSize: const Size(150, 150), - imageSize: const Size(150, 150) + imageSize: const Size(150, 150), ), ); - newMarkers.add(marker); + markers.add(marker); } setState(() { - mapMarkers = newMarkers; + mapMarkers = markers; }); } + void setMapRoute() async { + List landmarks = widget.trip?.landmarks.toList() ?? []; + Set polyLines = {}; + + if (landmarks.length < 2) { + return; + } + + for (Landmark landmark in landmarks) { + if (landmark.next != null) { + List 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 Widget build(BuildContext context) { - widget.trip?.addListener(setMapMarkers); Future preferences = SharedPreferences.getInstance(); return FutureBuilder( @@ -84,7 +124,7 @@ class _CurrentTripMapState extends State { } else { return const CircularProgressIndicator(); } - } + }, ); } @@ -93,8 +133,8 @@ class _CurrentTripMapState extends State { onMapCreated: _onMapCreated, initialCameraPosition: _cameraPosition, onCameraIdle: _onCameraIdle, - // onLongPress: , markers: mapMarkers, + polylines: mapPolylines, cloudMapId: MAP_ID, mapToolbarEnabled: false, zoomControlsEnabled: false, @@ -102,5 +142,4 @@ class _CurrentTripMapState extends State { myLocationButtonEnabled: false, ); } - } diff --git a/frontend/lib/modules/current_trip_panel.dart b/frontend/lib/modules/current_trip_panel.dart index 0cf111b..230c4c3 100644 --- a/frontend/lib/modules/current_trip_panel.dart +++ b/frontend/lib/modules/current_trip_panel.dart @@ -1,6 +1,7 @@ import 'package:anyway/constants.dart'; import 'package:anyway/modules/current_trip_error_message.dart'; import 'package:anyway/modules/current_trip_loading_indicator.dart'; +import 'package:anyway/structs/landmark.dart'; import 'package:flutter/material.dart'; import 'package:anyway/structs/trip.dart'; @@ -63,13 +64,40 @@ class _CurrentTripPanelState extends State { 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)), - // CurrentTripSummary(trip: widget.trip), - - // const Divider(), - - ...landmarksList(widget.trip), + // upcoming landmarks + ...landmarksList(widget.trip, selector: (Landmark landmark) => landmark.visited == false), const Padding(padding: EdgeInsets.only(top: 10)), diff --git a/frontend/lib/modules/current_trip_summary.dart b/frontend/lib/modules/current_trip_summary.dart index 77c9461..bc46092 100644 --- a/frontend/lib/modules/current_trip_summary.dart +++ b/frontend/lib/modules/current_trip_summary.dart @@ -15,17 +15,27 @@ class CurrentTripSummary extends StatefulWidget { class _CurrentTripSummaryState extends State { @override Widget build(BuildContext context) { - return Column( - children: [ - Text('Summary'), - // Text('Start: ${widget.trip.start}'), - // Text('End: ${widget.trip.end}'), - Text('Total duration: ${widget.trip.totalTime}'), - Text('Total distance: ${widget.trip.totalTime}'), - // Text('Fuel: ${widget.trip.fuel}'), - // Text('Cost: ${widget.trip.cost}'), - ], + return Padding( + padding: EdgeInsets.symmetric(vertical: 10, horizontal: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon(Icons.flag, size: 20), + 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,), + ] + ), + ], + ) ); - } -} \ No newline at end of file +} diff --git a/frontend/lib/modules/landmark_card.dart b/frontend/lib/modules/landmark_card.dart index e920e79..e3e8a75 100644 --- a/frontend/lib/modules/landmark_card.dart +++ b/frontend/lib/modules/landmark_card.dart @@ -1,3 +1,4 @@ +import 'package:anyway/constants.dart'; import 'package:anyway/main.dart'; import 'package:anyway/structs/trip.dart'; import 'package:flutter/material.dart'; @@ -13,7 +14,7 @@ class LandmarkCard extends StatefulWidget { LandmarkCard( this.landmark, this.parentTrip, - ); + ); @override _LandmarkCardState createState() => _LandmarkCardState(); @@ -31,142 +32,176 @@ class _LandmarkCardState extends State { ); } - // else: + return Container( + constraints: BoxConstraints( + minHeight: 50, + maxHeight: 200, + ), child: Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15.0), ), elevation: 5, clipBehavior: Clip.antiAliasWithSaveLayer, + // 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(), - ), - ); - } - - Widget splitLayout() { - // If an image is available, display it on the left side of the card - 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 + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Image and landmark "type" on the left + AspectRatio( + aspectRatio: 3 / 4, + child: Column( children: [ - TextButton.icon( - onPressed: () {}, - icon: widget.landmark.type.icon, - label: Text(widget.landmark.type.name), - ), - 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) - 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'), - ), - 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")) - ); - }, + if (widget.landmark.imageURL != null && widget.landmark.imageURL!.isNotEmpty) + Expanded( + child: CachedNetworkImage( + imageUrl: widget.landmark.imageURL!, + placeholder: (context, url) => Center(child: CircularProgressIndicator()), + errorWidget: (context, error, stackTrace) => Icon(Icons.question_mark_outlined), + fit: BoxFit.cover, + ) + ) + else + Expanded( + child: + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [GRADIENT_START, GRADIENT_END], + ), + ), + child: Center( + child: Icon(widget.landmark.type.icon.icon, size: 50), ), ), - PopupMenuItem( - child: ListTile( - leading: Icon(Icons.star), - title: Text('Favorite'), - onTap: () async { - // delete the landmark - // await deleteLandmark(widget.landmark); - }, - ), - ), - ], + ), + + Container( + color: PRIMARY_COLOR, + child: Center( + child: Padding( + 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); + }, + ), + ), + ], + ); } diff --git a/frontend/lib/modules/landmark_map_marker.dart b/frontend/lib/modules/landmark_map_marker.dart index 2e8f5d7..fccf50a 100644 --- a/frontend/lib/modules/landmark_map_marker.dart +++ b/frontend/lib/modules/landmark_map_marker.dart @@ -40,7 +40,7 @@ class ThemedMarker extends StatelessWidget { children: [ Container( decoration: BoxDecoration( - gradient: APP_GRADIENT, + gradient: landmark.visited ? LinearGradient(colors: [Colors.grey, Colors.white]) : APP_GRADIENT, shape: BoxShape.circle, border: Border.all(color: Colors.black, width: 5), ), diff --git a/frontend/lib/structs/landmark.dart b/frontend/lib/structs/landmark.dart index 50a71c5..a0e5905 100644 --- a/frontend/lib/structs/landmark.dart +++ b/frontend/lib/structs/landmark.dart @@ -27,7 +27,7 @@ final class Landmark extends LinkedListEntry{ String? imageURL; // not final because it can be patched final String? description; final Duration? duration; - final bool? visited; + bool visited; // Next node // final Landmark? next; @@ -46,7 +46,7 @@ final class Landmark extends LinkedListEntry{ this.imageURL, this.description, this.duration, - this.visited, + this.visited = false, // this.next, this.tripTime, @@ -71,7 +71,7 @@ final class Landmark extends LinkedListEntry{ final imageURL = json['image_url'] as String?; final description = json['description'] as String?; 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?; return Landmark( diff --git a/frontend/lib/structs/trip.dart b/frontend/lib/structs/trip.dart index 44e12c3..8d816ca 100644 --- a/frontend/lib/structs/trip.dart +++ b/frontend/lib/structs/trip.dart @@ -52,6 +52,7 @@ class Trip with ChangeNotifier { totalTime = json['total_time']; notifyListeners(); } + void addLandmark(Landmark landmark) { landmarks.add(landmark); notifyListeners(); @@ -66,12 +67,16 @@ class Trip with ChangeNotifier { landmarks.remove(landmark); notifyListeners(); } - + void updateError(String error) { errorDescription = error; notifyListeners(); } + void notifyUpdate(){ + notifyListeners(); + } + factory Trip.fromPrefs(SharedPreferences prefs, String uuid) { String? content = prefs.getString('trip_$uuid'); Map json = jsonDecode(content!); @@ -80,7 +85,6 @@ class Trip with ChangeNotifier { log('Loading trip $uuid with first landmark $firstUUID'); LinkedList landmarks = readLandmarks(prefs, firstUUID); trip.landmarks = landmarks; - // notifyListeners(); return trip; }