ui improvements for trips and landmarks
All checks were successful
Build and push docker image / Build (pull_request) Successful in 1m48s
Build and release APK / Build APK (pull_request) Successful in 4m51s

This commit is contained in:
2024-08-05 10:18:00 +02:00
parent c87a01b2e8
commit 71d9554d97
12 changed files with 237 additions and 217 deletions

View File

@@ -141,7 +141,7 @@ Trip getFirstTrip(Future<List<Trip>> trips) {
uuid: '1',
name: "Eiffel Tower",
location: [48.859, 2.295],
type: LandmarkType(name: "Tower"),
type: monument,
imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a8/Tour_Eiffel_Wikimedia_Commons.jpg/1037px-Tour_Eiffel_Wikimedia_Commons.jpg"
),
);
@@ -150,7 +150,7 @@ Trip getFirstTrip(Future<List<Trip>> trips) {
uuid: "2",
name: "Notre Dame Cathedral",
location: [48.8530, 2.3498],
type: LandmarkType(name: "Monument"),
type: monument,
imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/Notre-Dame_de_Paris%2C_4_October_2017.jpg/440px-Notre-Dame_de_Paris%2C_4_October_2017.jpg"
),
);
@@ -159,7 +159,7 @@ Trip getFirstTrip(Future<List<Trip>> trips) {
uuid: "3",
name: "Louvre palace",
location: [48.8606, 2.3376],
type: LandmarkType(name: "Museum"),
type: museum,
imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/66/Louvre_Museum_Wikimedia_Commons.jpg/540px-Louvre_Museum_Wikimedia_Commons.jpg"
),
);
@@ -168,7 +168,7 @@ Trip getFirstTrip(Future<List<Trip>> trips) {
uuid: "4",
name: "Pont-des-arts",
location: [48.8585, 2.3376],
type: LandmarkType(name: "Bridge"),
type: monument,
imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d1/Pont_des_Arts%2C_6e_Arrondissement%2C_Paris_%28HDR%29_20140320_1.jpg/560px-Pont_des_Arts%2C_6e_Arrondissement%2C_Paris_%28HDR%29_20140320_1.jpg"
),
);
@@ -177,7 +177,7 @@ Trip getFirstTrip(Future<List<Trip>> trips) {
uuid: "5",
name: "Panthéon",
location: [48.847, 2.347],
type: LandmarkType(name: "Monument"),
type: monument,
imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/8/80/Pantheon_of_Paris_007.JPG/1280px-Pantheon_of_Paris_007.JPG"
),
);

View File

@@ -1,4 +1,5 @@
import 'package:anyway/structs/trip.dart';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
@@ -24,14 +25,21 @@ class _GreeterState extends State<Greeter> {
future: widget.trip.cityName,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.hasData) {
return Text(
return AutoSizeText(
maxLines: 1,
'Welcome to ${snapshot.data}!',
style: TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24),
);
} else if (snapshot.hasError) {
return const Text('Welcome to your trip!');
return const AutoSizeText(
maxLines: 1,
'Welcome to your trip!'
);
} else {
return const Text('Welcome to ...');
return const AutoSizeText(
maxLines: 1,
'Welcome to ...'
);
}
}
);

View File

