location picker and ui fixes
All checks were successful
Build and release APK / Build APK (pull_request) Successful in 5m25s

This commit is contained in:
2024-08-09 00:48:45 +02:00
parent bea3a65fec
commit 311b1c2218
18 changed files with 588 additions and 395 deletions

View File

@@ -1,17 +1,19 @@
import 'package:anyway/structs/landmark.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:anyway/structs/landmark.dart';
class LandmarkCard extends StatefulWidget {
final Landmark landmark;
LandmarkCard(this.landmark);
@override
_LandmarkCardState createState() => _LandmarkCardState();
LandmarkCard(this.landmark);
}
class _LandmarkCardState extends State<LandmarkCard> {
@override
Widget build(BuildContext context) {

View File

@@ -0,0 +1,64 @@
import 'dart:developer';
import 'package:anyway/modules/step_between_landmarks.dart';
import 'package:flutter/material.dart';
import 'package:anyway/modules/landmark_card.dart';
import 'package:anyway/structs/landmark.dart';
import 'package:anyway/structs/trip.dart';
import 'package:anyway/main.dart';
List<Widget> landmarksList(Trip trip) {
log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks");
List<Widget> children = [];
log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks");
if (trip.landmarks.isEmpty || trip.landmarks.length <= 1 && trip.landmarks.first.type == start ) {
children.add(
const Text("No landmarks in this trip"),
);
return children;
}
for (Landmark landmark in trip.landmarks) {
children.add(
Dismissible(
key: ValueKey<int>(landmark.hashCode),
child: LandmarkCard(landmark),
dismissThresholds: {DismissDirection.endToStart: 0.95, DismissDirection.startToEnd: 0.95},
onDismissed: (direction) {
log('Removing ${landmark.name}');
trip.removeLandmark(landmark);
// Then show a snackbar
rootScaffoldMessengerKey.currentState!.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) {
children.add(
StepBetweenLandmarks(current: landmark, next: landmark.next!)
);
}
}
return children;
}

View File

@@ -1,168 +0,0 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:anyway/modules/landmark_card.dart';
import 'package:anyway/structs/landmark.dart';
import 'package:anyway/structs/trip.dart';
class LandmarksOverview extends StatefulWidget {
final Trip? trip;
const LandmarksOverview({super.key, this.trip});
@override
State<LandmarksOverview> createState() => _LandmarksOverviewState();
}
class _LandmarksOverviewState extends State<LandmarksOverview> {
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: widget.trip!,
builder: (BuildContext context, Widget? child) {
Trip trip = widget.trip!;
log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks");
List<Widget> children;
if (trip.uuid != 'pending' && trip.uuid != 'error') {
log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks");
if (trip.landmarks.length <= 1) {
children = [
const Text("No landmarks in this trip"),
];
} else {
children = [
landmarksWithSteps(),
saveButton(),
];
}
} else if(trip.uuid == 'pending') {
// the trip is still being fetched from the api
children = [Center(child: CircularProgressIndicator())];
} else {
// trip.uuid == 'error'
// show the error raised by the api
// String error =
children = [
const Icon(
Icons.error_outline,
color: Colors.red,
size: 60,
),
Padding(
padding: const EdgeInsets.only(top: 16),
child: Text('Error: ${trip.errorDescription}'),
),
];
}
return Column(
children: children,
);
},
);
}
Widget saveButton() => ElevatedButton(
onPressed: () async {
Trip? trip = await widget.trip;
SharedPreferences prefs = await SharedPreferences.getInstance();
trip?.toPrefs(prefs);
},
child: const Text('Save'),
);
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
);
},
);
}
}
Widget stepBetweenLandmarks(Landmark current, Landmark next) {
int timeRounded = 5 * (current.tripTime?.inMinutes ?? 0) ~/ 5;
// ~/ is integer division (rounding)
return Container(
margin: EdgeInsets.all(10),
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
border: Border(
left: BorderSide(width: 3.0, color: Colors.black),
),
// gradient: LinearGradient(
// begin: Alignment.topLeft,
// end: Alignment.bottomRight,
// colors: [Colors.grey, Colors.white, Colors.white],
// ),
),
child: Row(
children: [
Column(
children: [
Icon(Icons.directions_walk),
Text("~$timeRounded min", style: TextStyle(fontSize: 10)),
],
),
Spacer(),
ElevatedButton(
onPressed: () {
// Open navigation instructions
},
child: Row(
children: [
Icon(Icons.directions),
Text("Directions"),
],
),
)
],
),
);
}

View File

