location picker and ui fixes
All checks were successful
Build and release APK / Build APK (pull_request) Successful in 5m25s
All checks were successful
Build and release APK / Build APK (pull_request) Successful in 5m25s
This commit is contained in:
parent
bea3a65fec
commit
311b1c2218
@ -2,3 +2,4 @@ const String APP_NAME = 'AnyWay';
|
||||
|
||||
String API_URL_BASE = 'https://anyway.kluster.moll.re';
|
||||
|
||||
const String MAP_ID = '41c21ac9b81dbfd8';
|
||||
|
@ -6,14 +6,17 @@ import 'package:flutter/material.dart';
|
||||
import 'package:anyway/constants.dart';
|
||||
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:anyway/modules/trips_overview.dart';
|
||||
import 'package:anyway/modules/trips_saved_list.dart';
|
||||
import 'package:anyway/utils/load_trips.dart';
|
||||
|
||||
import 'package:anyway/pages/new_trip.dart';
|
||||
import 'package:anyway/pages/tutorial.dart';
|
||||
import 'package:anyway/pages/overview.dart';
|
||||
import 'package:anyway/pages/trip.dart';
|
||||
import 'package:anyway/pages/profile.dart';
|
||||
|
||||
|
||||
|
||||
|
||||
// BasePage is the scaffold that holds all other pages
|
||||
// A side drawer is used to switch between pages
|
||||
class BasePage extends StatefulWidget {
|
||||
@ -39,7 +42,7 @@ class _BasePageState extends State<BasePage> {
|
||||
|
||||
|
||||
if (widget.mainScreen == "map") {
|
||||
currentView = NavigationOverview(trip: widget.trip ?? getFirstTrip(trips));
|
||||
currentView = TripPage(trip: widget.trip ?? getFirstTrip(trips));
|
||||
} else if (widget.mainScreen == "tutorial") {
|
||||
currentView = TutorialPage();
|
||||
} else if (widget.mainScreen == "profile") {
|
||||
|
@ -4,6 +4,8 @@ import 'package:anyway/layout.dart';
|
||||
|
||||
void main() => runApp(const App());
|
||||
|
||||
final GlobalKey<ScaffoldMessengerState> rootScaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||
|
||||
class App extends StatelessWidget {
|
||||
const App({super.key});
|
||||
|
||||
@ -14,6 +16,7 @@ class App extends StatelessWidget {
|
||||
title: APP_NAME,
|
||||
home: BasePage(mainScreen: "map"),
|
||||
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.red[600]),
|
||||
scaffoldMessengerKey: rootScaffoldMessengerKey
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
64
frontend/lib/modules/landmarks_list.dart
Normal file
64
frontend/lib/modules/landmarks_list.dart
Normal 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;
|
||||
}
|
||||
|
@ -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"),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
78
frontend/lib/modules/new_trip_button.dart
Normal file
78
frontend/lib/modules/new_trip_button.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
64
frontend/lib/modules/new_trip_location_search.dart
Normal file
64
frontend/lib/modules/new_trip_location_search.dart
Normal 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'),
|
||||
),]
|
||||
|
||||
);
|
||||
}
|
||||
}
|
86
frontend/lib/modules/new_trip_map.dart
Normal file
86
frontend/lib/modules/new_trip_map.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
33
frontend/lib/modules/save_button.dart
Normal file
33
frontend/lib/modules/save_button.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
|
60
frontend/lib/modules/step_between_landmarks.dart
Normal file
60
frontend/lib/modules/step_between_landmarks.dart
Normal 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"),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
68
frontend/lib/modules/themed_marker.dart
Normal file
68
frontend/lib/modules/themed_marker.dart
Normal 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,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import 'package:anyway/modules/new_trip_button.dart';
|
||||
import 'package:anyway/structs/landmark.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geocoding/geocoding.dart';
|
||||
@ -6,7 +7,8 @@ import 'package:anyway/layout.dart';
|
||||
import 'package:anyway/utils/fetch_trip.dart';
|
||||
import 'package:anyway/structs/preferences.dart';
|
||||
import "package:anyway/structs/trip.dart";
|
||||
|
||||
import 'package:anyway/modules/new_trip_location_search.dart';
|
||||
import 'package:anyway/modules/new_trip_map.dart';
|
||||
|
||||
|
||||
class NewTripPage extends StatefulWidget {
|
||||
@ -20,74 +22,31 @@ class _NewTripPageState extends State<NewTripPage> {
|
||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
||||
final TextEditingController latController = TextEditingController();
|
||||
final TextEditingController lonController = TextEditingController();
|
||||
Trip trip = Trip();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// floating search bar and map as a background
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('New Trip'),
|
||||
),
|
||||
body: Form(
|
||||
key: _formKey,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(15.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
||||
children: <Widget>[
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(hintText: 'Lat'),
|
||||
controller: latController,
|
||||
validator: (String? value) {
|
||||
if (value == null || value.isEmpty || double.tryParse(value) == null){
|
||||
return 'Please enter a floating point number';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(hintText: 'Lon'),
|
||||
controller: lonController,
|
||||
|
||||
validator: (String? value) {
|
||||
if (value == null || value.isEmpty || double.tryParse(value) == null){
|
||||
return 'Please enter a floating point number';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
Divider(height: 15, color: Colors.transparent),
|
||||
ElevatedButton(
|
||||
child: const Text('Create trip'),
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
List<double> startPoint = [
|
||||
double.parse(latController.text),
|
||||
double.parse(lonController.text)
|
||||
];
|
||||
Future<UserPreferences> preferences = loadUserPreferences();
|
||||
Trip trip = Trip();
|
||||
trip.landmarks.add(
|
||||
Landmark(
|
||||
location: startPoint,
|
||||
name: "Start",
|
||||
type: start,
|
||||
uuid: "pending"
|
||||
)
|
||||
);
|
||||
fetchTrip(trip, preferences);
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => BasePage(mainScreen: "map", trip: trip)
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
body: Stack(
|
||||
children: [
|
||||
NewTripMap(trip),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(15),
|
||||
child: NewTripLocationSearch(trip),
|
||||
),
|
||||
)
|
||||
)
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(15),
|
||||
child: NewTripButton(trip: trip)
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,82 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sliding_up_panel/sliding_up_panel.dart';
|
||||
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
|
||||
import 'package:anyway/modules/landmarks_overview.dart';
|
||||
import 'package:anyway/modules/map.dart';
|
||||
import 'package:anyway/modules/greeter.dart';
|
||||
|
||||
|
||||
|
||||
class NavigationOverview extends StatefulWidget {
|
||||
final Trip trip;
|
||||
|
||||
NavigationOverview({
|
||||
required this.trip,
|
||||
});
|
||||
|
||||
@override
|
||||
State<NavigationOverview> createState() => _NavigationOverviewState();
|
||||
}
|
||||
|
||||
|
||||
|
||||
class _NavigationOverviewState extends State<NavigationOverview> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SlidingUpPanel(
|
||||
panel: _floatingPanel(),
|
||||
// collapsed: _floatingCollapsed(),
|
||||
body: MapWidget(trip: widget.trip),
|
||||
// renderPanelSheet: false,
|
||||
// backdropEnabled: true,
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.8,
|
||||
padding: EdgeInsets.all(10),
|
||||
// panelSnapping: false,
|
||||
borderRadius: BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
blurRadius: 20.0,
|
||||
color: Colors.black,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _floatingCollapsed(){
|
||||
return Greeter(
|
||||
trip: widget.trip
|
||||
);
|
||||
}
|
||||
|
||||
Widget _floatingPanel(){
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(15),
|
||||
child:
|
||||
Center(
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.all(Radius.circular(12.0)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
Greeter(trip: widget.trip),
|
||||
LandmarksOverview(trip: widget.trip)
|
||||
]
|
||||
)
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
84
frontend/lib/pages/trip.dart
Normal file
84
frontend/lib/pages/trip.dart
Normal file
@ -0,0 +1,84 @@
|
||||
import 'package:anyway/modules/save_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sliding_up_panel/sliding_up_panel.dart';
|
||||
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:anyway/modules/landmarks_list.dart';
|
||||
import 'package:anyway/modules/greeter.dart';
|
||||
import 'package:anyway/modules/map.dart';
|
||||
|
||||
|
||||
|
||||
class TripPage extends StatefulWidget {
|
||||
final Trip trip;
|
||||
|
||||
TripPage({
|
||||
required this.trip,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TripPage> createState() => _TripPageState();
|
||||
}
|
||||
|
||||
|
||||
|
||||
class _TripPageState extends State<TripPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SlidingUpPanel(
|
||||
panelBuilder: (sc) => _panelFull(sc),
|
||||
// collapsed: _floatingCollapsed(),
|
||||
body: MapWidget(trip: widget.trip),
|
||||
// renderPanelSheet: false,
|
||||
// backdropEnabled: true,
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.8,
|
||||
padding: EdgeInsets.only(left: 10, right: 10, top: 25, bottom: 10),
|
||||
// panelSnapping: false,
|
||||
borderRadius: BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
blurRadius: 20.0,
|
||||
color: Colors.black,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _panelFull(ScrollController sc) {
|
||||
return ListenableBuilder(
|
||||
listenable: widget.trip,
|
||||
builder: (context, child) {
|
||||
if (widget.trip.uuid != 'pending' && widget.trip.uuid != 'error') {
|
||||
return ListView(
|
||||
controller: sc,
|
||||
padding: EdgeInsets.only(bottom: 35),
|
||||
children: [
|
||||
Greeter(trip: widget.trip),
|
||||
...landmarksList(widget.trip),
|
||||
Padding(padding: EdgeInsets.only(top: 10)),
|
||||
Center(child: saveButton(widget.trip)),
|
||||
],
|
||||
);
|
||||
} else if(widget.trip.uuid == 'pending') {
|
||||
return Greeter(trip: widget.trip);
|
||||
} else {
|
||||
return Column(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
color: Colors.red,
|
||||
size: 60,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: Text('Error: ${widget.trip.errorDescription}'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:anyway/structs/landmark.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@ -75,8 +76,11 @@ class Trip with ChangeNotifier {
|
||||
String? content = prefs.getString('trip_$uuid');
|
||||
Map<String, dynamic> json = jsonDecode(content!);
|
||||
Trip trip = Trip.fromJson(json);
|
||||
String? firstUUID = json['entry_uuid'];
|
||||
readLandmarks(trip.landmarks, prefs, firstUUID);
|
||||
String? firstUUID = json['first_landmark_uuid'];
|
||||
log('Loading trip $uuid with first landmark $firstUUID');
|
||||
LinkedList<Landmark> landmarks = readLandmarks(prefs, firstUUID);
|
||||
trip.landmarks = landmarks;
|
||||
// notifyListeners();
|
||||
return trip;
|
||||
}
|
||||
|
||||
@ -90,6 +94,7 @@ class Trip with ChangeNotifier {
|
||||
|
||||
void toPrefs(SharedPreferences prefs){
|
||||
Map<String, dynamic> json = toJson();
|
||||
log('Saving trip $uuid : $json');
|
||||
prefs.setString('trip_$uuid', jsonEncode(json));
|
||||
for (Landmark landmark in landmarks) {
|
||||
landmarkToPrefs(prefs, landmark, landmark.next);
|
||||
@ -99,12 +104,14 @@ class Trip with ChangeNotifier {
|
||||
|
||||
|
||||
// Helper
|
||||
readLandmarks(LinkedList<Landmark> landmarks, SharedPreferences prefs, String? firstUUID) {
|
||||
LinkedList<Landmark> readLandmarks(SharedPreferences prefs, String? firstUUID) {
|
||||
LinkedList<Landmark> landmarks = LinkedList<Landmark>();
|
||||
while (firstUUID != null) {
|
||||
var (head, nextUUID) = getLandmarkFromPrefs(prefs, firstUUID);
|
||||
landmarks.add(head);
|
||||
firstUUID = nextUUID;
|
||||
}
|
||||
return landmarks;
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user