Usability and styling #24
| @@ -1,6 +1,9 @@ | |||||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||||||
|     <!-- Required to fetch data from the internet. --> |     <!-- Required to fetch data from the internet. --> | ||||||
|     <uses-permission android:name="android.permission.INTERNET"/> |     <uses-permission android:name="android.permission.INTERNET"/> | ||||||
|  |     <!-- Required to show user location --> | ||||||
|  |     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> | ||||||
|  |     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> | ||||||
|  |  | ||||||
|     <application |     <application | ||||||
|         android:label="anyway" |         android:label="anyway" | ||||||
|   | |||||||
							
								
								
									
										90
									
								
								frontend/android/gradlew.bat
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										90
									
								
								frontend/android/gradlew.bat
									
									
									
									
										vendored
									
									
										Executable file
									
								
							| @@ -0,0 +1,90 @@ | |||||||
|  | @if "%DEBUG%" == "" @echo off | ||||||
|  | @rem ########################################################################## | ||||||
|  | @rem | ||||||
|  | @rem  Gradle startup script for Windows | ||||||
|  | @rem | ||||||
|  | @rem ########################################################################## | ||||||
|  |  | ||||||
|  | @rem Set local scope for the variables with windows NT shell | ||||||
|  | if "%OS%"=="Windows_NT" setlocal | ||||||
|  |  | ||||||
|  | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. | ||||||
|  | set DEFAULT_JVM_OPTS= | ||||||
|  |  | ||||||
|  | set DIRNAME=%~dp0 | ||||||
|  | if "%DIRNAME%" == "" set DIRNAME=. | ||||||
|  | set APP_BASE_NAME=%~n0 | ||||||
|  | set APP_HOME=%DIRNAME% | ||||||
|  |  | ||||||
|  | @rem Find java.exe | ||||||
|  | if defined JAVA_HOME goto findJavaFromJavaHome | ||||||
|  |  | ||||||
|  | set JAVA_EXE=java.exe | ||||||
|  | %JAVA_EXE% -version >NUL 2>&1 | ||||||
|  | if "%ERRORLEVEL%" == "0" goto init | ||||||
|  |  | ||||||
|  | echo. | ||||||
|  | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. | ||||||
|  | echo. | ||||||
|  | echo Please set the JAVA_HOME variable in your environment to match the | ||||||
|  | echo location of your Java installation. | ||||||
|  |  | ||||||
|  | goto fail | ||||||
|  |  | ||||||
|  | :findJavaFromJavaHome | ||||||
|  | set JAVA_HOME=%JAVA_HOME:"=% | ||||||
|  | set JAVA_EXE=%JAVA_HOME%/bin/java.exe | ||||||
|  |  | ||||||
|  | if exist "%JAVA_EXE%" goto init | ||||||
|  |  | ||||||
|  | echo. | ||||||
|  | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% | ||||||
|  | echo. | ||||||
|  | echo Please set the JAVA_HOME variable in your environment to match the | ||||||
|  | echo location of your Java installation. | ||||||
|  |  | ||||||
|  | goto fail | ||||||
|  |  | ||||||
|  | :init | ||||||
|  | @rem Get command-line arguments, handling Windowz variants | ||||||
|  |  | ||||||
|  | if not "%OS%" == "Windows_NT" goto win9xME_args | ||||||
|  | if "%@eval[2+2]" == "4" goto 4NT_args | ||||||
|  |  | ||||||
|  | :win9xME_args | ||||||
|  | @rem Slurp the command line arguments. | ||||||
|  | set CMD_LINE_ARGS= | ||||||
|  | set _SKIP=2 | ||||||
|  |  | ||||||
|  | :win9xME_args_slurp | ||||||
|  | if "x%~1" == "x" goto execute | ||||||
|  |  | ||||||
|  | set CMD_LINE_ARGS=%* | ||||||
|  | goto execute | ||||||
|  |  | ||||||
|  | :4NT_args | ||||||
|  | @rem Get arguments from the 4NT Shell from JP Software | ||||||
|  | set CMD_LINE_ARGS=%$ | ||||||
|  |  | ||||||
|  | :execute | ||||||
|  | @rem Setup the command line | ||||||
|  |  | ||||||
|  | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar | ||||||
|  |  | ||||||
|  | @rem Execute Gradle | ||||||
|  | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% | ||||||
|  |  | ||||||
|  | :end | ||||||
|  | @rem End local scope for the variables with windows NT shell | ||||||
|  | if "%ERRORLEVEL%"=="0" goto mainEnd | ||||||
|  |  | ||||||
|  | :fail | ||||||
|  | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of | ||||||
|  | rem the _cmd.exe /c_ return code! | ||||||
|  | if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 | ||||||
|  | exit /b 1 | ||||||
|  |  | ||||||
|  | :mainEnd | ||||||
|  | if "%OS%"=="Windows_NT" endlocal | ||||||
|  |  | ||||||
|  | :omega | ||||||
							
								
								
									
										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'; | const String APP_NAME = 'AnyWay'; | ||||||
