Usability and styling #24
| @@ -1,6 +1,9 @@ | |||||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||||||
|     <!-- Required to fetch data from the internet. --> |     <!-- Required to fetch data from the internet. --> | ||||||
|     <uses-permission android:name="android.permission.INTERNET"/> |     <uses-permission android:name="android.permission.INTERNET"/> | ||||||
|  |     <!-- Required to show user location --> | ||||||
|  |     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> | ||||||
|  |     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> | ||||||
|  |  | ||||||
|     <application |     <application | ||||||
|         android:label="anyway" |         android:label="anyway" | ||||||
|   | |||||||
							
								
								
									
										90
									
								
								frontend/android/gradlew.bat
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										90
									
								
								frontend/android/gradlew.bat
									
									
									
									
										vendored
									
									
										Executable file
									
								
							| @@ -0,0 +1,90 @@ | |||||||
|  | @if "%DEBUG%" == "" @echo off | ||||||
|  | @rem ########################################################################## | ||||||
|  | @rem | ||||||
|  | @rem  Gradle startup script for Windows | ||||||
|  | @rem | ||||||
|  | @rem ########################################################################## | ||||||
|  |  | ||||||
|  | @rem Set local scope for the variables with windows NT shell | ||||||
|  | if "%OS%"=="Windows_NT" setlocal | ||||||
|  |  | ||||||
|  | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. | ||||||
|  | set DEFAULT_JVM_OPTS= | ||||||
|  |  | ||||||
|  | set DIRNAME=%~dp0 | ||||||
|  | if "%DIRNAME%" == "" set DIRNAME=. | ||||||
|  | set APP_BASE_NAME=%~n0 | ||||||
|  | set APP_HOME=%DIRNAME% | ||||||
|  |  | ||||||
|  | @rem Find java.exe | ||||||
|  | if defined JAVA_HOME goto findJavaFromJavaHome | ||||||
|  |  | ||||||
|  | set JAVA_EXE=java.exe | ||||||
|  | %JAVA_EXE% -version >NUL 2>&1 | ||||||
|  | if "%ERRORLEVEL%" == "0" goto init | ||||||
|  |  | ||||||
|  | echo. | ||||||
|  | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. | ||||||
|  | echo. | ||||||
|  | echo Please set the JAVA_HOME variable in your environment to match the | ||||||
|  | echo location of your Java installation. | ||||||
|  |  | ||||||
|  | goto fail | ||||||
|  |  | ||||||
|  | :findJavaFromJavaHome | ||||||
|  | set JAVA_HOME=%JAVA_HOME:"=% | ||||||
|  | set JAVA_EXE=%JAVA_HOME%/bin/java.exe | ||||||
|  |  | ||||||
|  | if exist "%JAVA_EXE%" goto init | ||||||
|  |  | ||||||
|  | echo. | ||||||
|  | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% | ||||||
|  | echo. | ||||||
|  | echo Please set the JAVA_HOME variable in your environment to match the | ||||||
|  | echo location of your Java installation. | ||||||
|  |  | ||||||
|  | goto fail | ||||||
|  |  | ||||||
|  | :init | ||||||
|  | @rem Get command-line arguments, handling Windowz variants | ||||||
|  |  | ||||||
|  | if not "%OS%" == "Windows_NT" goto win9xME_args | ||||||
|  | if "%@eval[2+2]" == "4" goto 4NT_args | ||||||
|  |  | ||||||
|  | :win9xME_args | ||||||
|  | @rem Slurp the command line arguments. | ||||||
|  | set CMD_LINE_ARGS= | ||||||
|  | set _SKIP=2 | ||||||
|  |  | ||||||
|  | :win9xME_args_slurp | ||||||
|  | if "x%~1" == "x" goto execute | ||||||
|  |  | ||||||
|  | set CMD_LINE_ARGS=%* | ||||||
|  | goto execute | ||||||
|  |  | ||||||
|  | :4NT_args | ||||||
|  | @rem Get arguments from the 4NT Shell from JP Software | ||||||
|  | set CMD_LINE_ARGS=%$ | ||||||
|  |  | ||||||
|  | :execute | ||||||
|  | @rem Setup the command line | ||||||
|  |  | ||||||
|  | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar | ||||||
|  |  | ||||||
|  | @rem Execute Gradle | ||||||
|  | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% | ||||||
|  |  | ||||||
|  | :end | ||||||
|  | @rem End local scope for the variables with windows NT shell | ||||||
|  | if "%ERRORLEVEL%"=="0" goto mainEnd | ||||||
|  |  | ||||||
|  | :fail | ||||||
|  | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of | ||||||
|  | rem the _cmd.exe /c_ return code! | ||||||
|  | if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 | ||||||
|  | exit /b 1 | ||||||
|  |  | ||||||
|  | :mainEnd | ||||||
|  | if "%OS%"=="Windows_NT" endlocal | ||||||
|  |  | ||||||
|  | :omega | ||||||
| @@ -20,7 +20,7 @@ pluginManagement { | |||||||
| plugins { | plugins { | ||||||
|     id "dev.flutter.flutter-plugin-loader" version "1.0.0" |     id "dev.flutter.flutter-plugin-loader" version "1.0.0" | ||||||
|     id "com.android.application" version "7.3.0" apply false |     id "com.android.application" version "7.3.0" apply false | ||||||
|     id "org.jetbrains.kotlin.android" version "1.7.10" apply false |     id "org.jetbrains.kotlin.android" version "2.0.20" apply false | ||||||
| } | } | ||||||
|  |  | ||||||
| include ":app" | include ":app" | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								frontend/devtools_options.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/devtools_options.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | description: This file stores settings for Dart & Flutter DevTools. | ||||||
|  | documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states | ||||||
|  | extensions: | ||||||
| @@ -1,6 +1,85 @@ | |||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  |  | ||||||
| const String APP_NAME = 'AnyWay'; | const String APP_NAME = 'AnyWay'; | ||||||
|  |  | ||||||
| String API_URL_BASE = 'https://anyway.anydev.info'; | String API_URL_BASE = 'https://anyway.anydev.info'; | ||||||
| String PRIVACY_URL = 'https://anydev.info/privacy'; | String PRIVACY_URL = 'https://anydev.info/privacy'; | ||||||
|  |  | ||||||
| const String MAP_ID = '41c21ac9b81dbfd8'; | const String MAP_ID = '41c21ac9b81dbfd8'; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | const Color GRADIENT_START = Color(0xFFF9B208); | ||||||
|  | const Color GRADIENT_END = Color(0xFFE72E77); | ||||||
|  |  | ||||||
|  | const Color PRIMARY_COLOR = Color(0xFFF38F1A); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | const double TRIP_PANEL_MAX_HEIGHT = 0.8; | ||||||
|  | const double TRIP_PANEL_MIN_HEIGHT = 0.12; | ||||||
|  |  | ||||||
|  | ThemeData APP_THEME = ThemeData( | ||||||
|  |   primaryColor: PRIMARY_COLOR, | ||||||
|  |  | ||||||
|  |   scaffoldBackgroundColor: Colors.white, | ||||||
|  |   cardColor: Colors.white, | ||||||
|  |   useMaterial3: true, | ||||||
|  |  | ||||||
|  |   colorScheme: ColorScheme.light( | ||||||
|  |     primary: PRIMARY_COLOR, | ||||||
|  |     secondary: GRADIENT_END, | ||||||
|  |     surface: Colors.white, | ||||||
|  |     error: Colors.red, | ||||||
|  |     onPrimary: Colors.white, | ||||||
|  |     onSecondary: const Color.fromARGB(255, 30, 22, 22), | ||||||
|  |     onSurface: Colors.black, | ||||||
|  |     onError: Colors.white, | ||||||
|  |     brightness: Brightness.light, | ||||||
|  |   ), | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   textButtonTheme: const TextButtonThemeData( | ||||||
|  |     style: ButtonStyle( | ||||||
|  |       foregroundColor: WidgetStatePropertyAll(PRIMARY_COLOR), | ||||||
|  |       side: WidgetStatePropertyAll( | ||||||
|  |         BorderSide( | ||||||
|  |           color: PRIMARY_COLOR, | ||||||
|  |           width: 1, | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |     ) | ||||||
|  |     ), | ||||||
|  |  | ||||||
|  |   elevatedButtonTheme: const ElevatedButtonThemeData( | ||||||
|  |     style: ButtonStyle( | ||||||
|  |       foregroundColor: WidgetStatePropertyAll(PRIMARY_COLOR), | ||||||
|  |     ) | ||||||
|  |   ), | ||||||
|  |  | ||||||
|  |   outlinedButtonTheme: const OutlinedButtonThemeData( | ||||||
|  |     style: ButtonStyle( | ||||||
|  |       foregroundColor: WidgetStatePropertyAll(PRIMARY_COLOR), | ||||||
|  |     ) | ||||||
|  |   ), | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   cardTheme: const CardTheme( | ||||||
|  |     shadowColor: Colors.grey, | ||||||
|  |     elevation: 2, | ||||||
|  |     margin: EdgeInsets.all(10), | ||||||
|  |   ), | ||||||
|  |  | ||||||
|  |   sliderTheme: const SliderThemeData( | ||||||
|  |     trackHeight: 15, | ||||||
|  |     inactiveTrackColor: Colors.grey, | ||||||
|  |     thumbColor: PRIMARY_COLOR, | ||||||
|  |     activeTrackColor: GRADIENT_END | ||||||
|  |   ) | ||||||
|  | ); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | const Gradient APP_GRADIENT = LinearGradient( | ||||||
|  |   begin: Alignment.topLeft, | ||||||
|  |   end: Alignment.bottomRight, | ||||||
|  |   colors: [GRADIENT_START, GRADIENT_END], | ||||||
|  | ); | ||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | import 'package:anyway/pages/settings.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  |  | ||||||
| import 'package:anyway/constants.dart'; | import 'package:anyway/constants.dart'; | ||||||
| @@ -6,10 +7,9 @@ import 'package:anyway/structs/trip.dart'; | |||||||
| import 'package:anyway/modules/trips_saved_list.dart'; | import 'package:anyway/modules/trips_saved_list.dart'; | ||||||
| import 'package:anyway/utils/load_trips.dart'; | import 'package:anyway/utils/load_trips.dart'; | ||||||
|  |  | ||||||
| import 'package:anyway/pages/new_trip.dart'; | import 'package:anyway/pages/new_trip_location.dart'; | ||||||
| import 'package:anyway/pages/current_trip.dart'; | import 'package:anyway/pages/current_trip.dart'; | ||||||
| import 'package:anyway/pages/onboarding.dart'; | import 'package:anyway/pages/onboarding.dart'; | ||||||
| import 'package:anyway/pages/profile.dart'; |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -62,7 +62,7 @@ class _BasePageState extends State<BasePage> { | |||||||
|                         ) |                         ) | ||||||
|                       ); |                       ); | ||||||
|                     }, |                     }, | ||||||
|                     label: Text("Plan a trip now"), |                     label: Text("Plan a trip"), | ||||||
|                   ), |                   ), | ||||||
|                 ); |                 ); | ||||||
|               } |               } | ||||||
| @@ -74,33 +74,33 @@ class _BasePageState extends State<BasePage> { | |||||||
|       } |       } | ||||||
|     } else if (widget.mainScreen == "tutorial") { |     } else if (widget.mainScreen == "tutorial") { | ||||||
|       currentView = OnboardingPage(); |       currentView = OnboardingPage(); | ||||||
|     } else if (widget.mainScreen == "profile") { |     } else if (widget.mainScreen == "settings") { | ||||||
|       currentView = ProfilePage(); |       currentView = SettingsPage(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     final ThemeData theme = Theme.of(context); |  | ||||||
|      |  | ||||||
|     return Scaffold( |     return Scaffold( | ||||||
|       appBar: AppBar(title: Text(APP_NAME)), |       appBar: AppBar(title: Text(APP_NAME)), | ||||||
|       body: Center(child: currentView), |       body: Center(child: currentView), | ||||||
|       drawer: Drawer( |       drawer: Drawer( | ||||||
|         child: Column( |         child: Column( | ||||||
|           children: [ |           children: [ | ||||||
|             DrawerHeader( |             Container( | ||||||
|               decoration: BoxDecoration( |               decoration: BoxDecoration( | ||||||
|                 gradient: LinearGradient(colors: [Colors.red, Colors.yellow]) |                 gradient: APP_GRADIENT, | ||||||
|               ), |               ), | ||||||
|  |               height: 150, | ||||||
|               child: Center( |               child: Center( | ||||||
|                 child: Text( |                 child: Text( | ||||||
|                   APP_NAME, |                   APP_NAME, | ||||||
|                   style: TextStyle( |                   style: TextStyle( | ||||||
|                     color: Colors.grey[800], |                     color: Colors.white, | ||||||
|                     fontSize: 24, |                     fontSize: 24, | ||||||
|                     fontWeight: FontWeight.bold, |                     fontWeight: FontWeight.bold, | ||||||
|                   ), |                   ), | ||||||
|                 ), |                 ), | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
|  |  | ||||||
|             ListTile( |             ListTile( | ||||||
|               title: const Text('Your Trips'), |               title: const Text('Your Trips'), | ||||||
|               leading: const Icon(Icons.map), |               leading: const Icon(Icons.map), | ||||||
| @@ -130,7 +130,7 @@ class _BasePageState extends State<BasePage> { | |||||||
|               }, |               }, | ||||||
|               child: const Text('Clear trips'), |               child: const Text('Clear trips'), | ||||||
|             ), |             ), | ||||||
|             const Divider(), |             const Divider(indent: 10, endIndent: 10), | ||||||
|             ListTile( |             ListTile( | ||||||
|               title: const Text('How to use'), |               title: const Text('How to use'), | ||||||
|               leading: Icon(Icons.help), |               leading: Icon(Icons.help), | ||||||
| @@ -148,11 +148,11 @@ class _BasePageState extends State<BasePage> { | |||||||
|             ListTile( |             ListTile( | ||||||
|               title: const Text('Settings'), |               title: const Text('Settings'), | ||||||
|               leading: const Icon(Icons.settings), |               leading: const Icon(Icons.settings), | ||||||
|               selected: widget.mainScreen == "profile", |               selected: widget.mainScreen == "settings", | ||||||
|               onTap: () { |               onTap: () { | ||||||
|                 Navigator.of(context).push( |                 Navigator.of(context).push( | ||||||
|                   MaterialPageRoute( |                   MaterialPageRoute( | ||||||
|                     builder: (context) => BasePage(mainScreen: "profile") |                     builder: (context) => BasePage(mainScreen: "settings") | ||||||
|                   ) |                   ) | ||||||
|                 ); |                 ); | ||||||
|               }, |               }, | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ class App extends StatelessWidget { | |||||||
|     return MaterialApp( |     return MaterialApp( | ||||||
|       title: APP_NAME, |       title: APP_NAME, | ||||||
|       home: BasePage(mainScreen: "map"), |       home: BasePage(mainScreen: "map"), | ||||||
|       theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.red[600]), |       theme: APP_THEME, | ||||||
|       scaffoldMessengerKey: rootScaffoldMessengerKey |       scaffoldMessengerKey: rootScaffoldMessengerKey | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| import 'dart:developer'; | import 'dart:developer'; | ||||||
|  |  | ||||||
|  | import 'package:anyway/constants.dart'; | ||||||
| import 'package:anyway/structs/trip.dart'; | import 'package:anyway/structs/trip.dart'; | ||||||
| import 'package:auto_size_text/auto_size_text.dart'; | import 'package:auto_size_text/auto_size_text.dart'; | ||||||
|  |  | ||||||
| @@ -20,8 +21,12 @@ class Greeter extends StatefulWidget { | |||||||
| class _GreeterState extends State<Greeter> { | class _GreeterState extends State<Greeter> { | ||||||
|    |    | ||||||
|   Widget greeterBuilder (BuildContext context, Widget? child) { |   Widget greeterBuilder (BuildContext context, Widget? child) { | ||||||
|     ThemeData theme = Theme.of(context); |     final Shader textGradient = APP_GRADIENT.createShader(Rect.fromLTWH(0.0, 0.0, 200.0, 70.0)); | ||||||
|     TextStyle greeterStyle = TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24); |     TextStyle greeterStyle = TextStyle( | ||||||
|  |       foreground: Paint()..shader = textGradient, | ||||||
|  |       fontWeight: FontWeight.bold, | ||||||
|  |       fontSize: 26 | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     Widget topGreeter; |     Widget topGreeter; | ||||||
|  |  | ||||||
| @@ -91,26 +96,10 @@ class _GreeterState extends State<Greeter> { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     return Center( |     return Center( | ||||||
|       child: Column( |       child: topGreeter, | ||||||
|         children: [ |  | ||||||
|           // Padding(padding: EdgeInsets.only(top: 20)), |  | ||||||
|           topGreeter, |  | ||||||
|           Padding( |  | ||||||
|             padding: EdgeInsets.all(20), |  | ||||||
|             child: bottomGreeter |  | ||||||
|           ), |  | ||||||
|         ], |  | ||||||
|       ) |  | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Widget bottomGreeter = const Text( |  | ||||||
|     "Busy day ahead? Here is how to make the most of it!", |  | ||||||
|     style: TextStyle(color: Colors.black, fontSize: 18), |  | ||||||
|     textAlign: TextAlign.center, |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ List<Widget> landmarksList(Trip trip) { | |||||||
|  |  | ||||||
|   log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks"); |   log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks"); | ||||||
|  |  | ||||||
|   if (trip.landmarks.isEmpty || trip.landmarks.length <= 1 && trip.landmarks.first.type == start ) { |   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"), | ||||||
|     ); |     ); | ||||||
| @@ -32,7 +32,6 @@ List<Widget> landmarksList(Trip trip) { | |||||||
|         onDismissed: (direction) { |         onDismissed: (direction) { | ||||||
|           log('Removing ${landmark.name}'); |           log('Removing ${landmark.name}'); | ||||||
|           trip.removeLandmark(landmark); |           trip.removeLandmark(landmark); | ||||||
|           // Then show a snackbar |  | ||||||
|  |  | ||||||
|           rootScaffoldMessengerKey.currentState!.showSnackBar( |           rootScaffoldMessengerKey.currentState!.showSnackBar( | ||||||
|             SnackBar(content: Text("We won't show ${landmark.name} again")) |             SnackBar(content: Text("We won't show ${landmark.name} again")) | ||||||
|   | |||||||
| @@ -1,27 +1,28 @@ | |||||||
| import 'dart:collection'; | import 'dart:collection'; | ||||||
|  |  | ||||||
| import 'package:anyway/constants.dart'; | import 'package:anyway/constants.dart'; | ||||||
| import 'package:anyway/modules/themed_marker.dart'; | import 'package:anyway/modules/landmark_map_marker.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:anyway/structs/landmark.dart'; | import 'package:anyway/structs/landmark.dart'; | ||||||
| import 'package:anyway/structs/trip.dart'; | import 'package:anyway/structs/trip.dart'; | ||||||
| import 'package:google_maps_flutter/google_maps_flutter.dart'; | 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'; | import 'package:widget_to_marker/widget_to_marker.dart'; | ||||||
|  |  | ||||||
|  |  | ||||||
| class MapWidget extends StatefulWidget { | class CurrentTripMap extends StatefulWidget { | ||||||
|  |  | ||||||
|   final Trip? trip; |   final Trip? trip; | ||||||
|  |  | ||||||
|   MapWidget({ |   CurrentTripMap({ | ||||||
|     this.trip |     this.trip | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   State<MapWidget> createState() => _MapWidgetState(); |   State<CurrentTripMap> createState() => _CurrentTripMapState(); | ||||||
| } | } | ||||||
|  |  | ||||||
| class _MapWidgetState extends State<MapWidget> { | class _CurrentTripMapState extends State<CurrentTripMap> { | ||||||
|   late GoogleMapController mapController; |   late GoogleMapController mapController; | ||||||
|  |  | ||||||
|   CameraPosition _cameraPosition = CameraPosition( |   CameraPosition _cameraPosition = CameraPosition( | ||||||
| @@ -67,9 +68,27 @@ class _MapWidgetState extends State<MapWidget> { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     widget.trip?.addListener(setMapMarkers); |     widget.trip?.addListener(setMapMarkers); | ||||||
|  |     Future<SharedPreferences> preferences = SharedPreferences.getInstance(); | ||||||
|  |  | ||||||
|  |     return FutureBuilder( | ||||||
|  |       future: preferences, | ||||||
|  |       builder: (context, snapshot) { | ||||||
|  |         if (snapshot.hasData) { | ||||||
|  |           SharedPreferences prefs = snapshot.data as SharedPreferences; | ||||||
|  |           bool useLocation = prefs.getBool('useLocation') ?? true; | ||||||
|  |           return _buildMap(useLocation); | ||||||
|  |         } else { | ||||||
|  |           return const CircularProgressIndicator(); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget _buildMap(bool useLocation) { | ||||||
|     return GoogleMap( |     return GoogleMap( | ||||||
|       onMapCreated: _onMapCreated, |       onMapCreated: _onMapCreated, | ||||||
|       initialCameraPosition: _cameraPosition, |       initialCameraPosition: _cameraPosition, | ||||||
| @@ -79,7 +98,9 @@ class _MapWidgetState extends State<MapWidget> { | |||||||
|       cloudMapId: MAP_ID, |       cloudMapId: MAP_ID, | ||||||
|       mapToolbarEnabled: false, |       mapToolbarEnabled: false, | ||||||
|       zoomControlsEnabled: false, |       zoomControlsEnabled: false, | ||||||
|  |       myLocationEnabled: useLocation, | ||||||
|  |       myLocationButtonEnabled: false, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										82
									
								
								frontend/lib/modules/current_trip_panel.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								frontend/lib/modules/current_trip_panel.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | |||||||
|  | import 'package:anyway/constants.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  |  | ||||||
|  | import 'package:anyway/structs/trip.dart'; | ||||||
|  | import 'package:anyway/modules/current_trip_summary.dart'; | ||||||
|  | import 'package:anyway/modules/current_trip_save_button.dart'; | ||||||
|  | import 'package:anyway/modules/current_trip_landmarks_list.dart'; | ||||||
|  | import 'package:anyway/modules/current_trip_greeter.dart'; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CurrentTripPanel extends StatefulWidget { | ||||||
|  |   final ScrollController controller; | ||||||
|  |   final Trip trip; | ||||||
|  |  | ||||||
|  |   const CurrentTripPanel({ | ||||||
|  |     super.key, | ||||||
|  |     required this.controller, | ||||||
|  |     required this.trip, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   State<CurrentTripPanel> createState() => _CurrentTripPanelState(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _CurrentTripPanelState extends State<CurrentTripPanel> { | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return ListenableBuilder( | ||||||
|  |       listenable: widget.trip, | ||||||
|  |       builder: (context, child) { | ||||||
|  |         if (widget.trip.uuid != 'pending' && widget.trip.uuid != 'error') { | ||||||
|  |           return ListView( | ||||||
|  |             controller: widget.controller, | ||||||
|  |             padding: const EdgeInsets.only(bottom: 30, left: 5, right: 5), | ||||||
|  |             children: [ | ||||||
|  |               SizedBox( | ||||||
|  |                 // reuse the exact same height as the panel has when collapsed | ||||||
|  |                 // this way the greeter will be centered when the panel is collapsed | ||||||
|  |                 height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT - 20, | ||||||
|  |                 child: Greeter(trip: widget.trip), | ||||||
|  |               ), | ||||||
|  |  | ||||||
|  |               const Padding(padding: EdgeInsets.only(top: 10)), | ||||||
|  |  | ||||||
|  |               // CurrentTripSummary(trip: widget.trip), | ||||||
|  |  | ||||||
|  |               // const Divider(), | ||||||
|  |  | ||||||
|  |               ...landmarksList(widget.trip), | ||||||
|  |  | ||||||
|  |               const Padding(padding: EdgeInsets.only(top: 10)), | ||||||
|  |  | ||||||
|  |               Center(child: saveButton(widget.trip)), | ||||||
|  |             ], | ||||||
|  |           ); | ||||||
|  |         } else if(widget.trip.uuid == 'pending') { | ||||||
|  |           return SizedBox( | ||||||
|  |             // reuse the exact same height as the panel has when collapsed | ||||||
|  |             // this way the greeter will be centered when the panel is collapsed | ||||||
|  |             height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT - 20, | ||||||
|  |             child: Greeter(trip: widget.trip) | ||||||
|  |           ); | ||||||
|  |         } else { | ||||||
|  |           return Row( | ||||||
|  |             mainAxisAlignment: MainAxisAlignment.center, | ||||||
|  |             children: [ | ||||||
|  |               const Icon( | ||||||
|  |                 Icons.error_outline, | ||||||
|  |                 color: Colors.red, | ||||||
|  |                 size: 50, | ||||||
|  |               ), | ||||||
|  |               Padding( | ||||||
|  |                 padding: const EdgeInsets.only(left: 10), | ||||||
|  |                 child: Text('Error: ${widget.trip.errorDescription}'), | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | }   | ||||||
| @@ -1,4 +1,5 @@ | |||||||
|  |  | ||||||
|  | import 'package:anyway/main.dart'; | ||||||
| import 'package:anyway/structs/trip.dart'; | import 'package:anyway/structs/trip.dart'; | ||||||
| import 'package:auto_size_text/auto_size_text.dart'; | import 'package:auto_size_text/auto_size_text.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| @@ -8,6 +9,13 @@ Widget saveButton(Trip trip) => ElevatedButton( | |||||||
|   onPressed: () async { |   onPressed: () async { | ||||||
|     SharedPreferences prefs = await SharedPreferences.getInstance(); |     SharedPreferences prefs = await SharedPreferences.getInstance(); | ||||||
|     trip.toPrefs(prefs); |     trip.toPrefs(prefs); | ||||||
|  |     rootScaffoldMessengerKey.currentState!.showSnackBar( | ||||||
|  |       SnackBar( | ||||||
|  |         content: Text('Trip saved'), | ||||||
|  |         duration: Duration(seconds: 2), | ||||||
|  |         dismissDirection: DismissDirection.horizontal | ||||||
|  |       ) | ||||||
|  |     ); | ||||||
|   }, |   }, | ||||||
|   child: SizedBox( |   child: SizedBox( | ||||||
|     width: 100, |     width: 100, | ||||||
|   | |||||||
							
								
								
									
										31
									
								
								frontend/lib/modules/current_trip_summary.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								frontend/lib/modules/current_trip_summary.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | import 'package:anyway/structs/trip.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  |  | ||||||
|  | class CurrentTripSummary extends StatefulWidget { | ||||||
|  |   final Trip trip; | ||||||
|  |   const CurrentTripSummary({ | ||||||
|  |     super.key, | ||||||
|  |     required this.trip, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   State<CurrentTripSummary> createState() => _CurrentTripSummaryState(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _CurrentTripSummaryState extends State<CurrentTripSummary> { | ||||||
|  |   @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}'), | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |      | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -18,10 +18,6 @@ class _LandmarkCardState extends State<LandmarkCard> { | |||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     ThemeData theme = Theme.of(context); |     ThemeData theme = Theme.of(context); | ||||||
|     ButtonStyle buttonStyle = TextButton.styleFrom( |  | ||||||
|       backgroundColor: Colors.orange, |  | ||||||
|       fixedSize: Size.fromHeight(20) |  | ||||||
|     ); |  | ||||||
|     return Container( |     return Container( | ||||||
|       height: 160, |       height: 160, | ||||||
|       child: Card( |       child: Card( | ||||||
| @@ -40,7 +36,7 @@ class _LandmarkCardState extends State<LandmarkCard> { | |||||||
|               width: 160, |               width: 160, | ||||||
|               child: CachedNetworkImage( |               child: CachedNetworkImage( | ||||||
|                 imageUrl: widget.landmark.imageURL ?? '', |                 imageUrl: widget.landmark.imageURL ?? '', | ||||||
|                 placeholder: (context, url) => 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), | ||||||
|                 // TODO: make this a switch statement to load a placeholder if null |                 // TODO: make this a switch statement to load a placeholder if null | ||||||
|                 // cover the whole container meaning the image will be cropped |                 // cover the whole container meaning the image will be cropped | ||||||
| @@ -88,21 +84,18 @@ class _LandmarkCardState extends State<LandmarkCard> { | |||||||
|                         // 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( |                           TextButton.icon( | ||||||
|                             style: buttonStyle, |  | ||||||
|                             onPressed: () {}, |                             onPressed: () {}, | ||||||
|                             icon: widget.landmark.type.icon, |                             icon: widget.landmark.type.icon, | ||||||
|                             label: Text(widget.landmark.type.name), |                             label: Text(widget.landmark.type.name), | ||||||
|                           ), |                           ), | ||||||
|                           if (widget.landmark.duration != null && widget.landmark.duration!.inMinutes > 0) |                           if (widget.landmark.duration != null && widget.landmark.duration!.inMinutes > 0) | ||||||
|                             TextButton.icon( |                             TextButton.icon( | ||||||
|                               style: buttonStyle, |  | ||||||
|                               onPressed: () {}, |                               onPressed: () {}, | ||||||
|                               icon: Icon(Icons.hourglass_bottom), |                               icon: Icon(Icons.hourglass_bottom), | ||||||
|                               label: Text('${widget.landmark.duration!.inMinutes} minutes'), |                               label: Text('${widget.landmark.duration!.inMinutes} minutes'), | ||||||
|                             ), |                             ), | ||||||
|                           if (widget.landmark.websiteURL != null) |                           if (widget.landmark.websiteURL != null) | ||||||
|                             TextButton.icon( |                             TextButton.icon( | ||||||
|                               style: buttonStyle, |  | ||||||
|                               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!)); | ||||||
| @@ -112,7 +105,6 @@ class _LandmarkCardState extends State<LandmarkCard> { | |||||||
|                             ), |                             ), | ||||||
|                           if (widget.landmark.wikipediaURL != null) |                           if (widget.landmark.wikipediaURL != null) | ||||||
|                             TextButton.icon( |                             TextButton.icon( | ||||||
|                               style: buttonStyle, |  | ||||||
|                               onPressed: () async { |                               onPressed: () async { | ||||||
|                                 // open a browser with the wikipedia link |                                 // open a browser with the wikipedia link | ||||||
|                                 await launchUrl(Uri.parse(widget.landmark.wikipediaURL!)); |                                 await launchUrl(Uri.parse(widget.landmark.wikipediaURL!)); | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | import 'package:anyway/constants.dart'; | ||||||
| import 'package:anyway/structs/landmark.dart'; | import 'package:anyway/structs/landmark.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| 
 | 
 | ||||||
| @@ -16,21 +17,9 @@ class ThemedMarker extends StatelessWidget { | |||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     // This returns an outlined circle, with an icon corresponding to the landmark type |     // 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 |     // 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; |     Widget? positionIndicator; | ||||||
|     if (landmark.type != start && landmark.type != finish) { |     if (landmark.type != typeStart && landmark.type != typeFinish) { | ||||||
|       positionIndicator = Positioned( |       positionIndicator = Positioned( | ||||||
|         top: 0, |         top: 0, | ||||||
|         right: 0, |         right: 0, | ||||||
| @@ -51,14 +40,14 @@ class ThemedMarker extends StatelessWidget { | |||||||
|         children: [ |         children: [ | ||||||
|           Container( |           Container( | ||||||
|             decoration: BoxDecoration( |             decoration: BoxDecoration( | ||||||
|               gradient: LinearGradient( |               gradient: APP_GRADIENT, | ||||||
|                 colors: [Colors.red, Colors.yellow] |  | ||||||
|               ), |  | ||||||
|               shape: BoxShape.circle, |               shape: BoxShape.circle, | ||||||
|               border: Border.all(color: Colors.black, width: 5), |               border: Border.all(color: Colors.black, width: 5), | ||||||
|             ), |             ), | ||||||
|             padding: EdgeInsets.all(5), |             width: 70, | ||||||
|             child: icon |             height: 70, | ||||||
|  |             padding: const EdgeInsets.all(5), | ||||||
|  |             child: Icon(landmark.type.icon.icon, size: 50), | ||||||
|           ), |           ), | ||||||
|           if (positionIndicator != null) positionIndicator, |           if (positionIndicator != null) positionIndicator, | ||||||
|         ], |         ], | ||||||
| @@ -1,4 +1,5 @@ | |||||||
| import 'package:anyway/layout.dart'; | import 'package:anyway/layout.dart'; | ||||||
|  | import 'package:anyway/main.dart'; | ||||||
| import 'package:anyway/structs/preferences.dart'; | import 'package:anyway/structs/preferences.dart'; | ||||||
| import 'package:anyway/structs/trip.dart'; | import 'package:anyway/structs/trip.dart'; | ||||||
| import 'package:anyway/utils/fetch_trip.dart'; | import 'package:anyway/utils/fetch_trip.dart'; | ||||||
| @@ -8,8 +9,12 @@ import 'package:flutter/material.dart'; | |||||||
|  |  | ||||||
| class NewTripButton extends StatefulWidget { | class NewTripButton extends StatefulWidget { | ||||||
|   final Trip trip; |   final Trip trip; | ||||||
|  |   final UserPreferences preferences; | ||||||
|  |  | ||||||
|   const NewTripButton({required this.trip}); |   const NewTripButton({ | ||||||
|  |     required this.trip, | ||||||
|  |     required this.preferences, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   State<NewTripButton> createState() => _NewTripButtonState(); |   State<NewTripButton> createState() => _NewTripButtonState(); | ||||||
| @@ -23,42 +28,39 @@ class _NewTripButtonState extends State<NewTripButton> { | |||||||
|       listenable: widget.trip, |       listenable: widget.trip, | ||||||
|       builder: (BuildContext context, Widget? child) { |       builder: (BuildContext context, Widget? child) { | ||||||
|         if (widget.trip.landmarks.isEmpty){ |         if (widget.trip.landmarks.isEmpty){ | ||||||
|  |           // Fallback if the trip setup is lagging behind | ||||||
|  |           // This should in theory never happen | ||||||
|           return Container(); |           return Container(); | ||||||
|         } |         } | ||||||
|         return FloatingActionButton.extended( |         return FloatingActionButton.extended( | ||||||
|           onPressed: () async { |           onPressed: onPressed, | ||||||
|             Future<UserPreferences> preferences = loadUserPreferences(); |           icon: const Icon(Icons.add), | ||||||
|             Trip trip = widget.trip; |           label: AutoSizeText('Start planning!'), | ||||||
|             fetchTrip(trip, preferences); |  | ||||||
|             Navigator.of(context).push( |  | ||||||
|               MaterialPageRoute( |  | ||||||
|                 builder: (context) => BasePage(mainScreen: "map", trip: trip) |  | ||||||
|               ) |  | ||||||
|             ); |  | ||||||
|           }, |  | ||||||
|           icon: Icon(Icons.add), |  | ||||||
|           label: 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, |  | ||||||
|                 ); |  | ||||||
|               } |  | ||||||
|             }, |  | ||||||
|           ) |  | ||||||
|         );  |         );  | ||||||
|          |  | ||||||
|       } |       } | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   void onPressed() async { | ||||||
|  |     // Check that the preferences are valid | ||||||
|  |     UserPreferences preferences = widget.preferences; | ||||||
|  |     if (preferences.nature.value == 0 && preferences.shopping.value == 0 && preferences.sightseeing.value == 0){ | ||||||
|  |       rootScaffoldMessengerKey.currentState!.showSnackBar( | ||||||
|  |         SnackBar(content: Text("Please specify at least one preference")) | ||||||
|  |       ); | ||||||
|  |     } else if (preferences.maxTime.value == 0){ | ||||||
|  |       rootScaffoldMessengerKey.currentState!.showSnackBar( | ||||||
|  |         SnackBar(content: Text("Please choose a longer duration")) | ||||||
|  |       ); | ||||||
|  |     } else { | ||||||
|  |       Trip trip = widget.trip; | ||||||
|  |       fetchTrip(trip, widget.preferences); | ||||||
|  |       Navigator.of(context).push( | ||||||
|  |         MaterialPageRoute( | ||||||
|  |           builder: (context) => BasePage(mainScreen: "map", trip: trip) | ||||||
|  |         ) | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,9 +6,13 @@ import 'dart:developer'; | |||||||
|  |  | ||||||
| import 'package:anyway/structs/trip.dart'; | import 'package:anyway/structs/trip.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:geolocator/geolocator.dart'; | ||||||
|  | import 'package:shared_preferences/shared_preferences.dart'; | ||||||
|  |  | ||||||
| class NewTripLocationSearch extends StatefulWidget { | class NewTripLocationSearch extends StatefulWidget { | ||||||
|  |   Future<SharedPreferences> prefs = SharedPreferences.getInstance(); | ||||||
|   Trip trip; |   Trip trip; | ||||||
|  |    | ||||||
|   NewTripLocationSearch( |   NewTripLocationSearch( | ||||||
|     this.trip, |     this.trip, | ||||||
|   ); |   ); | ||||||
| @@ -39,26 +43,66 @@ class _NewTripLocationSearchState extends State<NewTripLocationSearch> { | |||||||
|           uuid: 'pending', |           uuid: 'pending', | ||||||
|           name: query, |           name: query, | ||||||
|           location: [location.latitude, location.longitude], |           location: [location.latitude, location.longitude], | ||||||
|           type: start |           type: typeStart | ||||||
|         ) |         ) | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   late Widget locationSearchBar = SearchBar( | ||||||
|   Widget build(BuildContext context) { |     hintText: 'Enter a city name or long press on the map.', | ||||||
|     return SearchBar( |     onSubmitted: setTripLocation, | ||||||
|       hintText: 'Enter a city name or long press on the map.', |     controller: _controller, | ||||||
|       onSubmitted: setTripLocation, |     leading: Icon(Icons.search), | ||||||
|       controller: _controller, |     trailing: [ | ||||||
|       leading: Icon(Icons.search), |       ElevatedButton( | ||||||
|       trailing: [ElevatedButton( |  | ||||||
|         onPressed: () { |         onPressed: () { | ||||||
|           setTripLocation(_controller.text); |           setTripLocation(_controller.text); | ||||||
|         }, |         }, | ||||||
|         child: Text('Search'), |         child: Text('Search'), | ||||||
|       ),] |       ) | ||||||
|  |     ] | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   late Widget useCurrentLocationButton = ElevatedButton( | ||||||
|  |     onPressed: () async { | ||||||
|  |       // this widget is only shown if the user has already granted location permissions | ||||||
|  |       Position position = await Geolocator.getCurrentPosition(); | ||||||
|  |       widget.trip.landmarks.clear(); | ||||||
|  |       widget.trip.addLandmark( | ||||||
|  |         Landmark( | ||||||
|  |           uuid: 'pending', | ||||||
|  |           name: 'start', | ||||||
|  |           location: [position.latitude, position.longitude], | ||||||
|  |           type: typeStart | ||||||
|  |         ) | ||||||
|  |       ); | ||||||
|  |     }, | ||||||
|  |     child: Text('Use current location'), | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return FutureBuilder( | ||||||
|  |       future: widget.prefs, | ||||||
|  |       builder: (context, snapshot) { | ||||||
|  |         if (snapshot.hasData) { | ||||||
|  |           final useLocation = snapshot.data!.getBool('useLocation') ?? false; | ||||||
|  |           if (useLocation) { | ||||||
|  |             return Column( | ||||||
|  |               children: [ | ||||||
|  |                 locationSearchBar, | ||||||
|  |                 useCurrentLocationButton, | ||||||
|  |               ], | ||||||
|  |             ); | ||||||
|  |           } else { | ||||||
|  |             return locationSearchBar; | ||||||
|  |           } | ||||||
|  |         } else { | ||||||
|  |           return locationSearchBar; | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -1,13 +1,13 @@ | |||||||
|  |  | ||||||
| // A map that allows the user to select a location for a new trip. | // A map that allows the user to select a location for a new trip. | ||||||
| import 'dart:developer'; | import 'dart:developer'; | ||||||
|  |  | ||||||
| import 'package:anyway/constants.dart'; | import 'package:anyway/constants.dart'; | ||||||
| import 'package:anyway/modules/themed_marker.dart'; | import 'package:anyway/modules/landmark_map_marker.dart'; | ||||||
| import 'package:anyway/structs/landmark.dart'; | import 'package:anyway/structs/landmark.dart'; | ||||||
| import 'package:anyway/structs/trip.dart'; | import 'package:anyway/structs/trip.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:google_maps_flutter/google_maps_flutter.dart'; | 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'; | import 'package:widget_to_marker/widget_to_marker.dart'; | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -22,7 +22,7 @@ class NewTripMap extends StatefulWidget { | |||||||
| } | } | ||||||
|  |  | ||||||
| class _NewTripMapState extends State<NewTripMap> { | class _NewTripMapState extends State<NewTripMap> { | ||||||
|   final CameraPosition _cameraPosition = CameraPosition( |   final CameraPosition _cameraPosition = const CameraPosition( | ||||||
|     target: LatLng(48.8566, 2.3522), |     target: LatLng(48.8566, 2.3522), | ||||||
|     zoom: 11.0, |     zoom: 11.0, | ||||||
|   ); |   ); | ||||||
| @@ -37,7 +37,7 @@ class _NewTripMapState extends State<NewTripMap> { | |||||||
|         uuid: 'pending', |         uuid: 'pending', | ||||||
|         name: 'start', |         name: 'start', | ||||||
|         location: [location.latitude, location.longitude], |         location: [location.latitude, location.longitude], | ||||||
|         type: start |         type: typeStart | ||||||
|       ) |       ) | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| @@ -70,10 +70,26 @@ class _NewTripMapState extends State<NewTripMap> { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     widget.trip.addListener(updateTripDetails); |     widget.trip.addListener(updateTripDetails); | ||||||
|  |     Future<SharedPreferences> preferences = SharedPreferences.getInstance(); | ||||||
|  |  | ||||||
|  |     return FutureBuilder( | ||||||
|  |       future: preferences, | ||||||
|  |       builder: (context, snapshot) { | ||||||
|  |         if (snapshot.hasData) { | ||||||
|  |           SharedPreferences prefs = snapshot.data as SharedPreferences; | ||||||
|  |           bool useLocation = prefs.getBool('useLocation') ?? true; | ||||||
|  |           return _buildMap(useLocation); | ||||||
|  |         } else { | ||||||
|  |           return const CircularProgressIndicator(); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget _buildMap(bool useLocation) { | ||||||
|     return GoogleMap( |     return GoogleMap( | ||||||
|       onMapCreated: _onMapCreated, |       onMapCreated: _onMapCreated, | ||||||
|       initialCameraPosition: _cameraPosition, |       initialCameraPosition: _cameraPosition, | ||||||
| @@ -82,6 +98,8 @@ class _NewTripMapState extends State<NewTripMap> { | |||||||
|       cloudMapId: MAP_ID, |       cloudMapId: MAP_ID, | ||||||
|       mapToolbarEnabled: false, |       mapToolbarEnabled: false, | ||||||
|       zoomControlsEnabled: false, |       zoomControlsEnabled: false, | ||||||
|  |       myLocationButtonEnabled: false, | ||||||
|  |       myLocationEnabled: useLocation, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
							
								
								
									
										41
									
								
								frontend/lib/modules/new_trip_options_button.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								frontend/lib/modules/new_trip_options_button.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | import 'package:anyway/pages/new_trip_preferences.dart'; | ||||||
|  | import 'package:anyway/structs/trip.dart'; | ||||||
|  | import 'package:auto_size_text/auto_size_text.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class NewTripOptionsButton extends StatefulWidget { | ||||||
|  |   final Trip trip; | ||||||
|  |  | ||||||
|  |   const NewTripOptionsButton({required this.trip}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   State<NewTripOptionsButton> createState() => _NewTripOptionsButtonState(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _NewTripOptionsButtonState extends State<NewTripOptionsButton> { | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return ListenableBuilder( | ||||||
|  |       listenable: widget.trip, | ||||||
|  |       builder: (BuildContext context, Widget? child) { | ||||||
|  |         if (widget.trip.landmarks.isEmpty){ | ||||||
|  |           return Container(); | ||||||
|  |         } | ||||||
|  |         return FloatingActionButton.extended( | ||||||
|  |           onPressed: () async { | ||||||
|  |             Navigator.of(context).push( | ||||||
|  |               MaterialPageRoute( | ||||||
|  |                 builder: (context) => NewTripPreferencesPage(trip: widget.trip) | ||||||
|  |               ) | ||||||
|  |             ); | ||||||
|  |           }, | ||||||
|  |           icon: const Icon(Icons.add), | ||||||
|  |           label: const AutoSizeText('Set preferences') | ||||||
|  |         );  | ||||||
|  |       } | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -16,20 +16,23 @@ class OnboardingCard extends StatelessWidget { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     Color baseColor = Theme.of(context).primaryColor; |     Color baseColor = Theme.of(context).colorScheme.secondary; | ||||||
|     // have a different color for each card, incrementing the hue |     // have a different color for each card, incrementing the hue | ||||||
|     Color currentColor = baseColor.withAlpha(baseColor.alpha - index * 30); |     Color currentColor = baseColor.withAlpha(baseColor.alpha - index * 30); | ||||||
|     return Container( |     return Container( | ||||||
|       color: currentColor, |       color: currentColor, | ||||||
|  |       alignment: Alignment.center, | ||||||
|       child: Padding( |       child: Padding( | ||||||
|         padding: EdgeInsets.all(20), |         padding: EdgeInsets.all(20), | ||||||
|         child: Column( |         child: Column( | ||||||
|  |           mainAxisAlignment: MainAxisAlignment.center, | ||||||
|           children: [ |           children: [ | ||||||
|             Text( |             Text( | ||||||
|               title, |               title, | ||||||
|               style: TextStyle( |               style: TextStyle( | ||||||
|                 fontSize: 24, |                 fontSize: 24, | ||||||
|                 fontWeight: FontWeight.bold, |                 fontWeight: FontWeight.bold, | ||||||
|  |                 color: Colors.white, | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
|             Padding(padding: EdgeInsets.only(top: 20)), |             Padding(padding: EdgeInsets.only(top: 20)), | ||||||
| @@ -44,7 +47,8 @@ class OnboardingCard extends StatelessWidget { | |||||||
|                 fontSize: 16, |                 fontSize: 16, | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
|           ], |  | ||||||
|  |           ] | ||||||
|         ), |         ), | ||||||
|       ) |       ) | ||||||
|     ); |     ); | ||||||
|   | |||||||
| @@ -1,11 +1,10 @@ | |||||||
| import 'package:anyway/modules/current_trip_save_button.dart'; | import 'package:anyway/constants.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:sliding_up_panel/sliding_up_panel.dart'; | import 'package:sliding_up_panel/sliding_up_panel.dart'; | ||||||
|  |  | ||||||
| import 'package:anyway/structs/trip.dart'; | import 'package:anyway/structs/trip.dart'; | ||||||
| import 'package:anyway/modules/current_trip_landmarks_list.dart'; |  | ||||||
| import 'package:anyway/modules/current_trip_greeter.dart'; |  | ||||||
| import 'package:anyway/modules/current_trip_map.dart'; | import 'package:anyway/modules/current_trip_map.dart'; | ||||||
|  | import 'package:anyway/modules/current_trip_panel.dart'; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -27,16 +26,21 @@ class _TripPageState extends State<TripPage> { | |||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     return SlidingUpPanel( |     return SlidingUpPanel( | ||||||
|         panelBuilder: (sc) => _panelFull(sc), |         // use panelBuilder instead of panel so that we can reuse the scrollcontroller for the listview | ||||||
|         // collapsed: _floatingCollapsed(), |         panelBuilder: (scrollcontroller) => CurrentTripPanel(controller: scrollcontroller, trip: widget.trip), | ||||||
|         body: MapWidget(trip: widget.trip), |         // using collapsed and panelBuilder seems to show both at the same time, so we include the greeter in the panelBuilder | ||||||
|         // renderPanelSheet: false, |         // collapsed: Greeter(trip: widget.trip), | ||||||
|         // backdropEnabled: true, |         body: CurrentTripMap(trip: widget.trip), | ||||||
|         maxHeight: MediaQuery.of(context).size.height * 0.8, |         minHeight: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT, | ||||||
|         padding: EdgeInsets.only(left: 10, right: 10, top: 25, bottom: 10), |         maxHeight: MediaQuery.of(context).size.height * TRIP_PANEL_MAX_HEIGHT, | ||||||
|         // panelSnapping: false, |         // padding in this context is annoying: it offsets the notion of vertical alignment. | ||||||
|         borderRadius: BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)), |         // children that want to be centered vertically need to have their size adjusted by 2x the padding | ||||||
|         boxShadow: [ |         padding: const EdgeInsets.only(top: 10), | ||||||
|  |         // Panel snapping should not be disabled because it significantly improves the user experience | ||||||
|  |         // panelSnapping: false | ||||||
|  |         borderRadius: const BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)), | ||||||
|  |         parallaxEnabled: true, | ||||||
|  |         boxShadow: const [ | ||||||
|           BoxShadow( |           BoxShadow( | ||||||
|             blurRadius: 20.0, |             blurRadius: 20.0, | ||||||
|             color: Colors.black, |             color: Colors.black, | ||||||
| @@ -44,41 +48,4 @@ class _TripPageState extends State<TripPage> { | |||||||
|         ], |         ], | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|    |  | ||||||
|   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}'), |  | ||||||
|               ), |  | ||||||
|             ], |  | ||||||
|           ); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,11 +1,7 @@ | |||||||
| import 'package:anyway/modules/new_trip_button.dart'; | import 'package:anyway/modules/new_trip_button.dart'; | ||||||
| import 'package:anyway/structs/landmark.dart'; | import 'package:anyway/modules/new_trip_options_button.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:geocoding/geocoding.dart'; |  | ||||||
| 
 | 
 | ||||||
| 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/structs/trip.dart"; | ||||||
| import 'package:anyway/modules/new_trip_location_search.dart'; | import 'package:anyway/modules/new_trip_location_search.dart'; | ||||||
| import 'package:anyway/modules/new_trip_map.dart'; | import 'package:anyway/modules/new_trip_map.dart'; | ||||||
| @@ -19,7 +15,6 @@ class NewTripPage extends StatefulWidget { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| class _NewTripPageState extends State<NewTripPage> { | class _NewTripPageState extends State<NewTripPage> { | ||||||
|   final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); |  | ||||||
|   final TextEditingController latController = TextEditingController(); |   final TextEditingController latController = TextEditingController(); | ||||||
|   final TextEditingController lonController = TextEditingController(); |   final TextEditingController lonController = TextEditingController(); | ||||||
|   Trip trip = Trip(); |   Trip trip = Trip(); | ||||||
| @@ -40,7 +35,7 @@ class _NewTripPageState extends State<NewTripPage> { | |||||||
|           ), |           ), | ||||||
|         ], |         ], | ||||||
|       ), |       ), | ||||||
|       floatingActionButton: NewTripButton(trip: trip), |       floatingActionButton: NewTripOptionsButton(trip: trip), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
							
								
								
									
										113
									
								
								frontend/lib/pages/new_trip_preferences.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								frontend/lib/pages/new_trip_preferences.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | |||||||
|  | import 'package:anyway/modules/new_trip_button.dart'; | ||||||
|  | import 'package:anyway/structs/preferences.dart'; | ||||||
|  | import 'package:anyway/structs/trip.dart'; | ||||||
|  | import 'package:flutter/cupertino.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class NewTripPreferencesPage extends StatefulWidget { | ||||||
|  |   final Trip trip; | ||||||
|  |   const NewTripPreferencesPage({required this.trip}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   _NewTripPreferencesPageState createState() => _NewTripPreferencesPageState(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _NewTripPreferencesPageState extends State<NewTripPreferencesPage> { | ||||||
|  |   UserPreferences preferences = UserPreferences(); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Scaffold( | ||||||
|  |       body: ListView( | ||||||
|  |         children: [ | ||||||
|  |           // Center( | ||||||
|  |           //   child: CircleAvatar( | ||||||
|  |           //     radius: 100, | ||||||
|  |           //     child: Icon(Icons.person, size: 100), | ||||||
|  |           //   ) | ||||||
|  |           // ), | ||||||
|  |           Padding(padding: EdgeInsets.only(top: 30)), | ||||||
|  |           Center( | ||||||
|  |             child: FutureBuilder( | ||||||
|  |               future: widget.trip.cityName, | ||||||
|  |               builder: (context, snapshot) => Text( | ||||||
|  |                 'Your trip to ${snapshot.hasData ? snapshot.data! : "..."}', | ||||||
|  |                 style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold) | ||||||
|  |               ) | ||||||
|  |             ) | ||||||
|  |           ), | ||||||
|  |  | ||||||
|  |           Center( | ||||||
|  |             child: Padding( | ||||||
|  |             padding: EdgeInsets.only(left: 10, right: 10, top: 20, bottom: 0), | ||||||
|  |               child: Text('Tell us about your ideal trip.', style: TextStyle(fontSize: 18)) | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |  | ||||||
|  |           Divider(indent: 25, endIndent: 25, height: 50), | ||||||
|  |  | ||||||
|  |           durationPicker(preferences.maxTime), | ||||||
|  |  | ||||||
|  |           preferenceSliders([preferences.sightseeing, preferences.shopping, preferences.nature]), | ||||||
|  |         ] | ||||||
|  |       ), | ||||||
|  |       floatingActionButton: NewTripButton(trip: widget.trip, preferences: preferences), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   Widget durationPicker(SinglePreference maxTime) { | ||||||
|  |     return Card( | ||||||
|  |       margin: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0), | ||||||
|  |       shadowColor: Colors.grey, | ||||||
|  |       child: ListTile( | ||||||
|  |         leading: Icon(Icons.timer), | ||||||
|  |         title: Text(preferences.maxTime.description), | ||||||
|  |         subtitle: CupertinoTimerPicker( | ||||||
|  |           mode: CupertinoTimerPickerMode.hm, | ||||||
|  |           initialTimerDuration: Duration(minutes: 90), | ||||||
|  |           minuteInterval: 15, | ||||||
|  |           onTimerDurationChanged: (Duration newDuration) { | ||||||
|  |             setState(() { | ||||||
|  |               preferences.maxTime.value = newDuration.inMinutes; | ||||||
|  |             }); | ||||||
|  |           }, | ||||||
|  |         ) | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   Widget preferenceSliders(List<SinglePreference> prefs) { | ||||||
|  |     List<Card> sliders = []; | ||||||
|  |       for (SinglePreference pref in prefs) { | ||||||
|  |       sliders.add( | ||||||
|  |         Card( | ||||||
|  |           child: ListTile( | ||||||
|  |             leading: pref.icon, | ||||||
|  |             title: Text(pref.name), | ||||||
|  |             subtitle: Slider( | ||||||
|  |               value: pref.value.toDouble(), | ||||||
|  |               min: pref.minVal.toDouble(), | ||||||
|  |               max: pref.maxVal.toDouble(), | ||||||
|  |               divisions: pref.maxVal - pref.minVal, | ||||||
|  |               label: pref.value.toString(), | ||||||
|  |               onChanged: (double newValue) { | ||||||
|  |                 setState(() { | ||||||
|  |                   pref.value = newValue.toInt(); | ||||||
|  |                 }); | ||||||
|  |               }, | ||||||
|  |             ) | ||||||
|  |           ), | ||||||
|  |         ) | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return Column( | ||||||
|  |       children: sliders | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| import 'package:anyway/modules/onboarding_card.dart'; | import 'package:anyway/modules/onboarding_card.dart'; | ||||||
| import 'package:anyway/pages/new_trip.dart'; | import 'package:anyway/pages/new_trip_location.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  |  | ||||||
| class OnboardingPage extends StatefulWidget { | class OnboardingPage extends StatefulWidget { | ||||||
|   | |||||||
| @@ -1,177 +0,0 @@ | |||||||
| import 'package:anyway/constants.dart'; |  | ||||||
| import 'package:anyway/structs/preferences.dart'; |  | ||||||
| import 'package:flutter/material.dart'; |  | ||||||
|  |  | ||||||
|  |  | ||||||
| bool debugMode = false; |  | ||||||
|  |  | ||||||
| class ProfilePage extends StatefulWidget { |  | ||||||
|   @override |  | ||||||
|   _ProfilePageState createState() => _ProfilePageState(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class _ProfilePageState extends State<ProfilePage> { |  | ||||||
|   Future<UserPreferences> _prefs = loadUserPreferences(); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     return ListView( |  | ||||||
|       children: [ |  | ||||||
|         // First a round, centered image |  | ||||||
|         Center( |  | ||||||
|           child: CircleAvatar( |  | ||||||
|             radius: 100, |  | ||||||
|             child: Icon(Icons.person, size: 100), |  | ||||||
|           ) |  | ||||||
|         ), |  | ||||||
|         Center( |  | ||||||
|           child: Text('Curious traveler', style: TextStyle(fontSize: 24)) |  | ||||||
|         ), |  | ||||||
|  |  | ||||||
|         Divider(indent: 25, endIndent: 25, height: 50), |  | ||||||
|  |  | ||||||
|         Center( |  | ||||||
|           child: Padding( |  | ||||||
|            padding: EdgeInsets.only(left: 10, right: 10, top: 0, bottom: 10), |  | ||||||
|             child: Text('For a tailored experience, please rate your discovery preferences.', style: TextStyle(fontSize: 18)) |  | ||||||
|           ), |  | ||||||
|         ), |  | ||||||
|  |  | ||||||
|         FutureBuilder(future: _prefs, builder: futureSliders), |  | ||||||
|  |  | ||||||
|         Divider(indent: 25, endIndent: 25, height: 50), |  | ||||||
|  |  | ||||||
|         privacyInfo(), |  | ||||||
|  |  | ||||||
|         debugButton(), |  | ||||||
|       ] |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget debugButton() { |  | ||||||
|     return Padding( |  | ||||||
|       padding: EdgeInsets.only(top: 20), |  | ||||||
|       child: Row( |  | ||||||
|         children: [ |  | ||||||
|           Text('Debug mode'), |  | ||||||
|           Switch( |  | ||||||
|             value: debugMode, |  | ||||||
|             onChanged: (bool? newValue) { |  | ||||||
|               setState(() { |  | ||||||
|                 debugMode = newValue!; |  | ||||||
|                 showDialog( |  | ||||||
|                   context: context, |  | ||||||
|                   builder: (BuildContext context) { |  | ||||||
|                     return AlertDialog( |  | ||||||
|                       title: Text('Debug mode - custom API'), |  | ||||||
|                       content: TextField( |  | ||||||
|                         decoration: InputDecoration( |  | ||||||
|                           hintText: 'http://localhost:8000' |  | ||||||
|                         ), |  | ||||||
|                       onChanged: (value) { |  | ||||||
|                         setState(() { |  | ||||||
|                           API_URL_BASE = value; |  | ||||||
|                         }); |  | ||||||
|                       }, |  | ||||||
|                     ), |  | ||||||
|                     actions: [ |  | ||||||
|                       TextButton( |  | ||||||
|                         child: Text('OK'), |  | ||||||
|                         onPressed: () { |  | ||||||
|                           Navigator.of(context).pop(); |  | ||||||
|                         }, |  | ||||||
|                       ), |  | ||||||
|                     ], |  | ||||||
|                     ); |  | ||||||
|                   } |  | ||||||
|                 ); |  | ||||||
|               }); |  | ||||||
|             } |  | ||||||
|           ) |  | ||||||
|         ], |  | ||||||
|       ) |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|   Widget futureSliders(BuildContext context, AsyncSnapshot<UserPreferences> snapshot) { |  | ||||||
|     if (snapshot.connectionState == ConnectionState.done) { |  | ||||||
|       UserPreferences prefs = snapshot.data!; |  | ||||||
|  |  | ||||||
|       return Column( |  | ||||||
|         children: [ |  | ||||||
|           PreferenceSliders(prefs: [prefs.maxTime, prefs.maxDetour]), |  | ||||||
|           Divider(indent: 25, endIndent: 25, height: 50), |  | ||||||
|           PreferenceSliders(prefs: [prefs.sightseeing, prefs.shopping, prefs.nature]) |  | ||||||
|         ] |  | ||||||
|       ); |  | ||||||
|     } else { |  | ||||||
|       return CircularProgressIndicator(); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget privacyInfo() { |  | ||||||
|     return Padding( |  | ||||||
|       padding: EdgeInsets.only(top: 20), |  | ||||||
|       child: Row( |  | ||||||
|         children: [ |  | ||||||
|           Text('Privacy policy is available at '), |  | ||||||
|           TextButton.icon( |  | ||||||
|             icon: Icon(Icons.info), |  | ||||||
|             label: Text(PRIVACY_URL), |  | ||||||
|             onPressed: () { |  | ||||||
|             } |  | ||||||
|           ) |  | ||||||
|         ], |  | ||||||
|       ) |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class PreferenceSliders extends StatefulWidget { |  | ||||||
|   final List<SinglePreference> prefs; |  | ||||||
|  |  | ||||||
|   PreferenceSliders({required this.prefs}); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   State<PreferenceSliders> createState() => _PreferenceSlidersState(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class _PreferenceSlidersState extends State<PreferenceSliders> { |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     List<Card> sliders = []; |  | ||||||
|       for (SinglePreference pref in widget.prefs) { |  | ||||||
|       sliders.add( |  | ||||||
|         Card( |  | ||||||
|           child: ListTile( |  | ||||||
|             leading: pref.icon, |  | ||||||
|             title: Text(pref.name), |  | ||||||
|             subtitle: Slider( |  | ||||||
|               value: pref.value.toDouble(), |  | ||||||
|               min: pref.minVal.toDouble(), |  | ||||||
|               max: pref.maxVal.toDouble(), |  | ||||||
|               divisions: pref.maxVal - pref.minVal, |  | ||||||
|               label: pref.value.toString(), |  | ||||||
|               onChanged: (double newValue) { |  | ||||||
|                 setState(() { |  | ||||||
|                   pref.value = newValue.toInt(); |  | ||||||
|                   pref.save(); |  | ||||||
|                 }); |  | ||||||
|               }, |  | ||||||
|             ) |  | ||||||
|           ), |  | ||||||
|           margin: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0), |  | ||||||
|           shadowColor: Colors.grey, |  | ||||||
|         ) |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return Column( |  | ||||||
|       children: sliders); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
							
								
								
									
										188
									
								
								frontend/lib/pages/settings.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								frontend/lib/pages/settings.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,188 @@ | |||||||
|  | import 'package:anyway/constants.dart'; | ||||||
|  | import 'package:anyway/main.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:permission_handler/permission_handler.dart'; | ||||||
|  | import 'package:shared_preferences/shared_preferences.dart'; | ||||||
|  | import 'package:url_launcher/url_launcher.dart'; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | bool debugMode = false; | ||||||
|  |  | ||||||
|  | class SettingsPage extends StatefulWidget { | ||||||
|  |   @override | ||||||
|  |   _SettingsPageState createState() => _SettingsPageState(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _SettingsPageState extends State<SettingsPage> { | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return ListView( | ||||||
|  |       padding: EdgeInsets.all(15), | ||||||
|  |       children: [ | ||||||
|  |         // First a round, centered image | ||||||
|  |         Center( | ||||||
|  |           child: CircleAvatar( | ||||||
|  |             radius: 75, | ||||||
|  |             child: Icon(Icons.settings, size: 100), | ||||||
|  |           ) | ||||||
|  |         ), | ||||||
|  |         Center( | ||||||
|  |           child: Text('Global settings', style: TextStyle(fontSize: 24)) | ||||||
|  |         ), | ||||||
|  |  | ||||||
|  |         Divider(indent: 25, endIndent: 25, height: 50), | ||||||
|  |  | ||||||
|  |         darkMode(), | ||||||
|  |         setLocationUsage(), | ||||||
|  |         setDebugMode(), | ||||||
|  |  | ||||||
|  |         Divider(indent: 25, endIndent: 25, height: 50), | ||||||
|  |  | ||||||
|  |         privacyInfo(), | ||||||
|  |       ] | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget setDebugMode() { | ||||||
|  |     return Row( | ||||||
|  |       children: [ | ||||||
|  |         Text('Debugging: use a custom API URL'), | ||||||
|  |         // white space | ||||||
|  |         Spacer(), | ||||||
|  |         Switch( | ||||||
|  |           value: debugMode, | ||||||
|  |           onChanged: (bool? newValue) { | ||||||
|  |             setState(() { | ||||||
|  |               debugMode = newValue!; | ||||||
|  |               if (debugMode) { | ||||||
|  |                 showDialog( | ||||||
|  |                   context: context, | ||||||
|  |                   builder: (BuildContext context) { | ||||||
|  |                     return AlertDialog( | ||||||
|  |                       title: Text('Debug mode - use a custom API endpoint'), | ||||||
|  |                       content: TextField( | ||||||
|  |                         decoration: InputDecoration( | ||||||
|  |                           hintText: 'https://anyway-stg.anydev.info' | ||||||
|  |                         ), | ||||||
|  |                         onChanged: (value) { | ||||||
|  |                           setState(() { | ||||||
|  |                             API_URL_BASE = value; | ||||||
|  |                           }); | ||||||
|  |                         }, | ||||||
|  |                       ), | ||||||
|  |                       actions: [ | ||||||
|  |                         TextButton( | ||||||
|  |                           child: Text('OK'), | ||||||
|  |                           onPressed: () { | ||||||
|  |                             Navigator.of(context).pop(); | ||||||
|  |                           }, | ||||||
|  |                         ), | ||||||
|  |                       ], | ||||||
|  |                     ); | ||||||
|  |                   } | ||||||
|  |                 ); | ||||||
|  |               } | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |         ) | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget darkMode() { | ||||||
|  |     return Row( | ||||||
|  |       children: [ | ||||||
|  |         Text('Dark mode'), | ||||||
|  |         Spacer(), | ||||||
|  |         Switch( | ||||||
|  |           value: Theme.of(context).brightness == Brightness.dark, | ||||||
|  |           onChanged: (bool? newValue) { | ||||||
|  |             setState(() { | ||||||
|  |               rootScaffoldMessengerKey.currentState!.showSnackBar( | ||||||
|  |                 SnackBar(content: Text('Dark mode is not implemented yet')) | ||||||
|  |               ); | ||||||
|  |               // if (newValue!) { | ||||||
|  |               //   // Dark mode | ||||||
|  |               //   Theme.of(context).brightness = Brightness.dark; | ||||||
|  |               // } else { | ||||||
|  |               //   // Light mode | ||||||
|  |               //   Theme.of(context).brightness = Brightness.light; | ||||||
|  |               // } | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |         ) | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget setLocationUsage() { | ||||||
|  |     Future<SharedPreferences> preferences = SharedPreferences.getInstance(); | ||||||
|  |     return Row( | ||||||
|  |       children: [ | ||||||
|  |         Text('Use location services'), | ||||||
|  |         // white space | ||||||
|  |         Spacer(), | ||||||
|  |         FutureBuilder( | ||||||
|  |           future: preferences, | ||||||
|  |           builder: (context, snapshot) { | ||||||
|  |             if (snapshot.hasData) { | ||||||
|  |               bool useLocation = snapshot.data!.getBool('useLocation') ?? false; | ||||||
|  |               return Switch( | ||||||
|  |                 value: useLocation, | ||||||
|  |                 onChanged: setUseLocation, | ||||||
|  |               ); | ||||||
|  |             } else { | ||||||
|  |               return CircularProgressIndicator(); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         ) | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void setUseLocation(bool newValue) async { | ||||||
|  |     await Permission.locationWhenInUse | ||||||
|  |       .onDeniedCallback(() { | ||||||
|  |         rootScaffoldMessengerKey.currentState!.showSnackBar( | ||||||
|  |           SnackBar(content: Text('Location services are required for this feature')) | ||||||
|  |         ); | ||||||
|  |       }) | ||||||
|  |       .onGrantedCallback(() { | ||||||
|  |         rootScaffoldMessengerKey.currentState!.showSnackBar( | ||||||
|  |           SnackBar(content: Text('Location services are now enabled')) | ||||||
|  |         ); | ||||||
|  |         SharedPreferences.getInstance().then( | ||||||
|  |           (SharedPreferences prefs) { | ||||||
|  |             setState(() { | ||||||
|  |               prefs.setBool('useLocation', newValue); | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |         ); | ||||||
|  |       }) | ||||||
|  |       .onPermanentlyDeniedCallback(() { | ||||||
|  |         rootScaffoldMessengerKey.currentState!.showSnackBar( | ||||||
|  |           SnackBar(content: Text('Location services are required for this feature')) | ||||||
|  |         ); | ||||||
|  |       })   | ||||||
|  |     .request(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget privacyInfo() { | ||||||
|  |     return Center( | ||||||
|  |       child: Column( | ||||||
|  |         children: [ | ||||||
|  |           Text('Our privacy policy is available under:'), | ||||||
|  |            | ||||||
|  |           TextButton.icon( | ||||||
|  |             icon: Icon(Icons.info), | ||||||
|  |             label: Text(PRIVACY_URL), | ||||||
|  |             onPressed: () async{ | ||||||
|  |               await launchUrl(Uri.parse(PRIVACY_URL)); | ||||||
|  |             } | ||||||
|  |           ) | ||||||
|  |         ], | ||||||
|  |       ) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -4,13 +4,13 @@ import 'dart:convert'; | |||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:shared_preferences/shared_preferences.dart'; | import 'package:shared_preferences/shared_preferences.dart'; | ||||||
|  |  | ||||||
| LandmarkType sightseeing = LandmarkType(name: 'sightseeing'); | LandmarkType typeSightseeing = LandmarkType(name: 'sightseeing'); | ||||||
| LandmarkType nature = LandmarkType(name: 'nature'); | LandmarkType typeNature = LandmarkType(name: 'nature'); | ||||||
| LandmarkType shopping = LandmarkType(name: 'shopping'); | LandmarkType typeShopping = LandmarkType(name: 'shopping'); | ||||||
| // LandmarkType museum = LandmarkType(name: 'Museum'); | // LandmarkType museum = LandmarkType(name: 'Museum'); | ||||||
| // LandmarkType restaurant = LandmarkType(name: 'Restaurant'); | // LandmarkType restaurant = LandmarkType(name: 'Restaurant'); | ||||||
| LandmarkType start = LandmarkType(name: 'start'); | LandmarkType typeStart = LandmarkType(name: 'start'); | ||||||
| LandmarkType finish = LandmarkType(name: 'finish'); | LandmarkType typeFinish = LandmarkType(name: 'finish'); | ||||||
|  |  | ||||||
|  |  | ||||||
| final class Landmark extends LinkedListEntry<Landmark>{ | final class Landmark extends LinkedListEntry<Landmark>{ | ||||||
| @@ -130,22 +130,22 @@ class LandmarkType { | |||||||
|   LandmarkType({required this.name, this.icon = const Icon(Icons.location_on)}) { |   LandmarkType({required this.name, this.icon = const Icon(Icons.location_on)}) { | ||||||
|     switch (name) { |     switch (name) { | ||||||
|       case 'sightseeing': |       case 'sightseeing': | ||||||
|         icon = Icon(Icons.church); |         icon = const Icon(Icons.church); | ||||||
|         break; |         break; | ||||||
|       case 'nature': |       case 'nature': | ||||||
|         icon = Icon(Icons.eco); |         icon = const Icon(Icons.eco); | ||||||
|         break; |         break; | ||||||
|       case 'shopping': |       case 'shopping': | ||||||
|         icon = Icon(Icons.shopping_cart); |         icon = const Icon(Icons.shopping_cart); | ||||||
|         break; |         break; | ||||||
|       case 'start': |       case 'start': | ||||||
|         icon = Icon(Icons.play_arrow); |         icon = const Icon(Icons.play_arrow); | ||||||
|         break; |         break; | ||||||
|       case 'finish': |       case 'finish': | ||||||
|         icon = Icon(Icons.flag); |         icon = const Icon(Icons.flag); | ||||||
|         break; |         break; | ||||||
|       default: |       default: | ||||||
|         icon = Icon(Icons.location_on); |         icon = const Icon(Icons.location_on); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   @override |   @override | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
|  | import 'package:anyway/structs/landmark.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:shared_preferences/shared_preferences.dart'; |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class SinglePreference { | class SinglePreference { | ||||||
| @@ -20,16 +20,6 @@ class SinglePreference { | |||||||
|     this.minVal = 0, |     this.minVal = 0, | ||||||
|     this.maxVal = 5, |     this.maxVal = 5, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   void save() async { |  | ||||||
|     SharedPreferences sharedPrefs = await SharedPreferences.getInstance(); |  | ||||||
|       sharedPrefs.setInt('pref_$slug', value); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void load() async { |  | ||||||
|     SharedPreferences sharedPrefs = await SharedPreferences.getInstance(); |  | ||||||
|       value = sharedPrefs.getInt('pref_$slug') ?? minVal; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -39,64 +29,41 @@ class UserPreferences { | |||||||
|     slug: "sightseeing", |     slug: "sightseeing", | ||||||
|     description: "How much do you like sightseeing?", |     description: "How much do you like sightseeing?", | ||||||
|     value: 0, |     value: 0, | ||||||
|     icon: Icon(Icons.church), |     icon: typeSightseeing.icon, | ||||||
|   ); |   ); | ||||||
|   SinglePreference shopping = SinglePreference( |   SinglePreference shopping = SinglePreference( | ||||||
|     name: "Shopping", |     name: "Shopping", | ||||||
|     slug: "shopping", |     slug: "shopping", | ||||||
|     description: "How much do you like shopping?", |     description: "How much do you like shopping?", | ||||||
|     value: 0, |     value: 0, | ||||||
|     icon: Icon(Icons.shopping_bag), |     icon: typeShopping.icon, | ||||||
|   ); |   ); | ||||||
|   SinglePreference nature = SinglePreference( |   SinglePreference nature = SinglePreference( | ||||||
|     name: "Nature", |     name: "Nature", | ||||||
|     slug: "nature", |     slug: "nature", | ||||||
|     description: "How much do you like nature?", |     description: "How much do you like nature?", | ||||||
|     value: 0, |     value: 0, | ||||||
|     icon: Icon(Icons.landscape), |     icon: typeNature.icon, | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   SinglePreference maxTime = SinglePreference( |   SinglePreference maxTime = SinglePreference( | ||||||
|     name: "Trip duration", |     name: "Trip duration", | ||||||
|     slug: "duration", |     slug: "duration", | ||||||
|     description: "How long do you want your trip to be?", |     description: "How long should your trip be?", | ||||||
|     value: 30, |     value: 30, | ||||||
|     minVal: 30, |     minVal: 30, | ||||||
|     maxVal: 720, |     maxVal: 720, | ||||||
|     icon: Icon(Icons.timer), |     icon: Icon(Icons.timer), | ||||||
|   ); |   ); | ||||||
|   SinglePreference maxDetour = SinglePreference( |  | ||||||
|     name: "Trip detours", |  | ||||||
|     slug: "detours", |  | ||||||
|     description: "Are you okay with roaming even if makes the trip longer?", |  | ||||||
|     value: 0, |  | ||||||
|     maxVal: 30, |  | ||||||
|     icon: Icon(Icons.loupe_sharp), |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|   Future<void> load() async { |  | ||||||
|     for (SinglePreference pref in [sightseeing, shopping, nature, maxTime, maxDetour]) { |  | ||||||
|       pref.load(); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Map<String, dynamic> toJson() { |   Map<String, dynamic> toJson() { | ||||||
|       // This is "opinionated" JSON, corresponding to the backend's expectations |       // This is "opinionated" JSON, corresponding to the backend's expectations | ||||||
|       return { |       return { | ||||||
|         "sightseeing": {"type": "sightseeing", "score": sightseeing.value}, |         "sightseeing": {"type": "sightseeing", "score": sightseeing.value}, | ||||||
|         "shopping": {"type": "shopping", "score": shopping.value}, |         "shopping": {"type": "shopping", "score": shopping.value}, | ||||||
|         "nature": {"type": "nature", "score": nature.value}, |         "nature": {"type": "nature", "score": nature.value}, | ||||||
|         "max_time_minute": maxTime.value, |         "max_time_minute": maxTime.value | ||||||
|         "detour_tolerance_minute": maxDetour.value |  | ||||||
|       }; |       }; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| Future<UserPreferences> loadUserPreferences() async { |  | ||||||
|   UserPreferences prefs = UserPreferences(); |  | ||||||
|   await prefs.load(); |  | ||||||
|   return prefs; |  | ||||||
| } |  | ||||||
| @@ -29,11 +29,10 @@ Dio dio = Dio( | |||||||
|  |  | ||||||
| fetchTrip( | fetchTrip( | ||||||
|   Trip trip, |   Trip trip, | ||||||
|   Future<UserPreferences> preferences, |   UserPreferences preferences, | ||||||
| ) async { | ) async { | ||||||
|   UserPreferences prefs = await preferences; |  | ||||||
|   Map<String, dynamic> data = { |   Map<String, dynamic> data = { | ||||||
|     "preferences": prefs.toJson(), |     "preferences": preferences.toJson(), | ||||||
|     "start": trip.landmarks!.first.location, |     "start": trip.landmarks!.first.location, | ||||||
|   }; |   }; | ||||||
|   String dataString = jsonEncode(data); |   String dataString = jsonEncode(data); | ||||||
| @@ -47,11 +46,16 @@ fetchTrip( | |||||||
|   // handle errors |   // handle errors | ||||||
|   if (response.statusCode != 200) { |   if (response.statusCode != 200) { | ||||||
|     trip.updateUUID("error"); |     trip.updateUUID("error"); | ||||||
|     if (response.data["detail"] != null) { |     String errorDetail; | ||||||
|       trip.updateError(response.data["detail"]); |     if (response.data.runtimeType == String) { | ||||||
|       log(response.data["detail"]); |       errorDetail = response.data; | ||||||
|       // throw Exception(response.data["detail"]); |     } else { | ||||||
|  |       errorDetail = response.data["detail"] ?? "Unknown error"; | ||||||
|     } |     } | ||||||
|  |     trip.updateError(errorDetail); | ||||||
|  |     log(errorDetail); | ||||||
|  |     // Actualy no need to throw an exception, we can just log the error and let the user retry | ||||||
|  |     // throw Exception(errorDetail); | ||||||
|   } else { |   } else { | ||||||
|     Map<String, dynamic> json = response.data; |     Map<String, dynamic> json = response.data; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,12 +5,14 @@ | |||||||
| import FlutterMacOS | import FlutterMacOS | ||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
|  | import geolocator_apple | ||||||
| import path_provider_foundation | import path_provider_foundation | ||||||
| import shared_preferences_foundation | import shared_preferences_foundation | ||||||
| import sqflite | import sqflite | ||||||
| import url_launcher_macos | import url_launcher_macos | ||||||
|  |  | ||||||
| func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { | ||||||
|  |   GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) | ||||||
|   PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) |   PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) | ||||||
|   SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) |   SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) | ||||||
|   SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) |   SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) | ||||||
|   | |||||||
| @@ -264,6 +264,54 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.2.0" |     version: "3.2.0" | ||||||
|  |   geolocator: | ||||||
|  |     dependency: "direct main" | ||||||
|  |     description: | ||||||
|  |       name: geolocator | ||||||
|  |       sha256: "0ec58b731776bc43097fcf751f79681b6a8f6d3bc737c94779fe9f1ad73c1a81" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "13.0.1" | ||||||
|  |   geolocator_android: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: geolocator_android | ||||||
|  |       sha256: "7aefc530db47d90d0580b552df3242440a10fe60814496a979aa67aa98b1fd47" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "4.6.1" | ||||||
|  |   geolocator_apple: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: geolocator_apple | ||||||
|  |       sha256: bc2aca02423ad429cb0556121f56e60360a2b7d694c8570301d06ea0c00732fd | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "2.3.7" | ||||||
|  |   geolocator_platform_interface: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: geolocator_platform_interface | ||||||
|  |       sha256: "386ce3d9cce47838355000070b1d0b13efb5bc430f8ecda7e9238c8409ace012" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "4.2.4" | ||||||
|  |   geolocator_web: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: geolocator_web | ||||||
|  |       sha256: "2ed69328e05cd94e7eb48bb0535f5fc0c0c44d1c4fa1e9737267484d05c29b5e" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "4.1.1" | ||||||
|  |   geolocator_windows: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: geolocator_windows | ||||||
|  |       sha256: "53da08937d07c24b0d9952eb57a3b474e29aae2abf9dd717f7e1230995f13f0e" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "0.2.3" | ||||||
|   google_maps: |   google_maps: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -496,6 +544,54 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.3.0" |     version: "2.3.0" | ||||||
|  |   permission_handler: | ||||||
|  |     dependency: "direct main" | ||||||
|  |     description: | ||||||
|  |       name: permission_handler | ||||||
|  |       sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "11.3.1" | ||||||
|  |   permission_handler_android: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: permission_handler_android | ||||||
|  |       sha256: "76e4ab092c1b240d31177bb64d2b0bea43f43d0e23541ec866151b9f7b2490fa" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "12.0.12" | ||||||
|  |   permission_handler_apple: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: permission_handler_apple | ||||||
|  |       sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0 | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "9.4.5" | ||||||
|  |   permission_handler_html: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: permission_handler_html | ||||||
|  |       sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851 | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "0.1.3+2" | ||||||
|  |   permission_handler_platform_interface: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: permission_handler_platform_interface | ||||||
|  |       sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9 | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "4.2.3" | ||||||
|  |   permission_handler_windows: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: permission_handler_windows | ||||||
|  |       sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "0.2.1" | ||||||
|   petitparser: |   petitparser: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -588,10 +684,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: shared_preferences_web |       name: shared_preferences_web | ||||||
|       sha256: "3a293170d4d9403c3254ee05b84e62e8a9b3c5808ebd17de6a33fe9ea6457936" |       sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.4.0" |     version: "2.4.2" | ||||||
|   shared_preferences_windows: |   shared_preferences_windows: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -825,10 +921,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: web |       name: web | ||||||
|       sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" |       sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.5.1" |     version: "1.1.0" | ||||||
|   widget_to_marker: |   widget_to_marker: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|   | |||||||
| @@ -49,6 +49,8 @@ dependencies: | |||||||
|   flutter_svg: ^2.0.10+1 |   flutter_svg: ^2.0.10+1 | ||||||
|   url_launcher: ^6.3.0 |   url_launcher: ^6.3.0 | ||||||
|   flutter_launcher_icons: ^0.13.1 |   flutter_launcher_icons: ^0.13.1 | ||||||
|  |   permission_handler: ^11.3.1 | ||||||
|  |   geolocator: ^13.0.1 | ||||||
|  |  | ||||||
| dev_dependencies: | dev_dependencies: | ||||||
|   flutter_test: |   flutter_test: | ||||||
|   | |||||||
| @@ -6,9 +6,15 @@ | |||||||
|  |  | ||||||
| #include "generated_plugin_registrant.h" | #include "generated_plugin_registrant.h" | ||||||
|  |  | ||||||
|  | #include <geolocator_windows/geolocator_windows.h> | ||||||
|  | #include <permission_handler_windows/permission_handler_windows_plugin.h> | ||||||
| #include <url_launcher_windows/url_launcher_windows.h> | #include <url_launcher_windows/url_launcher_windows.h> | ||||||
|  |  | ||||||
| void RegisterPlugins(flutter::PluginRegistry* registry) { | void RegisterPlugins(flutter::PluginRegistry* registry) { | ||||||
|  |   GeolocatorWindowsRegisterWithRegistrar( | ||||||
|  |       registry->GetRegistrarForPlugin("GeolocatorWindows")); | ||||||
|  |   PermissionHandlerWindowsPluginRegisterWithRegistrar( | ||||||
|  |       registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); | ||||||
|   UrlLauncherWindowsRegisterWithRegistrar( |   UrlLauncherWindowsRegisterWithRegistrar( | ||||||
|       registry->GetRegistrarForPlugin("UrlLauncherWindows")); |       registry->GetRegistrarForPlugin("UrlLauncherWindows")); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,6 +3,8 @@ | |||||||
| # | # | ||||||
|  |  | ||||||
| list(APPEND FLUTTER_PLUGIN_LIST | list(APPEND FLUTTER_PLUGIN_LIST | ||||||
|  |   geolocator_windows | ||||||
|  |   permission_handler_windows | ||||||
|   url_launcher_windows |   url_launcher_windows | ||||||
| ) | ) | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user