@@ -1,6 +1,7 @@
import 'dart:collection';
import 'dart:developer';
import 'package:anyway/constants.dart';
import 'package:anyway/modules/themed_marker.dart';
import 'package:flutter/material.dart';
import 'package:anyway/structs/landmark.dart';
import 'package:anyway/structs/trip.dart';
@@ -54,7 +55,7 @@ class _MapWidgetState extends State<MapWidget> {
Marker marker = Marker(
markerId: MarkerId(landmark.uuid),
position: LatLng(location[0], location[1]),
icon: await CustomMarker(landmark: landmark, position: i).toBitmapDescriptor(
icon: await ThemedMarker(landmark: landmark, position: i).toBitmapDescriptor(
logicalSize: const Size(150, 150),
imageSize: const Size(150, 150)
),
@@ -75,77 +76,7 @@ class _MapWidgetState extends State<MapWidget> {
onCameraIdle: _onCameraIdle,
// onLongPress: ,
markers: mapMarkers,
cloudMapId: '41c21ac9b81dbfd8',
cloudMapId: MAP_ID,
);
}
}
class CustomMarker extends StatelessWidget {
final Landmark landmark;
final int position;
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 == sightseeing) {
icon = Icon(Icons.church, color: Colors.black, size: 50);
} else if (landmark.type == nature) {
icon = Icon(Icons.park, color: Colors.black, size: 50);
} else if (landmark.type == shopping) {
icon = Icon(Icons.shopping_cart, color: Colors.black, size: 50);
} else if (landmark.type == start || landmark.type == finish) {
icon = Icon(Icons.flag, color: Colors.black, size: 50);
} else {
icon = Icon(Icons.location_on, color: Colors.black, size: 50);
}
Widget? positionIndicator;
if (landmark.type != start && landmark.type != finish) {
positionIndicator = Positioned(
top: 0,
right: 0,
child: Container(
padding: EdgeInsets.all(5),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
shape: BoxShape.circle,
),
child: Text('$position', style: TextStyle(color: Colors.white, fontSize: 20)),
),
);
}
return RepaintBoundary(
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(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.red, Colors.yellow]
),
shape: BoxShape.circle,
border: Border.all(color: Colors.black, width: 5),
),
child: icon,
),
positionIndicator ?? Container(),
],
),
);
}
}

View File

@@ -0,0 +1,78 @@
import 'package:anyway/layout.dart';
import 'package:anyway/structs/preferences.dart';
import 'package:anyway/structs/trip.dart';
import 'package:anyway/utils/fetch_trip.dart';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
class NewTripButton extends StatefulWidget {
final Trip trip;
const NewTripButton({required this.trip});
@override
State<NewTripButton> createState() => _NewTripButtonState();
}
class _NewTripButtonState extends State<NewTripButton> {
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: widget.trip,
builder: (BuildContext context, Widget? child) {
if (widget.trip.landmarks.isEmpty){
return Container();
}
return SizedBox(
width: 200,
child: ElevatedButton(
onPressed: () async {
Future<UserPreferences> preferences = loadUserPreferences();
Trip trip = widget.trip;
fetchTrip(trip, preferences);
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => BasePage(mainScreen: "map", trip: trip)
)
);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.add,
),
Expanded(
child: Padding(
padding: EdgeInsets.only(left: 10, top: 5, bottom: 5, right: 5),
child: FutureBuilder(
future: widget.trip.cityName,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return AutoSizeText(
'New trip to ${snapshot.data.toString()}',
style: TextStyle(fontSize: 18),
maxLines: 2,
);
} else {
return AutoSizeText(
'New trip to ...',
style: TextStyle(fontSize: 18),
maxLines: 2,
);
}
},
)
)
)
],
),
),
);
}
);
}
}

View File

@@ -0,0 +1,64 @@
// A search bar that allow the user to enter a city name
import 'package:anyway/structs/landmark.dart';
import 'package:geocoding/geocoding.dart';
import 'dart:developer';
import 'package:anyway/structs/trip.dart';
import 'package:flutter/material.dart';
class NewTripLocationSearch extends StatefulWidget {
Trip trip;
NewTripLocationSearch(
this.trip,
);
@override
State<NewTripLocationSearch> createState() => _NewTripLocationSearchState();
}
class _NewTripLocationSearchState extends State<NewTripLocationSearch> {
final TextEditingController _controller = TextEditingController();
setTripLocation (String query) async {
List<Location> locations = [];
log('Searching for: $query');
try{
locations = await locationFromAddress(query);
} catch (e) {
log('No results found for: $query : $e');
}
if (locations.isNotEmpty) {
Location location = locations.first;
widget.trip.landmarks.clear();
widget.trip.addLandmark(
Landmark(
uuid: 'pending',
name: query,
location: [location.latitude, location.longitude],
type: start
)
);
}
}
@override
Widget build(BuildContext context) {
return SearchBar(
hintText: 'Enter a city name or long press on the map.',
onSubmitted: setTripLocation,
controller: _controller,
leading: Icon(Icons.search),
trailing: [ElevatedButton(
onPressed: () {
setTripLocation(_controller.text);
},
child: Text('Search'),
),]
);
}
}

View File