|  |  | ||||||
| String API_URL_BASE = 'https://anyway.anydev.info'; | String API_URL_BASE = 'https://anyway.anydev.info'; | ||||||
| String PRIVACY_URL = 'https://anydev.info/privacy'; | String PRIVACY_URL = 'https://anydev.info/privacy'; | ||||||
|  |  | ||||||
| const String MAP_ID = '41c21ac9b81dbfd8'; | const String MAP_ID = '41c21ac9b81dbfd8'; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | const Color GRADIENT_START = Color(0xFFF9B208); | ||||||
|  | const Color GRADIENT_END = Color(0xFFE72E77); | ||||||
|  |  | ||||||
|  | const Color PRIMARY_COLOR = Color(0xFFF38F1A); | ||||||
|  |  | ||||||
|  | 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:flutter/material.dart'; | ||||||
|  |  | ||||||
| import 'package:anyway/constants.dart'; | import 'package:anyway/constants.dart'; | ||||||
| @@ -6,10 +7,9 @@ import 'package:anyway/structs/trip.dart'; | |||||||
| import 'package:anyway/modules/trips_saved_list.dart'; | import 'package:anyway/modules/trips_saved_list.dart'; | ||||||
| import 'package:anyway/utils/load_trips.dart'; | import 'package:anyway/utils/load_trips.dart'; | ||||||
|  |  | ||||||
| import 'package:anyway/pages/new_trip.dart'; | import 'package:anyway/pages/new_trip_location.dart'; | ||||||
| import 'package:anyway/pages/current_trip.dart'; | import 'package:anyway/pages/current_trip.dart'; | ||||||
| import 'package:anyway/pages/onboarding.dart'; | import 'package:anyway/pages/onboarding.dart'; | ||||||
| import 'package:anyway/pages/profile.dart'; |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -62,7 +62,7 @@ class _BasePageState extends State<BasePage> { | |||||||
|                         ) |                         ) | ||||||
|                       ); |                       ); | ||||||
|                     }, |                     }, | ||||||
|                     label: Text("Plan a trip now"), |                     label: Text("Plan a trip"), | ||||||
|                   ), |                   ), | ||||||
|                 ); |                 ); | ||||||
|               } |               } | ||||||
| @@ -74,33 +74,33 @@ class _BasePageState extends State<BasePage> { | |||||||
|       } |       } | ||||||
|     } else if (widget.mainScreen == "tutorial") { |     } else if (widget.mainScreen == "tutorial") { | ||||||
|       currentView = OnboardingPage(); |       currentView = OnboardingPage(); | ||||||
|     } else if (widget.mainScreen == "profile") { |     } else if (widget.mainScreen == "settings") { | ||||||
|       currentView = ProfilePage(); |       currentView = SettingsPage(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     final ThemeData theme = Theme.of(context); |  | ||||||
|      |  | ||||||
|     return Scaffold( |     return Scaffold( | ||||||
|       appBar: AppBar(title: Text(APP_NAME)), |       appBar: AppBar(title: Text(APP_NAME)), | ||||||
|       body: Center(child: currentView), |       body: Center(child: currentView), | ||||||
|       drawer: Drawer( |       drawer: Drawer( | ||||||
|         child: Column( |         child: Column( | ||||||
|           children: [ |           children: [ | ||||||
|             DrawerHeader( |             Container( | ||||||
|               decoration: BoxDecoration( |               decoration: BoxDecoration( | ||||||
|                 gradient: LinearGradient(colors: [Colors.red, Colors.yellow]) |                 gradient: APP_GRADIENT, | ||||||
|               ), |               ), | ||||||
|  |               height: 150, | ||||||
|               child: Center( |               child: Center( | ||||||
|                 child: Text( |                 child: Text( | ||||||
|                   APP_NAME, |                   APP_NAME, | ||||||
|                   style: TextStyle( |                   style: TextStyle( | ||||||
|                     color: Colors.grey[800], |                     color: Colors.white, | ||||||
|                     fontSize: 24, |                     fontSize: 24, | ||||||
|                     fontWeight: FontWeight.bold, |                     fontWeight: FontWeight.bold, | ||||||
|                   ), |                   ), | ||||||
|                 ), |                 ), | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
|  |  | ||||||
|             ListTile( |             ListTile( | ||||||
|               title: const Text('Your Trips'), |               title: const Text('Your Trips'), | ||||||
|               leading: const Icon(Icons.map), |               leading: const Icon(Icons.map), | ||||||
| @@ -130,7 +130,7 @@ class _BasePageState extends State<BasePage> { | |||||||
|               }, |               }, | ||||||
|               child: const Text('Clear trips'), |               child: const Text('Clear trips'), | ||||||
|             ), |             ), | ||||||
|             const Divider(), |             const Divider(indent: 10, endIndent: 10), | ||||||
|             ListTile( |             ListTile( | ||||||
|               title: const Text('How to use'), |               title: const Text('How to use'), | ||||||
|               leading: Icon(Icons.help), |               leading: Icon(Icons.help), | ||||||
| @@ -148,11 +148,11 @@ class _BasePageState extends State<BasePage> { | |||||||
|             ListTile( |             ListTile( | ||||||
|               title: const Text('Settings'), |               title: const Text('Settings'), | ||||||
|               leading: const Icon(Icons.settings), |               leading: const Icon(Icons.settings), | ||||||
|               selected: widget.mainScreen == "profile", |               selected: widget.mainScreen == "settings", | ||||||
|               onTap: () { |               onTap: () { | ||||||
|                 Navigator.of(context).push( |                 Navigator.of(context).push( | ||||||
|                   MaterialPageRoute( |                   MaterialPageRoute( | ||||||
|                     builder: (context) => BasePage(mainScreen: "profile") |                     builder: (context) => BasePage(mainScreen: "settings") | ||||||
|                   ) |                   ) | ||||||
|                 ); |                 ); | ||||||
|               }, |               }, | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ class App extends StatelessWidget { | |||||||
|     return MaterialApp( |     return MaterialApp( | ||||||
|       title: APP_NAME, |       title: APP_NAME, | ||||||
|       home: BasePage(mainScreen: "map"), |       home: BasePage(mainScreen: "map"), | ||||||
|       theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.red[600]), |       theme: APP_THEME, | ||||||
|       scaffoldMessengerKey: rootScaffoldMessengerKey |       scaffoldMessengerKey: rootScaffoldMessengerKey | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -32,7 +32,6 @@ List<Widget> landmarksList(Trip trip) { | |||||||
|         onDismissed: (direction) { |         onDismissed: (direction) { | ||||||
|           log('Removing ${landmark.name}'); |           log('Removing ${landmark.name}'); | ||||||
|           trip.removeLandmark(landmark); |           trip.removeLandmark(landmark); | ||||||
|           // Then show a snackbar |  | ||||||
|  |  | ||||||
|           rootScaffoldMessengerKey.currentState!.showSnackBar( |           rootScaffoldMessengerKey.currentState!.showSnackBar( | ||||||
|             SnackBar(content: Text("We won't show ${landmark.name} again")) |             SnackBar(content: Text("We won't show ${landmark.name} again")) | ||||||
|   | |||||||
| @@ -79,7 +79,7 @@ class _MapWidgetState extends State<MapWidget> { | |||||||
|       cloudMapId: MAP_ID, |       cloudMapId: MAP_ID, | ||||||
|       mapToolbarEnabled: false, |       mapToolbarEnabled: false, | ||||||
|       zoomControlsEnabled: false, |       zoomControlsEnabled: false, | ||||||
|  |       myLocationEnabled: true, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -18,10 +18,6 @@ class _LandmarkCardState extends State<LandmarkCard> { | |||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     ThemeData theme = Theme.of(context); |     ThemeData theme = Theme.of(context); | ||||||
|     ButtonStyle buttonStyle = TextButton.styleFrom( |  | ||||||
|       backgroundColor: Colors.orange, |  | ||||||
|       fixedSize: Size.fromHeight(20) |  | ||||||
|     ); |  | ||||||
|     return Container( |     return Container( | ||||||
|       height: 160, |       height: 160, | ||||||
|       child: Card( |       child: Card( | ||||||
| @@ -88,21 +84,21 @@ class _LandmarkCardState extends State<LandmarkCard> { | |||||||
|                         // show the type, the website, and the wikipedia link as buttons/labels in a row |                         // show the type, the website, and the wikipedia link as buttons/labels in a row | ||||||
|                         children: [ |                         children: [ | ||||||
|                           TextButton.icon( |                           TextButton.icon( | ||||||
|                             style: buttonStyle, |                             style: theme.iconButtonTheme.style, | ||||||
|                             onPressed: () {}, |                             onPressed: () {}, | ||||||
|                             icon: widget.landmark.type.icon, |                             icon: widget.landmark.type.icon, | ||||||
|                             label: Text(widget.landmark.type.name), |                             label: Text(widget.landmark.type.name), | ||||||
|                           ), |                           ), | ||||||
|                           if (widget.landmark.duration != null && widget.landmark.duration!.inMinutes > 0) |                           if (widget.landmark.duration != null && widget.landmark.duration!.inMinutes > 0) | ||||||
|                             TextButton.icon( |                             TextButton.icon( | ||||||
|                               style: buttonStyle, |                               style: theme.iconButtonTheme.style, | ||||||
|                               onPressed: () {}, |                               onPressed: () {}, | ||||||
|                               icon: Icon(Icons.hourglass_bottom), |                               icon: Icon(Icons.hourglass_bottom), | ||||||
|                               label: Text('${widget.landmark.duration!.inMinutes} minutes'), |                               label: Text('${widget.landmark.duration!.inMinutes} minutes'), | ||||||
|                             ), |                             ), | ||||||
|                           if (widget.landmark.websiteURL != null) |                           if (widget.landmark.websiteURL != null) | ||||||
|                             TextButton.icon( |                             TextButton.icon( | ||||||
|                               style: buttonStyle, |                               style: theme.iconButtonTheme.style, | ||||||
|                               onPressed: () async { |                               onPressed: () async { | ||||||
|                                 // open a browser with the website link |                                 // open a browser with the website link | ||||||
|                                 await launchUrl(Uri.parse(widget.landmark.websiteURL!)); |                                 await launchUrl(Uri.parse(widget.landmark.websiteURL!)); | ||||||
| @@ -112,7 +108,7 @@ class _LandmarkCardState extends State<LandmarkCard> { | |||||||
|                             ), |                             ), | ||||||
|                           if (widget.landmark.wikipediaURL != null) |                           if (widget.landmark.wikipediaURL != null) | ||||||
|                             TextButton.icon( |                             TextButton.icon( | ||||||
|                               style: buttonStyle, |                               style: theme.iconButtonTheme.style, | ||||||
|                               onPressed: () async { |                               onPressed: () async { | ||||||
|                                 // open a browser with the wikipedia link |                                 // open a browser with the wikipedia link | ||||||
|                                 await launchUrl(Uri.parse(widget.landmark.wikipediaURL!)); |                                 await launchUrl(Uri.parse(widget.landmark.wikipediaURL!)); | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import 'package:anyway/layout.dart'; | import 'package:anyway/layout.dart'; | ||||||
|  | import 'package:anyway/main.dart'; | ||||||
| import 'package:anyway/structs/preferences.dart'; | import 'package:anyway/structs/preferences.dart'; | ||||||
| import 'package:anyway/structs/trip.dart'; | import 'package:anyway/structs/trip.dart'; | ||||||
| import 'package:anyway/utils/fetch_trip.dart'; | import 'package:anyway/utils/fetch_trip.dart'; | ||||||
| @@ -8,8 +9,12 @@ import 'package:flutter/material.dart'; | |||||||
|  |  | ||||||
| class NewTripButton extends StatefulWidget { | class NewTripButton extends StatefulWidget { | ||||||
|   final Trip trip; |   final Trip trip; | ||||||
|  |   final UserPreferences preferences; | ||||||
|  |  | ||||||
|   const NewTripButton({required this.trip}); |   const NewTripButton({ | ||||||
|  |     required this.trip, | ||||||
|  |     required this.preferences, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   State<NewTripButton> createState() => _NewTripButtonState(); |   State<NewTripButton> createState() => _NewTripButtonState(); | ||||||
| @@ -23,42 +28,39 @@ class _NewTripButtonState extends State<NewTripButton> { | |||||||
|       listenable: widget.trip, |       listenable: widget.trip, | ||||||
|       builder: (BuildContext context, Widget? child) { |       builder: (BuildContext context, Widget? child) { | ||||||
|         if (widget.trip.landmarks.isEmpty){ |         if (widget.trip.landmarks.isEmpty){ | ||||||
|  |           // Fallback if the trip setup is lagging behind | ||||||
|  |           // This should in theory never happen | ||||||
|           return Container(); |           return Container(); | ||||||
|         } |         } | ||||||
|         return FloatingActionButton.extended( |         return FloatingActionButton.extended( | ||||||
|           onPressed: () async { |           onPressed: onPressed, | ||||||
|             Future<UserPreferences> preferences = loadUserPreferences(); |           icon: const Icon(Icons.add), | ||||||
|  |           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; |       Trip trip = widget.trip; | ||||||
|             fetchTrip(trip, preferences); |       fetchTrip(trip, widget.preferences); | ||||||
|       Navigator.of(context).push( |       Navigator.of(context).push( | ||||||
|         MaterialPageRoute( |         MaterialPageRoute( | ||||||
|           builder: (context) => BasePage(mainScreen: "map", trip: trip) |           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:anyway/structs/trip.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:shared_preferences/shared_preferences.dart'; | ||||||
|  |  | ||||||
| class NewTripLocationSearch extends StatefulWidget { | class NewTripLocationSearch extends StatefulWidget { | ||||||
|  |   Future<SharedPreferences> prefs = SharedPreferences.getInstance(); | ||||||
|   Trip trip; |   Trip trip; | ||||||
|  |    | ||||||
|   NewTripLocationSearch( |   NewTripLocationSearch( | ||||||
|     this.trip, |     this.trip, | ||||||
|   ); |   ); | ||||||
| @@ -45,20 +48,51 @@ class _NewTripLocationSearchState extends State<NewTripLocationSearch> { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   late Widget locationSearchBar = SearchBar( | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     return SearchBar( |  | ||||||
|     hintText: 'Enter a city name or long press on the map.', |     hintText: 'Enter a city name or long press on the map.', | ||||||
|     onSubmitted: setTripLocation, |     onSubmitted: setTripLocation, | ||||||
|     controller: _controller, |     controller: _controller, | ||||||
|     leading: Icon(Icons.search), |     leading: Icon(Icons.search), | ||||||
|       trailing: [ElevatedButton( |     trailing: [ | ||||||
|  |       ElevatedButton( | ||||||
|         onPressed: () { |         onPressed: () { | ||||||
|           setTripLocation(_controller.text); |           setTripLocation(_controller.text); | ||||||
|         }, |         }, | ||||||
|         child: Text('Search'), |         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. | // A map that allows the user to select a location for a new trip. | ||||||
| import 'dart:developer'; | import 'dart:developer'; | ||||||
|  |  | ||||||
| @@ -22,7 +21,7 @@ class NewTripMap extends StatefulWidget { | |||||||
| } | } | ||||||
|  |  | ||||||
| class _NewTripMapState extends State<NewTripMap> { | class _NewTripMapState extends State<NewTripMap> { | ||||||
|   final CameraPosition _cameraPosition = CameraPosition( |   final CameraPosition _cameraPosition = const CameraPosition( | ||||||
|     target: LatLng(48.8566, 2.3522), |     target: LatLng(48.8566, 2.3522), | ||||||
|     zoom: 11.0, |     zoom: 11.0, | ||||||
|   ); |   ); | ||||||
| @@ -70,7 +69,6 @@ class _NewTripMapState extends State<NewTripMap> { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     widget.trip.addListener(updateTripDetails); |     widget.trip.addListener(updateTripDetails); | ||||||
| @@ -82,6 +80,8 @@ class _NewTripMapState extends State<NewTripMap> { | |||||||
|       cloudMapId: MAP_ID, |       cloudMapId: MAP_ID, | ||||||
|       mapToolbarEnabled: false, |       mapToolbarEnabled: false, | ||||||
|       zoomControlsEnabled: 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 |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     Color baseColor = Theme.of(context).primaryColor; |     Color baseColor = Theme.of(context).colorScheme.secondary; | ||||||
|     // have a different color for each card, incrementing the hue |     // have a different color for each card, incrementing the hue | ||||||
|     Color currentColor = baseColor.withAlpha(baseColor.alpha - index * 30); |     Color currentColor = baseColor.withAlpha(baseColor.alpha - index * 30); | ||||||
|     return Container( |     return Container( | ||||||
|       color: currentColor, |       color: currentColor, | ||||||
|  |       alignment: Alignment.center, | ||||||
|       child: Padding( |       child: Padding( | ||||||
|         padding: EdgeInsets.all(20), |         padding: EdgeInsets.all(20), | ||||||
|         child: Column( |         child: Column( | ||||||
|  |           mainAxisAlignment: MainAxisAlignment.center, | ||||||
|           children: [ |           children: [ | ||||||
|             Text( |             Text( | ||||||
|               title, |               title, | ||||||
|               style: TextStyle( |               style: TextStyle( | ||||||
|                 fontSize: 24, |                 fontSize: 24, | ||||||
|                 fontWeight: FontWeight.bold, |                 fontWeight: FontWeight.bold, | ||||||
|  |                 color: Colors.white, | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
|             Padding(padding: EdgeInsets.only(top: 20)), |             Padding(padding: EdgeInsets.only(top: 20)), | ||||||
| @@ -44,7 +47,8 @@ class OnboardingCard extends StatelessWidget { | |||||||
|                 fontSize: 16, |                 fontSize: 16, | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
|           ], |  | ||||||
|  |           ] | ||||||
|         ), |         ), | ||||||
|       ) |       ) | ||||||
|     ); |     ); | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | import 'package:anyway/constants.dart'; | ||||||
| import 'package:anyway/structs/landmark.dart'; | import 'package:anyway/structs/landmark.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  |  | ||||||
| @@ -51,9 +52,7 @@ class ThemedMarker extends StatelessWidget { | |||||||
|         children: [ |         children: [ | ||||||
|           Container( |           Container( | ||||||
|             decoration: BoxDecoration( |             decoration: BoxDecoration( | ||||||
|               gradient: LinearGradient( |               gradient: APP_GRADIENT, | ||||||
|                 colors: [Colors.red, Colors.yellow] |  | ||||||
|               ), |  | ||||||
|               shape: BoxShape.circle, |               shape: BoxShape.circle, | ||||||
|               border: Border.all(color: Colors.black, width: 5), |               border: Border.all(color: Colors.black, width: 5), | ||||||
|             ), |             ), | ||||||
|   | |||||||
| @@ -1,11 +1,7 @@ | |||||||
| import 'package:anyway/modules/new_trip_button.dart'; | import 'package:anyway/modules/new_trip_button.dart'; | ||||||
| import 'package:anyway/structs/landmark.dart'; | import 'package:anyway/modules/new_trip_options_button.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:geocoding/geocoding.dart'; |  | ||||||
| 
 | 
 | ||||||
| import 'package:anyway/layout.dart'; |  | ||||||
| import 'package:anyway/utils/fetch_trip.dart'; |  | ||||||
| import 'package:anyway/structs/preferences.dart'; |  | ||||||
| import "package:anyway/structs/trip.dart"; | import "package:anyway/structs/trip.dart"; | ||||||
| import 'package:anyway/modules/new_trip_location_search.dart'; | import 'package:anyway/modules/new_trip_location_search.dart'; | ||||||
| import 'package:anyway/modules/new_trip_map.dart'; | import 'package:anyway/modules/new_trip_map.dart'; | ||||||
| @@ -19,7 +15,6 @@ class NewTripPage extends StatefulWidget { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| class _NewTripPageState extends State<NewTripPage> { | class _NewTripPageState extends State<NewTripPage> { | ||||||
|   final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); |  | ||||||
|   final TextEditingController latController = TextEditingController(); |   final TextEditingController latController = TextEditingController(); | ||||||
|   final TextEditingController lonController = TextEditingController(); |   final TextEditingController lonController = TextEditingController(); | ||||||
|   Trip trip = Trip(); |   Trip trip = Trip(); | ||||||
| @@ -40,7 +35,7 @@ class _NewTripPageState extends State<NewTripPage> { | |||||||
|           ), |           ), | ||||||
|         ], |         ], | ||||||
|       ), |       ), | ||||||
|       floatingActionButton: NewTripButton(trip: trip), |       floatingActionButton: NewTripOptionsButton(trip: trip), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
							
								
								
									
										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/modules/onboarding_card.dart'; | ||||||
| import 'package:anyway/pages/new_trip.dart'; | import 'package:anyway/pages/new_trip_location.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  |  | ||||||
| class OnboardingPage extends StatefulWidget { | class OnboardingPage extends StatefulWidget { | ||||||
|   | |||||||
| @@ -1,177 +0,0 @@ | |||||||
| import 'package:anyway/constants.dart'; |  | ||||||
| import 'package:anyway/structs/preferences.dart'; |  | ||||||
| import 'package:flutter/material.dart'; |  | ||||||
|  |  | ||||||
|  |  | ||||||
| bool debugMode = false; |  | ||||||
|  |  | ||||||
| class ProfilePage extends StatefulWidget { |  | ||||||
|   @override |  | ||||||
|   _ProfilePageState createState() => _ProfilePageState(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class _ProfilePageState extends State<ProfilePage> { |  | ||||||
|   Future<UserPreferences> _prefs = loadUserPreferences(); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     return ListView( |  | ||||||
|       children: [ |  | ||||||
|         // First a round, centered image |  | ||||||
|         Center( |  | ||||||
|           child: CircleAvatar( |  | ||||||
|             radius: 100, |  | ||||||
|             child: Icon(Icons.person, size: 100), |  | ||||||
|           ) |  | ||||||
|         ), |  | ||||||
|         Center( |  | ||||||
|           child: Text('Curious traveler', style: TextStyle(fontSize: 24)) |  | ||||||
|         ), |  | ||||||
|  |  | ||||||
|         Divider(indent: 25, endIndent: 25, height: 50), |  | ||||||
|  |  | ||||||
|         Center( |  | ||||||
|           child: Padding( |  | ||||||
|            padding: EdgeInsets.only(left: 10, right: 10, top: 0, bottom: 10), |  | ||||||
|             child: Text('For a tailored experience, please rate your discovery preferences.', style: TextStyle(fontSize: 18)) |  | ||||||
|           ), |  | ||||||
|         ), |  | ||||||
|  |  | ||||||
|         FutureBuilder(future: _prefs, builder: futureSliders), |  | ||||||
|  |  | ||||||
|         Divider(indent: 25, endIndent: 25, height: 50), |  | ||||||
|  |  | ||||||
|         privacyInfo(), |  | ||||||
|  |  | ||||||
|         debugButton(), |  | ||||||
|       ] |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget debugButton() { |  | ||||||
|     return Padding( |  | ||||||
|       padding: EdgeInsets.only(top: 20), |  | ||||||
|       child: Row( |  | ||||||
|         children: [ |  | ||||||
|           Text('Debug mode'), |  | ||||||
|           Switch( |  | ||||||
|             value: debugMode, |  | ||||||
|             onChanged: (bool? newValue) { |  | ||||||
|               setState(() { |  | ||||||
|                 debugMode = newValue!; |  | ||||||
|                 showDialog( |  | ||||||
|                   context: context, |  | ||||||
|                   builder: (BuildContext context) { |  | ||||||
|                     return AlertDialog( |  | ||||||
|                       title: Text('Debug mode - custom API'), |  | ||||||
|                       content: TextField( |  | ||||||
|                         decoration: InputDecoration( |  | ||||||
|                           hintText: 'http://localhost:8000' |  | ||||||
|                         ), |  | ||||||
|                       onChanged: (value) { |  | ||||||
|                         setState(() { |  | ||||||
|                           API_URL_BASE = value; |  | ||||||
|                         }); |  | ||||||
|                       }, |  | ||||||
|                     ), |  | ||||||
|                     actions: [ |  | ||||||
|                       TextButton( |  | ||||||
|                         child: Text('OK'), |  | ||||||
|                         onPressed: () { |  | ||||||
|                           Navigator.of(context).pop(); |  | ||||||
|                         }, |  | ||||||
|                       ), |  | ||||||
|                     ], |  | ||||||
|                     ); |  | ||||||
|                   } |  | ||||||
|                 ); |  | ||||||
|               }); |  | ||||||
|             } |  | ||||||
|           ) |  | ||||||
|         ], |  | ||||||
|       ) |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|   Widget futureSliders(BuildContext context, AsyncSnapshot<UserPreferences> snapshot) { |  | ||||||
|     if (snapshot.connectionState == ConnectionState.done) { |  | ||||||
|       UserPreferences prefs = snapshot.data!; |  | ||||||
|  |  | ||||||
|       return Column( |  | ||||||
|         children: [ |  | ||||||
|           PreferenceSliders(prefs: [prefs.maxTime, prefs.maxDetour]), |  | ||||||
|           Divider(indent: 25, endIndent: 25, height: 50), |  | ||||||
|           PreferenceSliders(prefs: [prefs.sightseeing, prefs.shopping, prefs.nature]) |  | ||||||
|         ] |  | ||||||
|       ); |  | ||||||
|     } else { |  | ||||||
|       return CircularProgressIndicator(); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget privacyInfo() { |  | ||||||
|     return Padding( |  | ||||||
|       padding: EdgeInsets.only(top: 20), |  | ||||||
|       child: Row( |  | ||||||
|         children: [ |  | ||||||
|           Text('Privacy policy is available at '), |  | ||||||
|           TextButton.icon( |  | ||||||
|             icon: Icon(Icons.info), |  | ||||||
|             label: Text(PRIVACY_URL), |  | ||||||
|             onPressed: () { |  | ||||||
|             } |  | ||||||
|           ) |  | ||||||
|         ], |  | ||||||
|       ) |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class PreferenceSliders extends StatefulWidget { |  | ||||||
|   final List<SinglePreference> prefs; |  | ||||||
|  |  | ||||||
|   PreferenceSliders({required this.prefs}); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   State<PreferenceSliders> createState() => _PreferenceSlidersState(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class _PreferenceSlidersState extends State<PreferenceSliders> { |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     List<Card> sliders = []; |  | ||||||
|       for (SinglePreference pref in widget.prefs) { |  | ||||||
|       sliders.add( |  | ||||||
|         Card( |  | ||||||
|           child: ListTile( |  | ||||||
|             leading: pref.icon, |  | ||||||
|             title: Text(pref.name), |  | ||||||
|             subtitle: Slider( |  | ||||||
|               value: pref.value.toDouble(), |  | ||||||
|               min: pref.minVal.toDouble(), |  | ||||||
|               max: pref.maxVal.toDouble(), |  | ||||||
|               divisions: pref.maxVal - pref.minVal, |  | ||||||
|               label: pref.value.toString(), |  | ||||||
|               onChanged: (double newValue) { |  | ||||||
|                 setState(() { |  | ||||||
|                   pref.value = newValue.toInt(); |  | ||||||
|                   pref.save(); |  | ||||||
|                 }); |  | ||||||
|               }, |  | ||||||
|             ) |  | ||||||
|           ), |  | ||||||
|           margin: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0), |  | ||||||
|           shadowColor: Colors.grey, |  | ||||||
|         ) |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return Column( |  | ||||||
|       children: sliders); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
							
								
								
									
										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:flutter/material.dart'; | ||||||
| import 'package:shared_preferences/shared_preferences.dart'; |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class SinglePreference { | class SinglePreference { | ||||||
| @@ -20,16 +19,6 @@ class SinglePreference { | |||||||
|     this.minVal = 0, |     this.minVal = 0, | ||||||
|     this.maxVal = 5, |     this.maxVal = 5, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   void save() async { |  | ||||||
|     SharedPreferences sharedPrefs = await SharedPreferences.getInstance(); |  | ||||||
|       sharedPrefs.setInt('pref_$slug', value); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void load() async { |  | ||||||
|     SharedPreferences sharedPrefs = await SharedPreferences.getInstance(); |  | ||||||
|       value = sharedPrefs.getInt('pref_$slug') ?? minVal; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -65,38 +54,15 @@ class UserPreferences { | |||||||
|     maxVal: 720, |     maxVal: 720, | ||||||
|     icon: Icon(Icons.timer), |     icon: Icon(Icons.timer), | ||||||
|   ); |   ); | ||||||
|   SinglePreference maxDetour = SinglePreference( |  | ||||||
|     name: "Trip detours", |  | ||||||
|     slug: "detours", |  | ||||||
|     description: "Are you okay with roaming even if makes the trip longer?", |  | ||||||
|     value: 0, |  | ||||||
|     maxVal: 30, |  | ||||||
|     icon: Icon(Icons.loupe_sharp), |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|   Future<void> load() async { |  | ||||||
|     for (SinglePreference pref in [sightseeing, shopping, nature, maxTime, maxDetour]) { |  | ||||||
|       pref.load(); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Map<String, dynamic> toJson() { |   Map<String, dynamic> toJson() { | ||||||
|       // This is "opinionated" JSON, corresponding to the backend's expectations |       // This is "opinionated" JSON, corresponding to the backend's expectations | ||||||
|       return { |       return { | ||||||
|         "sightseeing": {"type": "sightseeing", "score": sightseeing.value}, |         "sightseeing": {"type": "sightseeing", "score": sightseeing.value}, | ||||||
|         "shopping": {"type": "shopping", "score": shopping.value}, |         "shopping": {"type": "shopping", "score": shopping.value}, | ||||||
|         "nature": {"type": "nature", "score": nature.value}, |         "nature": {"type": "nature", "score": nature.value}, | ||||||
|         "max_time_minute": maxTime.value, |         "max_time_minute": maxTime.value | ||||||
|         "detour_tolerance_minute": maxDetour.value |  | ||||||
|       }; |       }; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| Future<UserPreferences> loadUserPreferences() async { |  | ||||||
|   UserPreferences prefs = UserPreferences(); |  | ||||||
|   await prefs.load(); |  | ||||||
|   return prefs; |  | ||||||
| } |  | ||||||
| @@ -29,11 +29,10 @@ Dio dio = Dio( | |||||||
|  |  | ||||||
| fetchTrip( | fetchTrip( | ||||||
|   Trip trip, |   Trip trip, | ||||||
|   Future<UserPreferences> preferences, |   UserPreferences preferences, | ||||||
| ) async { | ) async { | ||||||
|   UserPreferences prefs = await preferences; |  | ||||||
|   Map<String, dynamic> data = { |   Map<String, dynamic> data = { | ||||||
|     "preferences": prefs.toJson(), |     "preferences": preferences.toJson(), | ||||||
|     "start": trip.landmarks!.first.location, |     "start": trip.landmarks!.first.location, | ||||||
|   }; |   }; | ||||||
|   String dataString = jsonEncode(data); |   String dataString = jsonEncode(data); | ||||||
| @@ -47,11 +46,16 @@ fetchTrip( | |||||||
|   // handle errors |   // handle errors | ||||||
|   if (response.statusCode != 200) { |   if (response.statusCode != 200) { | ||||||
|     trip.updateUUID("error"); |     trip.updateUUID("error"); | ||||||
|     if (response.data["detail"] != null) { |     String errorDetail; | ||||||
|       trip.updateError(response.data["detail"]); |     if (response.data.runtimeType == String) { | ||||||
|       log(response.data["detail"]); |       errorDetail = response.data; | ||||||
|       // throw Exception(response.data["detail"]); |     } else { | ||||||
|  |       errorDetail = response.data["detail"] ?? "Unknown error"; | ||||||
|     } |     } | ||||||
|  |     trip.updateError(errorDetail); | ||||||
|  |     log(errorDetail); | ||||||
|  |     // Actualy no need to throw an exception, we can just log the error and let the user retry | ||||||
|  |     // throw Exception(errorDetail); | ||||||
|   } else { |   } else { | ||||||
|     Map<String, dynamic> json = response.data; |     Map<String, dynamic> json = response.data; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -496,6 +496,54 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.3.0" |     version: "2.3.0" | ||||||
|  |   permission_handler: | ||||||
|  |     dependency: "direct main" | ||||||
|  |     description: | ||||||
|  |       name: permission_handler | ||||||
|  |       sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "11.3.1" | ||||||
|  |   permission_handler_android: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: permission_handler_android | ||||||
|  |       sha256: "76e4ab092c1b240d31177bb64d2b0bea43f43d0e23541ec866151b9f7b2490fa" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "12.0.12" | ||||||
|  |   permission_handler_apple: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: permission_handler_apple | ||||||
|  |       sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0 | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "9.4.5" | ||||||
|  |   permission_handler_html: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: permission_handler_html | ||||||
|  |       sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851 | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "0.1.3+2" | ||||||
|  |   permission_handler_platform_interface: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: permission_handler_platform_interface | ||||||
|  |       sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9 | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "4.2.3" | ||||||
|  |   permission_handler_windows: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: permission_handler_windows | ||||||
|  |       sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "0.2.1" | ||||||
|   petitparser: |   petitparser: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|   | |||||||
| @@ -49,6 +49,7 @@ dependencies: | |||||||
|   flutter_svg: ^2.0.10+1 |   flutter_svg: ^2.0.10+1 | ||||||
|   url_launcher: ^6.3.0 |   url_launcher: ^6.3.0 | ||||||
|   flutter_launcher_icons: ^0.13.1 |   flutter_launcher_icons: ^0.13.1 | ||||||
|  |   permission_handler: ^11.3.1 | ||||||
|  |  | ||||||
| dev_dependencies: | dev_dependencies: | ||||||
|   flutter_test: |   flutter_test: | ||||||
|   | |||||||
| @@ -6,9 +6,12 @@ | |||||||
|  |  | ||||||
| #include "generated_plugin_registrant.h" | #include "generated_plugin_registrant.h" | ||||||
|  |  | ||||||
|  | #include <permission_handler_windows/permission_handler_windows_plugin.h> | ||||||
| #include <url_launcher_windows/url_launcher_windows.h> | #include <url_launcher_windows/url_launcher_windows.h> | ||||||
|  |  | ||||||
| void RegisterPlugins(flutter::PluginRegistry* registry) { | void RegisterPlugins(flutter::PluginRegistry* registry) { | ||||||
|  |   PermissionHandlerWindowsPluginRegisterWithRegistrar( | ||||||
|  |       registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); | ||||||
|   UrlLauncherWindowsRegisterWithRegistrar( |   UrlLauncherWindowsRegisterWithRegistrar( | ||||||
|       registry->GetRegistrarForPlugin("UrlLauncherWindows")); |       registry->GetRegistrarForPlugin("UrlLauncherWindows")); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ | |||||||
| # | # | ||||||
|  |  | ||||||
| list(APPEND FLUTTER_PLUGIN_LIST | list(APPEND FLUTTER_PLUGIN_LIST | ||||||
|  |   permission_handler_windows | ||||||
|   url_launcher_windows |   url_launcher_windows | ||||||
| ) | ) | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user