@@ -51,7 +51,7 @@ class _LandmarksOverviewState extends State<LandmarksOverview> {
];
} else {
children = [
landmarksWithSteps(trip.landmarks),
landmarksWithSteps(),
saveButton(),
];
}
@@ -71,55 +71,61 @@ class _LandmarksOverviewState extends State<LandmarksOverview> {
child: const Text('Save'),
);
}
Widget landmarksWithSteps(LinkedList<Landmark> landmarks) {
List<Widget> children = [];
int lkey = 0;
for (Landmark landmark in landmarks) {
children.add(
Dismissible(
key: ValueKey<int>(lkey),
child: LandmarkCard(landmark),
// onDismissed: (direction) {
// // Remove the item from the data source.
// setState(() {
// landmarks.remove(landmark);
// });
// // Then show a snackbar.
// ScaffoldMessenger.of(context)
// .showSnackBar(SnackBar(content: Text("${landmark.name} dismissed")));
// },
background: Container(color: Colors.red),
secondaryBackground: Container(
color: Colors.red,
child: Icon(
Icons.delete,
color: Colors.white,
),
padding: EdgeInsets.all(15),
alignment: Alignment.centerRight,
),
)
Widget landmarksWithSteps() {
return ListenableBuilder(
listenable: widget.trip!,
builder: (BuildContext context, Widget? child) {
List<Widget> children = [];
for (Landmark landmark in widget.trip!.landmarks) {
children.add(
Dismissible(
key: ValueKey<int>(landmark.hashCode),
child: LandmarkCard(landmark),
dismissThresholds: {DismissDirection.endToStart: 0.6},
onDismissed: (direction) {
// Remove the item from the data source.
log(landmark.name);
setState(() {
widget.trip!.removeLandmark(landmark);
});
// Then show a snackbar.
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text("We won't show ${landmark.name} again")));
},
background: Container(color: Colors.red),
secondaryBackground: Container(
color: Colors.red,
child: Icon(
Icons.delete,
color: Colors.white,
),
padding: EdgeInsets.all(15),
alignment: Alignment.centerRight,
),
)
);
if (landmark.next != null) {
Widget step = stepBetweenLandmarks(landmark, landmark.next!);
children.add(step);
}
}
return Column(
children: children
);
},
);
lkey++;
if (landmark.next != null) {
Widget step = stepBetweenLandmarks(landmark);
children.add(step);
}
}
return Column(
children: children
);
}
Widget stepBetweenLandmarks(Landmark landmark) {
Widget stepBetweenLandmarks(Landmark current, Landmark next) {
// This is a simple widget that draws a line between landmark-cards
// It's a vertical dotted line
// Next to the line is the icon for the mode of transport (walking for now) and the estimated time
// There is also a button to open the navigation instructions as a new intent
// next landmark is not actually required, but it ensures that the widget is deleted when the next landmark is removed (which makes sense, because then there will be another step)
int timeRounded = 5 * (current.tripTime?.inMinutes ?? 0) ~/ 5;
// ~/ is integer division (rounding)
return Container(
margin: EdgeInsets.all(10),
padding: EdgeInsets.all(10),
@@ -138,7 +144,7 @@ Widget stepBetweenLandmarks(Landmark landmark) {
Column(
children: [
Icon(Icons.directions_walk),
Text("${landmark.tripTime} min", style: TextStyle(fontSize: 10)),
Text("~$timeRounded min", style: TextStyle(fontSize: 10)),
],
),
Spacer(),
@@ -146,8 +152,13 @@ Widget stepBetweenLandmarks(Landmark landmark) {
onPressed: () {
// Open navigation instructions
},
child: Text("Navigate"),
),
child: Row(
children: [
Icon(Icons.directions),
Text("Directions"),
],
),
)
],
),
);

View File

