Usability and styling #24
| @@ -1,6 +1,9 @@ | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <!-- Required to fetch data from the 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 | ||||
|         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 { | ||||
|     id "dev.flutter.flutter-plugin-loader" version "1.0.0" | ||||
|     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" | ||||
|   | ||||
							
								
								
									
										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'; | ||||
|  | ||||
| String API_URL_BASE = 'https://anyway.anydev.info'; | ||||
| String PRIVACY_URL = 'https://anydev.info/privacy'; | ||||
|  | ||||
| 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:anyway/constants.dart'; | ||||
| @@ -6,10 +7,9 @@ import 'package:anyway/structs/trip.dart'; | ||||
| import 'package:anyway/modules/trips_saved_list.dart'; | ||||
| import 'package:anyway/utils/load_trips.dart'; | ||||
|  | ||||
| import 'package:anyway/pages/new_trip.dart'; | ||||
| import 'package:anyway/pages/new_trip_location.dart'; | ||||
| import 'package:anyway/pages/current_trip.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") { | ||||
|       currentView = OnboardingPage(); | ||||
|     } else if (widget.mainScreen == "profile") { | ||||
|       currentView = ProfilePage(); | ||||
|     } else if (widget.mainScreen == "settings") { | ||||
|       currentView = SettingsPage(); | ||||
|     } | ||||
|  | ||||
|     final ThemeData theme = Theme.of(context); | ||||
|      | ||||
|     return Scaffold( | ||||
|       appBar: AppBar(title: Text(APP_NAME)), | ||||
|       body: Center(child: currentView), | ||||
|       drawer: Drawer( | ||||
|         child: Column( | ||||
|           children: [ | ||||
|             DrawerHeader( | ||||
|             Container( | ||||
|               decoration: BoxDecoration( | ||||
|                 gradient: LinearGradient(colors: [Colors.red, Colors.yellow]) | ||||
|                 gradient: APP_GRADIENT, | ||||
|               ), | ||||
|               height: 150, | ||||
|               child: Center( | ||||
|                 child: Text( | ||||
|                   APP_NAME, | ||||
|                   style: TextStyle( | ||||
|                     color: Colors.grey[800], | ||||
|                     color: Colors.white, | ||||
|                     fontSize: 24, | ||||
|                     fontWeight: FontWeight.bold, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|  | ||||
|             ListTile( | ||||
|               title: const Text('Your Trips'), | ||||
|               leading: const Icon(Icons.map), | ||||
| @@ -130,7 +130,7 @@ class _BasePageState extends State<BasePage> { | ||||
|               }, | ||||
|               child: const Text('Clear trips'), | ||||
|             ), | ||||
|             const Divider(), | ||||
|             const Divider(indent: 10, endIndent: 10), | ||||
|             ListTile( | ||||
|               title: const Text('How to use'), | ||||
|               leading: Icon(Icons.help), | ||||
| @@ -148,11 +148,11 @@ class _BasePageState extends State<BasePage> { | ||||
|             ListTile( | ||||
|               title: const Text('Settings'), | ||||
|               leading: const Icon(Icons.settings), | ||||
|               selected: widget.mainScreen == "profile", | ||||
|               selected: widget.mainScreen == "settings", | ||||
|               onTap: () { | ||||
|                 Navigator.of(context).push( | ||||
|                   MaterialPageRoute( | ||||
|                     builder: (context) => BasePage(mainScreen: "profile") | ||||
|                     builder: (context) => BasePage(mainScreen: "settings") | ||||
|                   ) | ||||
|                 ); | ||||
|               }, | ||||
|   | ||||
| @@ -15,7 +15,7 @@ class App extends StatelessWidget { | ||||
|     return MaterialApp( | ||||
|       title: APP_NAME, | ||||
|       home: BasePage(mainScreen: "map"), | ||||
|       theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.red[600]), | ||||
|       theme: APP_THEME, | ||||
|       scaffoldMessengerKey: rootScaffoldMessengerKey | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import 'dart:developer'; | ||||
|  | ||||
| import 'package:anyway/constants.dart'; | ||||
| import 'package:anyway/structs/trip.dart'; | ||||
| import 'package:auto_size_text/auto_size_text.dart'; | ||||
|  | ||||
| @@ -20,8 +21,12 @@ class Greeter extends StatefulWidget { | ||||
| class _GreeterState extends State<Greeter> { | ||||
|    | ||||
|   Widget greeterBuilder (BuildContext context, Widget? child) { | ||||
|     ThemeData theme = Theme.of(context); | ||||
|     TextStyle greeterStyle = TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24); | ||||
|     final Shader textGradient = APP_GRADIENT.createShader(Rect.fromLTWH(0.0, 0.0, 200.0, 70.0)); | ||||
|     TextStyle greeterStyle = TextStyle( | ||||
|       foreground: Paint()..shader = textGradient, | ||||
|       fontWeight: FontWeight.bold, | ||||
|       fontSize: 26 | ||||
|     ); | ||||
|  | ||||
|     Widget topGreeter; | ||||
|  | ||||
| @@ -91,26 +96,10 @@ class _GreeterState extends State<Greeter> { | ||||
|     } | ||||
|  | ||||
|     return Center( | ||||
|       child: Column( | ||||
|         children: [ | ||||
|           // Padding(padding: EdgeInsets.only(top: 20)), | ||||
|           topGreeter, | ||||
|           Padding( | ||||
|             padding: EdgeInsets.all(20), | ||||
|             child: bottomGreeter | ||||
|           ), | ||||
|         ], | ||||
|       ) | ||||
|       child: topGreeter, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   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 | ||||
|   Widget build(BuildContext context) { | ||||
|   | ||||
| @@ -16,7 +16,7 @@ List<Widget> landmarksList(Trip trip) { | ||||
|  | ||||
|   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( | ||||
|       const Text("No landmarks in this trip"), | ||||
|     ); | ||||
| @@ -32,7 +32,6 @@ List<Widget> landmarksList(Trip trip) { | ||||
|         onDismissed: (direction) { | ||||
|           log('Removing ${landmark.name}'); | ||||
|           trip.removeLandmark(landmark); | ||||
|           // Then show a snackbar | ||||
|  | ||||
|           rootScaffoldMessengerKey.currentState!.showSnackBar( | ||||
|             SnackBar(content: Text("We won't show ${landmark.name} again")) | ||||
|   | ||||
| @@ -1,27 +1,28 @@ | ||||
| import 'dart:collection'; | ||||
|  | ||||
| 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:anyway/structs/landmark.dart'; | ||||
| import 'package:anyway/structs/trip.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'; | ||||
|  | ||||
|  | ||||
| class MapWidget extends StatefulWidget { | ||||
| class CurrentTripMap extends StatefulWidget { | ||||
|  | ||||
|   final Trip? trip; | ||||
|  | ||||
|   MapWidget({ | ||||
|   CurrentTripMap({ | ||||
|     this.trip | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   State<MapWidget> createState() => _MapWidgetState(); | ||||
|   State<CurrentTripMap> createState() => _CurrentTripMapState(); | ||||
| } | ||||
|  | ||||
| class _MapWidgetState extends State<MapWidget> { | ||||
| class _CurrentTripMapState extends State<CurrentTripMap> { | ||||
|   late GoogleMapController mapController; | ||||
|  | ||||
|   CameraPosition _cameraPosition = CameraPosition( | ||||
| @@ -67,9 +68,27 @@ class _MapWidgetState extends State<MapWidget> { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     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( | ||||
|       onMapCreated: _onMapCreated, | ||||
|       initialCameraPosition: _cameraPosition, | ||||
| @@ -79,7 +98,9 @@ class _MapWidgetState extends State<MapWidget> { | ||||
|       cloudMapId: MAP_ID, | ||||
|       mapToolbarEnabled: 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:auto_size_text/auto_size_text.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| @@ -8,6 +9,13 @@ Widget saveButton(Trip trip) => ElevatedButton( | ||||
|   onPressed: () async { | ||||
|     SharedPreferences prefs = await SharedPreferences.getInstance(); | ||||
|     trip.toPrefs(prefs); | ||||
|     rootScaffoldMessengerKey.currentState!.showSnackBar( | ||||
|       SnackBar( | ||||
|         content: Text('Trip saved'), | ||||
|         duration: Duration(seconds: 2), | ||||
|         dismissDirection: DismissDirection.horizontal | ||||
|       ) | ||||
|     ); | ||||
|   }, | ||||
|   child: SizedBox( | ||||
|     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 | ||||
|   Widget build(BuildContext context) { | ||||
|     ThemeData theme = Theme.of(context); | ||||
|     ButtonStyle buttonStyle = TextButton.styleFrom( | ||||
|       backgroundColor: Colors.orange, | ||||
|       fixedSize: Size.fromHeight(20) | ||||
|     ); | ||||
|     return Container( | ||||
|       height: 160, | ||||
|       child: Card( | ||||
| @@ -40,7 +36,7 @@ class _LandmarkCardState extends State<LandmarkCard> { | ||||
|               width: 160, | ||||
|               child: CachedNetworkImage( | ||||
|                 imageUrl: widget.landmark.imageURL ?? '', | ||||
|                 placeholder: (context, url) => CircularProgressIndicator(), | ||||
|                 placeholder: (context, url) => Center(child: CircularProgressIndicator()), | ||||
|                 errorWidget: (context, error, stackTrace) => Icon(Icons.question_mark_outlined), | ||||
|                 // TODO: make this a switch statement to load a placeholder if null | ||||
|                 // 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 | ||||
|                         children: [ | ||||
|                           TextButton.icon( | ||||
|                             style: buttonStyle, | ||||
|                             onPressed: () {}, | ||||
|                             icon: widget.landmark.type.icon, | ||||
|                             label: Text(widget.landmark.type.name), | ||||
|                           ), | ||||
|                           if (widget.landmark.duration != null && widget.landmark.duration!.inMinutes > 0) | ||||
|                             TextButton.icon( | ||||
|                               style: buttonStyle, | ||||
|                               onPressed: () {}, | ||||
|                               icon: Icon(Icons.hourglass_bottom), | ||||
|                               label: Text('${widget.landmark.duration!.inMinutes} minutes'), | ||||
|                             ), | ||||
|                           if (widget.landmark.websiteURL != null) | ||||
|                             TextButton.icon( | ||||
|                               style: buttonStyle, | ||||
|                               onPressed: () async { | ||||
|                                 // open a browser with the website link | ||||
|                                 await launchUrl(Uri.parse(widget.landmark.websiteURL!)); | ||||
| @@ -112,7 +105,6 @@ class _LandmarkCardState extends State<LandmarkCard> { | ||||
|                             ), | ||||
|                           if (widget.landmark.wikipediaURL != null) | ||||
|                             TextButton.icon( | ||||
|                               style: buttonStyle, | ||||
|                               onPressed: () async { | ||||
|                                 // open a browser with the wikipedia link | ||||
|                                 await launchUrl(Uri.parse(widget.landmark.wikipediaURL!)); | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:anyway/constants.dart'; | ||||
| import 'package:anyway/structs/landmark.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| 
 | ||||
| @@ -16,21 +17,9 @@ class ThemedMarker extends StatelessWidget { | ||||
|   Widget build(BuildContext context) { | ||||
|     // This returns an outlined circle, with an icon corresponding to the landmark type | ||||
|     // As a small dot, the number of the landmark is displayed in the top right | ||||
|     Icon icon; | ||||
|     if (landmark.type == sightseeing) { | ||||
|       icon = Icon(Icons.church, color: Colors.black, size: 50); | ||||
|     } else if (landmark.type == nature) { | ||||
|       icon = Icon(Icons.park, color: Colors.black, size: 50); | ||||
|     } else if (landmark.type == shopping) { | ||||
|       icon = Icon(Icons.shopping_cart, color: Colors.black, size: 50); | ||||
|     } else if (landmark.type == start || landmark.type == finish) { | ||||
|       icon = Icon(Icons.flag, color: Colors.black, size: 50); | ||||
|     } else { | ||||
|       icon = Icon(Icons.location_on, color: Colors.black, size: 50); | ||||
|     } | ||||
| 
 | ||||
|     Widget? positionIndicator; | ||||
|     if (landmark.type != start && landmark.type != finish) { | ||||
|     if (landmark.type != typeStart && landmark.type != typeFinish) { | ||||
|       positionIndicator = Positioned( | ||||
|         top: 0, | ||||
|         right: 0, | ||||
| @@ -51,14 +40,14 @@ class ThemedMarker extends StatelessWidget { | ||||
|         children: [ | ||||
|           Container( | ||||
|             decoration: BoxDecoration( | ||||
|               gradient: LinearGradient( | ||||
|                 colors: [Colors.red, Colors.yellow] | ||||
|               ), | ||||
|               gradient: APP_GRADIENT, | ||||
|               shape: BoxShape.circle, | ||||
|               border: Border.all(color: Colors.black, width: 5), | ||||
|             ), | ||||
|             padding: EdgeInsets.all(5), | ||||
|             child: icon | ||||
|             width: 70, | ||||
|             height: 70, | ||||
|             padding: const EdgeInsets.all(5), | ||||
|             child: Icon(landmark.type.icon.icon, size: 50), | ||||
|           ), | ||||
|           if (positionIndicator != null) positionIndicator, | ||||
|         ], | ||||
| @@ -1,4 +1,5 @@ | ||||
| import 'package:anyway/layout.dart'; | ||||
| import 'package:anyway/main.dart'; | ||||
| import 'package:anyway/structs/preferences.dart'; | ||||
| import 'package:anyway/structs/trip.dart'; | ||||
| import 'package:anyway/utils/fetch_trip.dart'; | ||||
| @@ -8,8 +9,12 @@ import 'package:flutter/material.dart'; | ||||
|  | ||||
| class NewTripButton extends StatefulWidget { | ||||
|   final Trip trip; | ||||
|   final UserPreferences preferences; | ||||
|  | ||||
|   const NewTripButton({required this.trip}); | ||||
|   const NewTripButton({ | ||||
|     required this.trip, | ||||
|     required this.preferences, | ||||
|     }); | ||||
|  | ||||
|   @override | ||||
|   State<NewTripButton> createState() => _NewTripButtonState(); | ||||
| @@ -23,42 +28,39 @@ class _NewTripButtonState extends State<NewTripButton> { | ||||
|       listenable: widget.trip, | ||||
|       builder: (BuildContext context, Widget? child) { | ||||
|         if (widget.trip.landmarks.isEmpty){ | ||||
|           // Fallback if the trip setup is lagging behind | ||||
|           // This should in theory never happen | ||||
|           return Container(); | ||||
|         } | ||||
|         return FloatingActionButton.extended( | ||||
|           onPressed: () async { | ||||
|             Future<UserPreferences> preferences = loadUserPreferences(); | ||||
|             Trip trip = widget.trip; | ||||
|             fetchTrip(trip, preferences); | ||||
|             Navigator.of(context).push( | ||||
|               MaterialPageRoute( | ||||
|                 builder: (context) => BasePage(mainScreen: "map", trip: trip) | ||||
|               ) | ||||
|             ); | ||||
|           }, | ||||
|           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, | ||||
|                 ); | ||||
|               } | ||||
|             }, | ||||
|           ) | ||||
|           onPressed: onPressed, | ||||
|           icon: const Icon(Icons.add), | ||||
|           label: AutoSizeText('Start planning!'), | ||||
|         );  | ||||
|          | ||||
|       } | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   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:flutter/material.dart'; | ||||
| import 'package:geolocator/geolocator.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
|  | ||||
| class NewTripLocationSearch extends StatefulWidget { | ||||
|   Future<SharedPreferences> prefs = SharedPreferences.getInstance(); | ||||
|   Trip trip; | ||||
|    | ||||
|   NewTripLocationSearch( | ||||
|     this.trip, | ||||
|   ); | ||||
| @@ -39,26 +43,66 @@ class _NewTripLocationSearchState extends State<NewTripLocationSearch> { | ||||
|           uuid: 'pending', | ||||
|           name: query, | ||||
|           location: [location.latitude, location.longitude], | ||||
|           type: start | ||||
|           type: typeStart | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return SearchBar( | ||||
|       hintText: 'Enter a city name or long press on the map.', | ||||
|       onSubmitted: setTripLocation, | ||||
|       controller: _controller, | ||||
|       leading: Icon(Icons.search), | ||||
|       trailing: [ElevatedButton( | ||||
|   late Widget locationSearchBar = SearchBar( | ||||
|     hintText: 'Enter a city name or long press on the map.', | ||||
|     onSubmitted: setTripLocation, | ||||
|     controller: _controller, | ||||
|     leading: Icon(Icons.search), | ||||
|     trailing: [ | ||||
|       ElevatedButton( | ||||
|         onPressed: () { | ||||
|           setTripLocation(_controller.text); | ||||
|         }, | ||||
|         child: Text('Search'), | ||||
|       ),] | ||||
|       ) | ||||
|     ] | ||||
|   ); | ||||
|  | ||||
|  | ||||
|   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. | ||||
| import 'dart:developer'; | ||||
|  | ||||
| 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/trip.dart'; | ||||
| import 'package:flutter/material.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'; | ||||
|  | ||||
|  | ||||
| @@ -22,7 +22,7 @@ class NewTripMap extends StatefulWidget { | ||||
| } | ||||
|  | ||||
| class _NewTripMapState extends State<NewTripMap> { | ||||
|   final CameraPosition _cameraPosition = CameraPosition( | ||||
|   final CameraPosition _cameraPosition = const CameraPosition( | ||||
|     target: LatLng(48.8566, 2.3522), | ||||
|     zoom: 11.0, | ||||
|   ); | ||||
| @@ -37,7 +37,7 @@ class _NewTripMapState extends State<NewTripMap> { | ||||
|         uuid: 'pending', | ||||
|         name: 'start', | ||||
|         location: [location.latitude, location.longitude], | ||||
|         type: start | ||||
|         type: typeStart | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
| @@ -70,10 +70,26 @@ class _NewTripMapState extends State<NewTripMap> { | ||||
|   } | ||||
|  | ||||
|  | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     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( | ||||
|       onMapCreated: _onMapCreated, | ||||
|       initialCameraPosition: _cameraPosition, | ||||
| @@ -82,6 +98,8 @@ class _NewTripMapState extends State<NewTripMap> { | ||||
|       cloudMapId: MAP_ID, | ||||
|       mapToolbarEnabled: 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 | ||||
|   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 | ||||
|     Color currentColor = baseColor.withAlpha(baseColor.alpha - index * 30); | ||||
|     return Container( | ||||
|       color: currentColor, | ||||
|       alignment: Alignment.center, | ||||
|       child: Padding( | ||||
|         padding: EdgeInsets.all(20), | ||||
|         child: Column( | ||||
|           mainAxisAlignment: MainAxisAlignment.center, | ||||
|           children: [ | ||||
|             Text( | ||||
|               title, | ||||
|               style: TextStyle( | ||||
|                 fontSize: 24, | ||||
|                 fontWeight: FontWeight.bold, | ||||
|                 color: Colors.white, | ||||
|               ), | ||||
|             ), | ||||
|             Padding(padding: EdgeInsets.only(top: 20)), | ||||
| @@ -44,7 +47,8 @@ class OnboardingCard extends StatelessWidget { | ||||
|                 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:sliding_up_panel/sliding_up_panel.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_panel.dart'; | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -27,16 +26,21 @@ class _TripPageState extends State<TripPage> { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return SlidingUpPanel( | ||||
|         panelBuilder: (sc) => _panelFull(sc), | ||||
|         // collapsed: _floatingCollapsed(), | ||||
|         body: MapWidget(trip: widget.trip), | ||||
|         // renderPanelSheet: false, | ||||
|         // backdropEnabled: true, | ||||
|         maxHeight: MediaQuery.of(context).size.height * 0.8, | ||||
|         padding: EdgeInsets.only(left: 10, right: 10, top: 25, bottom: 10), | ||||
|         // panelSnapping: false, | ||||
|         borderRadius: BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)), | ||||
|         boxShadow: [ | ||||
|         // use panelBuilder instead of panel so that we can reuse the scrollcontroller for the listview | ||||
|         panelBuilder: (scrollcontroller) => CurrentTripPanel(controller: scrollcontroller, trip: widget.trip), | ||||
|         // using collapsed and panelBuilder seems to show both at the same time, so we include the greeter in the panelBuilder | ||||
|         // collapsed: Greeter(trip: widget.trip), | ||||
|         body: CurrentTripMap(trip: widget.trip), | ||||
|         minHeight: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT, | ||||
|         maxHeight: MediaQuery.of(context).size.height * TRIP_PANEL_MAX_HEIGHT, | ||||
|         // padding in this context is annoying: it offsets the notion of vertical alignment. | ||||
|         // children that want to be centered vertically need to have their size adjusted by 2x the padding | ||||
|         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( | ||||
|             blurRadius: 20.0, | ||||
|             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/structs/landmark.dart'; | ||||
| import 'package:anyway/modules/new_trip_options_button.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/modules/new_trip_location_search.dart'; | ||||
| import 'package:anyway/modules/new_trip_map.dart'; | ||||
| @@ -19,7 +15,6 @@ class NewTripPage extends StatefulWidget { | ||||
| } | ||||
| 
 | ||||
| class _NewTripPageState extends State<NewTripPage> { | ||||
|   final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); | ||||
|   final TextEditingController latController = TextEditingController(); | ||||
|   final TextEditingController lonController = TextEditingController(); | ||||
|   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/pages/new_trip.dart'; | ||||
| import 'package:anyway/pages/new_trip_location.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
|  | ||||
| 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:shared_preferences/shared_preferences.dart'; | ||||
|  | ||||
| LandmarkType sightseeing = LandmarkType(name: 'sightseeing'); | ||||
| LandmarkType nature = LandmarkType(name: 'nature'); | ||||
| LandmarkType shopping = LandmarkType(name: 'shopping'); | ||||
| LandmarkType typeSightseeing = LandmarkType(name: 'sightseeing'); | ||||
| LandmarkType typeNature = LandmarkType(name: 'nature'); | ||||
| LandmarkType typeShopping = LandmarkType(name: 'shopping'); | ||||
| // LandmarkType museum = LandmarkType(name: 'Museum'); | ||||
| // LandmarkType restaurant = LandmarkType(name: 'Restaurant'); | ||||
| LandmarkType start = LandmarkType(name: 'start'); | ||||
| LandmarkType finish = LandmarkType(name: 'finish'); | ||||
| LandmarkType typeStart = LandmarkType(name: 'start'); | ||||
| LandmarkType typeFinish = LandmarkType(name: 'finish'); | ||||
|  | ||||
|  | ||||
| final class Landmark extends LinkedListEntry<Landmark>{ | ||||
| @@ -130,22 +130,22 @@ class LandmarkType { | ||||
|   LandmarkType({required this.name, this.icon = const Icon(Icons.location_on)}) { | ||||
|     switch (name) { | ||||
|       case 'sightseeing': | ||||
|         icon = Icon(Icons.church); | ||||
|         icon = const Icon(Icons.church); | ||||
|         break; | ||||
|       case 'nature': | ||||
|         icon = Icon(Icons.eco); | ||||
|         icon = const Icon(Icons.eco); | ||||
|         break; | ||||
|       case 'shopping': | ||||
|         icon = Icon(Icons.shopping_cart); | ||||
|         icon = const Icon(Icons.shopping_cart); | ||||
|         break; | ||||
|       case 'start': | ||||
|         icon = Icon(Icons.play_arrow); | ||||
|         icon = const Icon(Icons.play_arrow); | ||||
|         break; | ||||
|       case 'finish': | ||||
|         icon = Icon(Icons.flag); | ||||
|         icon = const Icon(Icons.flag); | ||||
|         break; | ||||
|       default: | ||||
|         icon = Icon(Icons.location_on); | ||||
|         icon = const Icon(Icons.location_on); | ||||
|     } | ||||
|   } | ||||
|   @override | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import 'package:anyway/structs/landmark.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
|  | ||||
|  | ||||
| class SinglePreference { | ||||
| @@ -20,16 +20,6 @@ class SinglePreference { | ||||
|     this.minVal = 0, | ||||
|     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", | ||||
|     description: "How much do you like sightseeing?", | ||||
|     value: 0, | ||||
|     icon: Icon(Icons.church), | ||||
|     icon: typeSightseeing.icon, | ||||
|   ); | ||||
|   SinglePreference shopping = SinglePreference( | ||||
|     name: "Shopping", | ||||
|     slug: "shopping", | ||||
|     description: "How much do you like shopping?", | ||||
|     value: 0, | ||||
|     icon: Icon(Icons.shopping_bag), | ||||
|     icon: typeShopping.icon, | ||||
|   ); | ||||
|   SinglePreference nature = SinglePreference( | ||||
|     name: "Nature", | ||||
|     slug: "nature", | ||||
|     description: "How much do you like nature?", | ||||
|     value: 0, | ||||
|     icon: Icon(Icons.landscape), | ||||
|     icon: typeNature.icon, | ||||
|   ); | ||||
|  | ||||
|   SinglePreference maxTime = SinglePreference( | ||||
|     name: "Trip duration", | ||||
|     slug: "duration", | ||||
|     description: "How long do you want your trip to be?", | ||||
|     description: "How long should your trip be?", | ||||
|     value: 30, | ||||
|     minVal: 30, | ||||
|     maxVal: 720, | ||||
|     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() { | ||||
|       // This is "opinionated" JSON, corresponding to the backend's expectations | ||||
|       return { | ||||
|         "sightseeing": {"type": "sightseeing", "score": sightseeing.value}, | ||||
|         "shopping": {"type": "shopping", "score": shopping.value}, | ||||
|         "nature": {"type": "nature", "score": nature.value}, | ||||
|         "max_time_minute": maxTime.value, | ||||
|         "detour_tolerance_minute": maxDetour.value | ||||
|         "max_time_minute": maxTime.value | ||||
|       }; | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| Future<UserPreferences> loadUserPreferences() async { | ||||
|   UserPreferences prefs = UserPreferences(); | ||||
|   await prefs.load(); | ||||
|   return prefs; | ||||
| } | ||||
| @@ -29,11 +29,10 @@ Dio dio = Dio( | ||||
|  | ||||
| fetchTrip( | ||||
|   Trip trip, | ||||
|   Future<UserPreferences> preferences, | ||||
|   UserPreferences preferences, | ||||
| ) async { | ||||
|   UserPreferences prefs = await preferences; | ||||
|   Map<String, dynamic> data = { | ||||
|     "preferences": prefs.toJson(), | ||||
|     "preferences": preferences.toJson(), | ||||
|     "start": trip.landmarks!.first.location, | ||||
|   }; | ||||
|   String dataString = jsonEncode(data); | ||||
| @@ -47,11 +46,16 @@ fetchTrip( | ||||
|   // handle errors | ||||
|   if (response.statusCode != 200) { | ||||
|     trip.updateUUID("error"); | ||||
|     if (response.data["detail"] != null) { | ||||
|       trip.updateError(response.data["detail"]); | ||||
|       log(response.data["detail"]); | ||||
|       // throw Exception(response.data["detail"]); | ||||
|     String errorDetail; | ||||
|     if (response.data.runtimeType == String) { | ||||
|       errorDetail = response.data; | ||||
|     } 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 { | ||||
|     Map<String, dynamic> json = response.data; | ||||
|  | ||||
|   | ||||
| @@ -5,12 +5,14 @@ | ||||
| import FlutterMacOS | ||||
| import Foundation | ||||
|  | ||||
| import geolocator_apple | ||||
| import path_provider_foundation | ||||
| import shared_preferences_foundation | ||||
| import sqflite | ||||
| import url_launcher_macos | ||||
|  | ||||
| func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { | ||||
|   GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) | ||||
|   PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) | ||||
|   SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) | ||||
|   SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) | ||||
|   | ||||
| @@ -264,6 +264,54 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     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: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -496,6 +544,54 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     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: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -588,10 +684,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: shared_preferences_web | ||||
|       sha256: "3a293170d4d9403c3254ee05b84e62e8a9b3c5808ebd17de6a33fe9ea6457936" | ||||
|       sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.4.0" | ||||
|     version: "2.4.2" | ||||
|   shared_preferences_windows: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -825,10 +921,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: web | ||||
|       sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" | ||||
|       sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.5.1" | ||||
|     version: "1.1.0" | ||||
|   widget_to_marker: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|   | ||||
| @@ -49,6 +49,8 @@ dependencies: | ||||
|   flutter_svg: ^2.0.10+1 | ||||
|   url_launcher: ^6.3.0 | ||||
|   flutter_launcher_icons: ^0.13.1 | ||||
|   permission_handler: ^11.3.1 | ||||
|   geolocator: ^13.0.1 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
|   | ||||
| @@ -6,9 +6,15 @@ | ||||
|  | ||||
| #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> | ||||
|  | ||||
| void RegisterPlugins(flutter::PluginRegistry* registry) { | ||||
|   GeolocatorWindowsRegisterWithRegistrar( | ||||
|       registry->GetRegistrarForPlugin("GeolocatorWindows")); | ||||
|   PermissionHandlerWindowsPluginRegisterWithRegistrar( | ||||
|       registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); | ||||
|   UrlLauncherWindowsRegisterWithRegistrar( | ||||
|       registry->GetRegistrarForPlugin("UrlLauncherWindows")); | ||||
| } | ||||
|   | ||||
| @@ -3,6 +3,8 @@ | ||||
| # | ||||
|  | ||||
| list(APPEND FLUTTER_PLUGIN_LIST | ||||
|   geolocator_windows | ||||
|   permission_handler_windows | ||||
|   url_launcher_windows | ||||
| ) | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user