more pleasant progress handling, although somewhat flawed

This commit is contained in:
Remy Moll 2025-02-14 12:23:41 +01:00
parent aed407e2d0
commit 8f6dfd404d
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,16 +20,18 @@ List<Widget> landmarksList(Trip trip) {
} }
for (Landmark landmark in trip.landmarks) { for (Landmark landmark in trip.landmarks) {
if (selector(landmark)) {
children.add( children.add(
LandmarkCard(landmark, trip), LandmarkCard(landmark, trip),
); );
if (landmark.next != null) { if (!landmark.visited && landmark.next != null) {
children.add( children.add(
StepBetweenLandmarks(current: landmark, next: landmark.next!) StepBetweenLandmarks(current: landmark, next: landmark.next!)
); );
} }
} }
}
return children; return children;
} }

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(
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text('Summary'), Row(
// Text('Start: ${widget.trip.start}'), children: [
// Text('End: ${widget.trip.end}'), Icon(Icons.flag, size: 20),
Text('Total duration: ${widget.trip.totalTime}'), Padding(padding: EdgeInsets.only(right: 10)),
Text('Total distance: ${widget.trip.totalTime}'), Text('Stops: ${widget.trip.landmarks.length}', style: Theme.of(context).textTheme.bodyLarge,),
// Text('Fuel: ${widget.trip.fuel}'), ]
// Text('Cost: ${widget.trip.cost}'), ),
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';
@ -31,108 +32,151 @@ 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
child: widget.landmark.imageURL != null ? splitLayout() : textLayout(),
),
);
}
Widget splitLayout() { // if the image is available, display it on the left side of the card, otherwise only display the text
// If an image is available, display it on the left side of the card child: Row(
return Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( // Image and landmark "type" on the left
// the image on the left AspectRatio(
width: 160, aspectRatio: 3 / 4,
height: 160, child: Column(
children: [
if (widget.landmark.imageURL != null && widget.landmark.imageURL!.isNotEmpty)
Expanded(
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: widget.landmark.imageURL ?? '', imageUrl: widget.landmark.imageURL!,
placeholder: (context, url) => Center(child: CircularProgressIndicator()), placeholder: (context, url) => Center(child: CircularProgressIndicator()),
errorWidget: (context, error, stackTrace) => Icon(Icons.question_mark_outlined), errorWidget: (context, error, stackTrace) => Icon(Icons.question_mark_outlined),
fit: BoxFit.cover, 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),
), ),
), ),
Flexible(
child: textLayout(),
), ),
],
);
}
Widget textLayout() { Container(
return Padding( color: PRIMARY_COLOR,
padding: EdgeInsets.all(10), child: Center(
child: Column( child: Padding(
padding: EdgeInsets.all(5),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 5,
children: [ children: [
Row( Icon(widget.landmark.type.icon.icon, size: 16),
children: [ Text(widget.landmark.type.name, style: TextStyle(fontWeight: FontWeight.bold)),
Flexible( ],
child: Text( )
widget.landmark.name, )
style: const TextStyle(
fontSize: 18,
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, maxLines: 2,
), ),
)
],
),
if (widget.landmark.nameEN != null) if (widget.landmark.nameEN != null)
Row( Text(
children: [
Flexible(
child: Text(
widget.landmark.nameEN!, widget.landmark.nameEN!,
style: const TextStyle( style: Theme.of(context).textTheme.bodyMedium,
fontSize: 16,
),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis,
), ),
)
], // fill the vspace
), const Spacer(),
Padding(padding: EdgeInsets.only(top: 10)),
Align( SingleChildScrollView(
alignment: Alignment.centerLeft,
child: SingleChildScrollView(
// allows the buttons to be scrolled // allows the buttons to be scrolled
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Wrap( child: Wrap(
spacing: 10, spacing: 10,
// show the type, the website, and the wikipedia link as buttons/labels in a row // show the type, the website, and the wikipedia link as buttons/labels in a row
children: [ children: [
TextButton.icon( doneToggleButton(),
onPressed: () {}, // if (widget.landmark.duration != null && widget.landmark.duration!.inMinutes > 0)
icon: widget.landmark.type.icon, // TextButton.icon(
label: Text(widget.landmark.type.name), // onPressed: () {},
), // icon: Icon(Icons.hourglass_bottom),
if (widget.landmark.duration != null && widget.landmark.duration!.inMinutes > 0) // label: Text('${widget.landmark.duration!.inMinutes} minutes'),
TextButton.icon( // ),
onPressed: () {},
icon: Icon(Icons.hourglass_bottom),
label: Text('${widget.landmark.duration!.inMinutes} minutes'),
),
if (widget.landmark.websiteURL != null) if (widget.landmark.websiteURL != null)
TextButton.icon( 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 { onPressed: () async {
// open a browser with the website link // open a browser with the website link
await launchUrl(Uri.parse(widget.landmark.websiteURL!)); await launchUrl(Uri.parse(widget.landmark.websiteURL!));
}, },
icon: Icon(Icons.link), icon: Icon(Icons.link),
label: Text('Website'), label: Text('Website'),
), );
PopupMenuButton(
Widget optionsButton () => PopupMenuButton(
icon: Icon(Icons.settings), icon: Icon(Icons.settings),
style: TextButtonTheme.of(context).style, style: TextButtonTheme.of(context).style,
itemBuilder: (context) => [ itemBuilder: (context) => [
@ -159,14 +203,5 @@ class _LandmarkCardState extends State<LandmarkCard> {
), ),
), ),
], ],
)
],
),
),
),
],
),
); );
}
} }

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();
@ -72,6 +73,10 @@ class Trip with ChangeNotifier {
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;
} }