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 | ||||
							
								
								
									
										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,64 @@ | ||||
| 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); | ||||
|  | ||||
| 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: TextButtonThemeData( | ||||
|     style: TextButton.styleFrom( | ||||
|       backgroundColor: PRIMARY_COLOR, | ||||
|       textStyle: TextStyle( | ||||
|         color: Colors.black, | ||||
|       ), | ||||
|     ), | ||||
|   ), | ||||
|  | ||||
|   iconButtonTheme: IconButtonThemeData( | ||||
|     style: ButtonStyle( | ||||
|       backgroundColor: MaterialStateProperty.all(PRIMARY_COLOR), | ||||
|     ) | ||||
|   ), | ||||
|    | ||||
|   buttonTheme: ButtonThemeData( | ||||
|     buttonColor: PRIMARY_COLOR, | ||||
|     textTheme: ButtonTextTheme.primary, | ||||
|   ), | ||||
|  | ||||
|    | ||||
| ); | ||||
|  | ||||
|  | ||||
| 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 | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -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")) | ||||
|   | ||||
| @@ -79,7 +79,7 @@ class _MapWidgetState extends State<MapWidget> { | ||||
|       cloudMapId: MAP_ID, | ||||
|       mapToolbarEnabled: false, | ||||
|       zoomControlsEnabled: false, | ||||
|  | ||||
|       myLocationEnabled: true, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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( | ||||
| @@ -88,21 +84,21 @@ 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, | ||||
|                             style: theme.iconButtonTheme.style, | ||||
|                             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, | ||||
|                               style: theme.iconButtonTheme.style, | ||||
|                               onPressed: () {}, | ||||
|                               icon: Icon(Icons.hourglass_bottom), | ||||
|                               label: Text('${widget.landmark.duration!.inMinutes} minutes'), | ||||
|                             ), | ||||
|                           if (widget.landmark.websiteURL != null) | ||||
|                             TextButton.icon( | ||||
|                               style: buttonStyle, | ||||
|                               style: theme.iconButtonTheme.style, | ||||
|                               onPressed: () async { | ||||
|                                 // open a browser with the website link | ||||
|                                 await launchUrl(Uri.parse(widget.landmark.websiteURL!)); | ||||
| @@ -112,7 +108,7 @@ class _LandmarkCardState extends State<LandmarkCard> { | ||||
|                             ), | ||||
|                           if (widget.landmark.wikipediaURL != null) | ||||
|                             TextButton.icon( | ||||
|                               style: buttonStyle, | ||||
|                               style: theme.iconButtonTheme.style, | ||||
|                               onPressed: () async { | ||||
|                                 // open a browser with the wikipedia link | ||||
|                                 await launchUrl(Uri.parse(widget.landmark.wikipediaURL!)); | ||||
|   | ||||
| @@ -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(); | ||||
|           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, preferences); | ||||
|       fetchTrip(trip, widget.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, | ||||
|                 ); | ||||
|     } | ||||
|             }, | ||||
|           ) | ||||
|         ); | ||||
|          | ||||
|       }  | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -6,9 +6,12 @@ import 'dart:developer'; | ||||
|  | ||||
| import 'package:anyway/structs/trip.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
|  | ||||
| class NewTripLocationSearch extends StatefulWidget { | ||||
|   Future<SharedPreferences> prefs = SharedPreferences.getInstance(); | ||||
|   Trip trip; | ||||
|    | ||||
|   NewTripLocationSearch( | ||||
|     this.trip, | ||||
|   ); | ||||
| @@ -45,20 +48,51 @@ class _NewTripLocationSearchState extends State<NewTripLocationSearch> { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return SearchBar( | ||||
|   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( | ||||
|     trailing: [ | ||||
|       ElevatedButton( | ||||
|         onPressed: () { | ||||
|           setTripLocation(_controller.text); | ||||
|         }, | ||||
|         child: Text('Search'), | ||||
|       ),] | ||||
|       ) | ||||
|     ] | ||||
|   ); | ||||
|  | ||||
|  | ||||
|   late Widget useCurrentLocationButton = ElevatedButton( | ||||
|     onPressed: () { | ||||
|       // setTripLocation(location); | ||||
|       // TODO: get current location | ||||
|     }, | ||||
|     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,4 +1,3 @@ | ||||
|  | ||||
| // A map that allows the user to select a location for a new trip. | ||||
| import 'dart:developer'; | ||||
|  | ||||
| @@ -22,7 +21,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, | ||||
|   ); | ||||
| @@ -70,7 +69,6 @@ class _NewTripMapState extends State<NewTripMap> { | ||||
|   } | ||||
|  | ||||
|  | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     widget.trip.addListener(updateTripDetails); | ||||
| @@ -82,6 +80,8 @@ class _NewTripMapState extends State<NewTripMap> { | ||||
|       cloudMapId: MAP_ID, | ||||
|       mapToolbarEnabled: false, | ||||
|       zoomControlsEnabled: false, | ||||
|       // TODO: should be loaded from the sharedprefs | ||||
|       myLocationEnabled: true, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										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,3 +1,4 @@ | ||||
| import 'package:anyway/constants.dart'; | ||||
| import 'package:anyway/structs/landmark.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
|  | ||||
| @@ -51,9 +52,7 @@ 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), | ||||
|             ), | ||||
|   | ||||
| @@ -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), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										114
									
								
								frontend/lib/pages/new_trip_preferences.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								frontend/lib/pages/new_trip_preferences.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| 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), | ||||