@@ -0,0 +1,86 @@
// A map that allows the user to select a location for a new trip.
import 'dart:developer';
import 'package:anyway/constants.dart';
import 'package:anyway/modules/themed_marker.dart';
import 'package:anyway/structs/landmark.dart';
import 'package:anyway/structs/trip.dart';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:widget_to_marker/widget_to_marker.dart';
class NewTripMap extends StatefulWidget {
Trip trip;
NewTripMap(
this.trip,
);
@override
State<NewTripMap> createState() => _NewTripMapState();
}
class _NewTripMapState extends State<NewTripMap> {
final CameraPosition _cameraPosition = CameraPosition(
target: LatLng(48.8566, 2.3522),
zoom: 11.0,
);
late GoogleMapController _mapController;
final Set<Marker> _markers = <Marker>{};
_onLongPress(LatLng location) {
log('Long press: $location');
widget.trip.landmarks.clear();
widget.trip.addLandmark(
Landmark(
uuid: 'pending',
name: 'start',
location: [location.latitude, location.longitude],
type: start
)
);
}
updateTripDetails() async {
_markers.clear();
if (widget.trip.landmarks.isNotEmpty) {
Landmark landmark = widget.trip.landmarks.first;
_markers.add(
Marker(
markerId: MarkerId(landmark.uuid),
position: LatLng(landmark.location[0], landmark.location[1]),
icon: await ThemedMarker(landmark: landmark, position: 0).toBitmapDescriptor(
logicalSize: const Size(150, 150),
imageSize: const Size(150, 150)
),
)
);
_mapController.moveCamera(
CameraUpdate.newLatLng(
LatLng(landmark.location[0], landmark.location[1])
)
);
setState(() {});
}
}
void _onMapCreated(GoogleMapController controller) async {
_mapController = controller;
}
@override
Widget build(BuildContext context) {
widget.trip.addListener(updateTripDetails);
return GoogleMap(
onMapCreated: _onMapCreated,
initialCameraPosition: _cameraPosition,
onLongPress: _onLongPress,
markers: _markers,
cloudMapId: MAP_ID,
mapToolbarEnabled: false,
);
}
}

View File

@@ -0,0 +1,33 @@
import 'package:anyway/structs/trip.dart';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
Widget saveButton(Trip trip) => ElevatedButton(
onPressed: () async {
SharedPreferences prefs = await SharedPreferences.getInstance();
trip.toPrefs(prefs);
},
child: SizedBox(
width: 100,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.save,
),
Expanded(
child: Padding(
padding: EdgeInsets.only(left: 10, top: 5, bottom: 5, right: 5),
child: AutoSizeText(
'Save trip',
maxLines: 2,
),
),
),
],
),
)
);

View File

@@ -0,0 +1,60 @@
import 'package:anyway/structs/landmark.dart';
import 'package:flutter/material.dart';
class StepBetweenLandmarks extends StatefulWidget {
final Landmark current;
final Landmark next;
const StepBetweenLandmarks({
super.key,
required this.current,
required this.next
});
@override
State<StepBetweenLandmarks> createState() => _StepBetweenLandmarksState();
}
class _StepBetweenLandmarksState extends State<StepBetweenLandmarks> {
@override
Widget build(BuildContext context) {
int timeRounded = 5 * (widget.current.tripTime?.inMinutes ?? 0) ~/ 5;
// ~/ is integer division (rounding)
return Container(
margin: EdgeInsets.all(10),
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
border: Border(
left: BorderSide(width: 3.0, color: Colors.black),
),
// gradient: LinearGradient(
// begin: Alignment.topLeft,
// end: Alignment.bottomRight,
// colors: [Colors.grey, Colors.white, Colors.white],
// ),
),
child: Row(
children: [
Column(
children: [
Icon(Icons.directions_walk),
Text("~$timeRounded min", style: TextStyle(fontSize: 10)),
],
),
Spacer(),
ElevatedButton(
onPressed: () {
// Open navigation instructions
},
child: Row(
children: [
Icon(Icons.directions),
Text("Directions"),
],
),
)
],
),
);
}
}

View File

@@ -0,0 +1,68 @@
import 'package:anyway/structs/landmark.dart';
import 'package:flutter/material.dart';
class ThemedMarker extends StatelessWidget {
final Landmark landmark;
final int position;
ThemedMarker({
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 == sightseeing) {
icon = Icon(Icons.church, color: Colors.black, size: 50);
} else if (landmark.type == nature) {
icon = Icon(Icons.park, color: Colors.black, size: 50);
} else if (landmark.type == shopping) {
icon = Icon(Icons.shopping_cart, color: Colors.black, size: 50);
} else if (landmark.type == start || landmark.type == finish) {
icon = Icon(Icons.flag, color: Colors.black, size: 50);
} else {
icon = Icon(Icons.location_on, color: Colors.black, size: 50);
}
Widget? positionIndicator;
if (landmark.type != start && landmark.type != finish) {
positionIndicator = Positioned(
top: 0,
right: 0,
child: Container(
padding: EdgeInsets.all(5),
decoration: BoxDecoration(
color: Colors.grey[100],
shape: BoxShape.circle,
),
child: Text('$position', style: TextStyle(color: Colors.black, fontSize: 25)),
),
);
}
return RepaintBoundary(
child: Stack(
alignment: Alignment.topRight,
children: [
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.red, Colors.yellow]
),
shape: BoxShape.circle,
border: Border.all(color: Colors.black, width: 5),
),
padding: EdgeInsets.all(5),
child: icon
),
if (positionIndicator != null) positionIndicator,
],
),
);
}
}