@@ -4,7 +4,8 @@ import 'package:flutter/material.dart';
import 'package:anyway/structs/landmark.dart';
import 'package:anyway/structs/trip.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:the_widget_marker/the_widget_marker.dart';
import 'package:widget_to_marker/widget_to_marker.dart';
class MapWidget extends StatefulWidget {
@@ -25,38 +26,42 @@ class _MapWidgetState extends State<MapWidget> {
target: LatLng(48.8566, 2.3522),
zoom: 11.0,
);
Set<Marker> markers = <Marker>{};
final GlobalKey globalKey = GlobalKey();
Set<Marker> mapMarkers = <Marker>{};
void _onMapCreated(GoogleMapController controller) async {
mapController = controller;
List<double>? newLocation = widget.trip?.landmarks.first.location;
List<double>? newLocation = widget.trip?.landmarks.firstOrNull?.location;
if (newLocation != null) {
CameraUpdate update = CameraUpdate.newLatLng(LatLng(newLocation[0], newLocation[1]));
controller.moveCamera(update);
}
drawLandmarks();
// addLandmarkMarker();
}
void _onCameraIdle() {
// print(mapController.getLatLng(ScreenCoordinate(x: 0, y: 0)));
}
void drawLandmarks() async {
// (re)draws landmarks on the map
void addLandmarkMarker() async {
LinkedList<Landmark>? landmarks = widget.trip?.landmarks;
if (landmarks != null){
for (Landmark landmark in landmarks) {
markers.add(Marker(
markerId: MarkerId(landmark.name),
position: LatLng(landmark.location[0], landmark.location[1]),
// infoWindow: InfoWindow(title: landmark.name, snippet: landmark.type.name),
icon: await MarkerIcon.widgetToIcon(globalKey),
));
}
int i = mapMarkers.length;
Landmark? current = landmarks!.elementAtOrNull(i);
if (current != null){
mapMarkers.add(
Marker(
markerId: MarkerId(current.name),
position: LatLng(current.location[0], current.location[1]),
icon: await CustomMarker(
landmark: current,
position: i+1
).toBitmapDescriptor(
logicalSize: const Size(150, 150),
imageSize: const Size(150, 150)
)
)
);
setState(() {});
}
}
@@ -64,39 +69,60 @@ class _MapWidgetState extends State<MapWidget> {
@override
Widget build(BuildContext context) {
return Stack(
children: [
MyMarker(globalKey),
GoogleMap(
onMapCreated: _onMapCreated,
initialCameraPosition: _cameraPosition,
onCameraIdle: _onCameraIdle,
// onLongPress: ,
markers: markers,
cloudMapId: '41c21ac9b81dbfd8',
)
]
return ListenableBuilder(
listenable: widget.trip!,
builder: (context, child) {
addLandmarkMarker();
return GoogleMap(
onMapCreated: _onMapCreated,
initialCameraPosition: _cameraPosition,
onCameraIdle: _onCameraIdle,
// onLongPress: ,
markers: mapMarkers,
cloudMapId: '41c21ac9b81dbfd8',
);
}
);
}
}
class MyMarker extends StatelessWidget {
// declare a global key and get it trough Constructor
class CustomMarker extends StatelessWidget {
final Landmark landmark;
final int position;
MyMarker(this.globalKeyMyWidget);
final GlobalKey globalKeyMyWidget;
CustomMarker({
super.key,
required this.landmark,
required this.position
});
@override
Widget build(BuildContext context) {
// This returns an outlined circle, with an icon corresponding to the landmark type
// As a small dot, the number of the landmark is displayed in the top right
Icon icon;
if (landmark.type == museum) {
icon = Icon(Icons.museum, color: Colors.black, size: 50);
} else if (landmark.type == monument) {
icon = Icon(Icons.church, color: Colors.black, size: 50);
} else if (landmark.type == park) {
icon = Icon(Icons.park, color: Colors.black, size: 50);
} else if (landmark.type == restaurant) {
icon = Icon(Icons.restaurant, color: Colors.black, size: 50);
} else if (landmark.type == shop) {
icon = Icon(Icons.shopping_cart, color: Colors.black, size: 50);
} else {
icon = Icon(Icons.location_on, color: Colors.black, size: 50);
}
return RepaintBoundary(
key: globalKeyMyWidget,
child: Stack(
children: [
Container(
// these are not the final sizes, since the final size is set in the toBitmapDescriptor method
// they are useful nevertheless to ensure the scale of the components are correct
width: 75,
height: 75,
decoration: BoxDecoration(
@@ -108,7 +134,7 @@ class MyMarker extends StatelessWidget {
shape: BoxShape.circle,
border: Border.all(color: Colors.black, width: 5),
),
child: Icon(Icons.location_on, color: Colors.black, size: 50),
child: icon,
),
Positioned(
top: 0,
@@ -119,7 +145,7 @@ class MyMarker extends StatelessWidget {
color: Theme.of(context).primaryColor,
shape: BoxShape.circle,
),
child: Text('1', style: TextStyle(color: Colors.white, fontSize: 20)),
child: Text('$position', style: TextStyle(color: Colors.white, fontSize: 20)),
),
),
],

View File

@@ -25,7 +25,18 @@ class _TripsOverviewState extends State<TripsOverview> {
children = List<Widget>.generate(snapshot.data!.length, (index) {
Trip trip = snapshot.data![index];
return ListTile(
title: Text("Trip to ${trip.cityName}"),
title: FutureBuilder(
future: trip.cityName,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.hasData) {
return Text("Trip to ${snapshot.data}");
} else if (snapshot.hasError) {
return Text("Error: ${snapshot.error}");
} else {
return const Text("Trip to ...");
}
},
),
leading: Icon(Icons.pin_drop),
onTap: () {
Navigator.of(context).push(

View File

@@ -2,6 +2,7 @@ import 'package:anyway/structs/preferences.dart';
import 'package:flutter/material.dart';
bool debugMode = false;
class ProfilePage extends StatefulWidget {
@override
@@ -12,6 +13,27 @@ class _ProfilePageState extends State<ProfilePage> {
Future<UserPreferences> _prefs = loadUserPreferences();
Widget debugButton() {
return Padding(
padding: EdgeInsets.only(top: 20),
child: Row(
children: [
Text('Debug mode'),
Switch(
value: debugMode,
onChanged: (bool? newValue) {
setState(() {
debugMode = newValue!;
});
}
)
],
)
);
}
@override
Widget build(BuildContext context) {
return ListView(
@@ -36,7 +58,8 @@ class _ProfilePageState extends State<ProfilePage> {
),
),
FutureBuilder(future: _prefs, builder: futureSliders)
FutureBuilder(future: _prefs, builder: futureSliders),
debugButton()
]
);
}
@@ -59,7 +82,6 @@ class _ProfilePageState extends State<ProfilePage> {
}
class PreferenceSliders extends StatefulWidget {
final List<SinglePreference> prefs;

View File

@@ -3,6 +3,14 @@ import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
const LandmarkType museum = LandmarkType(name: 'Museum');
const LandmarkType monument = LandmarkType(name: 'Monument');
const LandmarkType park = LandmarkType(name: 'Park');
const LandmarkType restaurant = LandmarkType(name: 'Restaurant');
const LandmarkType shop = LandmarkType(name: 'Shop');
final class Landmark extends LinkedListEntry<Landmark>{
// A linked node of a list of Landmarks
final String uuid;
@@ -55,11 +63,12 @@ final class Landmark extends LinkedListEntry<Landmark>{
final imageURL = json['image_url'] as String?;
final description = json['description'] as String?;
var duration = Duration(minutes: json['duration'] ?? 0) as Duration?;
if (duration == const Duration()) {duration = null;};
// if (duration == const Duration()) {duration = null;};
final visited = json['visited'] as bool?;
var tripTime = Duration(minutes: json['time_to_reach_next'] ?? 0) as Duration?;
return Landmark(
uuid: uuid, name: name, location: locationFixed, type: typeFixed, isSecondary: isSecondary, imageURL: imageURL, description: description, duration: duration, visited: visited);
uuid: uuid, name: name, location: locationFixed, type: typeFixed, isSecondary: isSecondary, imageURL: imageURL, description: description, duration: duration, visited: visited, tripTime: tripTime);
} else {
throw FormatException('Invalid JSON: $json');
}
@@ -81,7 +90,8 @@ final class Landmark extends LinkedListEntry<Landmark>{
'image_url': imageURL,
'description': description,
'duration': duration?.inMinutes,
'visited': visited
'visited': visited,
'trip_time': tripTime?.inMinutes,
};
}

View File

@@ -1,46 +0,0 @@
// import "package:anyway/structs/landmark.dart";
// class Linked<Landmark> {
// Landmark? head;
// Linked();
// // class methods
// bool get isEmpty => head == null;
// // Add a new node to the end of the list
// void add(Landmark value) {
// if (isEmpty) {
// // If the list is empty, set the new node as the head
// head = value;
// } else {
// Landmark? current = head;
// while (current!.next != null) {
// // Traverse the list to find the last node
// current = current.next;
// }
// current.next = value; // Set the new node as the next node of the last node
// }
// }
// // Remove the first node with the given value
// void remove(Landmark value) {
// if (isEmpty) return;
// // If the value is in the head node, update the head to the next node
// if (head! == value) {
// head = head.next;
// return;
// }
// var current = head;
// while (current!.next != null) {
// if (current.next! == value) {
// // If the value is found in the next node, skip the next node
// current.next = current.next.next;
// return;
// }
// current = current.next;
// }
// }
// }

View File

@@ -16,11 +16,15 @@ class Trip with ChangeNotifier {
// could be empty as well
Future<String> get cityName async {
List<double>? location = landmarks.firstOrNull?.location;
if (GeocodingPlatform.instance == null) {
return '${landmarks.first.location[0]}, ${landmarks.first.location[1]}';
return '$location';
} else if (location == null) {
return 'Unknown';
} else{
List<Placemark> placemarks = await placemarkFromCoordinates(location[0], location[1]);
return placemarks.first.locality ?? 'Unknown';
}
List<Placemark> placemarks = await placemarkFromCoordinates(landmarks.first.location[0], landmarks.first.location[1]);
return placemarks.first.locality ?? 'Unknown';
}
@@ -56,6 +60,11 @@ class Trip with ChangeNotifier {
notifyListeners();
}
void removeLandmark(Landmark landmark) {
landmarks.remove(landmark);
notifyListeners();
}
factory Trip.fromPrefs(SharedPreferences prefs, String uuid) {
String? content = prefs.getString('trip_$uuid');
Map<String, dynamic> json = jsonDecode(content!);

View File

@@ -57,6 +57,13 @@ fetchTrip(
// only fill in the trip "meta" data for now
trip.loadFromJson(json);
// now fill the trip with landmarks
// if (trip.landmarks.isNotEmpty) {
// trip.landmarks.clear();
// }
// we are going to recreate all the landmarks from the information given by the api
trip.landmarks.remove(trip.landmarks.first);
String? nextUUID = json["first_landmark_uuid"];
while (nextUUID != null) {
var (landmark, newUUID) = await fetchLandmark(nextUUID);