|           //   ) | ||||
|           // ), | ||||
|           Center( | ||||
|             child: FutureBuilder( | ||||
|               future: widget.trip.cityName, | ||||
|               builder: (context, snapshot) => Text( | ||||
|                 'New trip to ${snapshot.hasData ? snapshot.data! : "..."}', | ||||
|                 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('Tell us about your ideal trip.', style: TextStyle(fontSize: 18)) | ||||
|             ), | ||||
|           ), | ||||
|  | ||||
|           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('How long should the trip be?'), | ||||
|         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(); | ||||
|                 }); | ||||
|               }, | ||||
|             ) | ||||
|           ), | ||||
|           margin: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0), | ||||
|           shadowColor: Colors.grey, | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     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); | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										177
									
								
								frontend/lib/pages/settings.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								frontend/lib/pages/settings.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,177 @@ | ||||
| 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; | ||||
| bool useLocation = 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() { | ||||
|     return Row( | ||||
|       children: [ | ||||
|         Text('Use location services'), | ||||
|         // white space | ||||
|         Spacer(), | ||||
|         Switch( | ||||
|           value: useLocation, | ||||
|           onChanged: (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')) | ||||
|                 ); | ||||
|                 setState(() { | ||||
|                   useLocation = newValue!; | ||||
|                 }); | ||||
|                 SharedPreferences.getInstance().then( | ||||
|                   (SharedPreferences prefs) { | ||||
|                     prefs.setBool('useLocation', useLocation); | ||||
|                   } | ||||
|                 ); | ||||
|               }) | ||||
|               .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)); | ||||
|             } | ||||
|           ) | ||||
|         ], | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -1,5 +1,4 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
|  | ||||
|  | ||||
| class SinglePreference { | ||||
| @@ -20,16 +19,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; | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -65,38 +54,15 @@ class UserPreferences { | ||||
|     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; | ||||
|  | ||||
|   | ||||
| @@ -496,6 +496,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: | ||||
|   | ||||
| @@ -49,6 +49,7 @@ dependencies: | ||||
|   flutter_svg: ^2.0.10+1 | ||||
|   url_launcher: ^6.3.0 | ||||
|   flutter_launcher_icons: ^0.13.1 | ||||
|   permission_handler: ^11.3.1 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
|   | ||||
| @@ -6,9 +6,12 @@ | ||||
|  | ||||
| #include "generated_plugin_registrant.h" | ||||
|  | ||||
| #include <permission_handler_windows/permission_handler_windows_plugin.h> | ||||
| #include <url_launcher_windows/url_launcher_windows.h> | ||||
|  | ||||
| void RegisterPlugins(flutter::PluginRegistry* registry) { | ||||
|   PermissionHandlerWindowsPluginRegisterWithRegistrar( | ||||
|       registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); | ||||
|   UrlLauncherWindowsRegisterWithRegistrar( | ||||
|       registry->GetRegistrarForPlugin("UrlLauncherWindows")); | ||||
| } | ||||
|   | ||||
| @@ -3,6 +3,7 @@ | ||||
| # | ||||
|  | ||||
| list(APPEND FLUTTER_PLUGIN_LIST | ||||
|   permission_handler_windows | ||||
|   url_launcher_windows | ||||
| ) | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user