From ed60fcba064378ee116a1d1b522455cf4500103b Mon Sep 17 00:00:00 2001 From: Remy Moll Date: Tue, 24 Sep 2024 22:58:28 +0200 Subject: [PATCH] revamp new trip flow --- .../android/app/src/main/AndroidManifest.xml | 3 + frontend/android/gradlew.bat | 90 +++++++++ frontend/devtools_options.yaml | 3 + frontend/lib/constants.dart | 58 ++++++ frontend/lib/layout.dart | 26 +-- frontend/lib/main.dart | 2 +- .../modules/current_trip_landmarks_list.dart | 1 - frontend/lib/modules/current_trip_map.dart | 2 +- frontend/lib/modules/landmark_card.dart | 12 +- frontend/lib/modules/new_trip_button.dart | 68 +++---- .../lib/modules/new_trip_location_search.dart | 54 +++++- frontend/lib/modules/new_trip_map.dart | 6 +- .../lib/modules/new_trip_options_button.dart | 41 ++++ frontend/lib/modules/onboarding_card.dart | 8 +- frontend/lib/modules/themed_marker.dart | 5 +- .../{new_trip.dart => new_trip_location.dart} | 9 +- frontend/lib/pages/new_trip_preferences.dart | 114 +++++++++++ frontend/lib/pages/onboarding.dart | 2 +- frontend/lib/pages/profile.dart | 177 ------------------ frontend/lib/pages/settings.dart | 177 ++++++++++++++++++ frontend/lib/structs/preferences.dart | 36 +--- frontend/lib/utils/fetch_trip.dart | 18 +- frontend/pubspec.lock | 48 +++++ frontend/pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + .../windows/flutter/generated_plugins.cmake | 1 + 26 files changed, 663 insertions(+), 302 deletions(-) create mode 100755 frontend/android/gradlew.bat create mode 100644 frontend/devtools_options.yaml create mode 100644 frontend/lib/modules/new_trip_options_button.dart rename frontend/lib/pages/{new_trip.dart => new_trip_location.dart} (75%) create mode 100644 frontend/lib/pages/new_trip_preferences.dart delete mode 100644 frontend/lib/pages/profile.dart create mode 100644 frontend/lib/pages/settings.dart diff --git a/frontend/android/app/src/main/AndroidManifest.xml b/frontend/android/app/src/main/AndroidManifest.xml index 35d3386..2d36363 100644 --- a/frontend/android/app/src/main/AndroidManifest.xml +++ b/frontend/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,9 @@ + + + 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 diff --git a/frontend/devtools_options.yaml b/frontend/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/frontend/devtools_options.yaml @@ -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: diff --git a/frontend/lib/constants.dart b/frontend/lib/constants.dart index a063cdd..3320062 100644 --- a/frontend/lib/constants.dart +++ b/frontend/lib/constants.dart @@ -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], +); \ No newline at end of file diff --git a/frontend/lib/layout.dart b/frontend/lib/layout.dart index b8f3f38..3441f6b 100644 --- a/frontend/lib/layout.dart +++ b/frontend/lib/layout.dart @@ -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 { ) ); }, - label: Text("Plan a trip now"), + label: Text("Plan a trip"), ), ); } @@ -74,33 +74,33 @@ class _BasePageState extends State { } } 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 { }, 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 { 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") ) ); }, diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 9e2f631..77c7616 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -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 ); } diff --git a/frontend/lib/modules/current_trip_landmarks_list.dart b/frontend/lib/modules/current_trip_landmarks_list.dart index 097b700..ada17f2 100644 --- a/frontend/lib/modules/current_trip_landmarks_list.dart +++ b/frontend/lib/modules/current_trip_landmarks_list.dart @@ -32,7 +32,6 @@ List 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")) diff --git a/frontend/lib/modules/current_trip_map.dart b/frontend/lib/modules/current_trip_map.dart index dfd174a..6ce4b4e 100644 --- a/frontend/lib/modules/current_trip_map.dart +++ b/frontend/lib/modules/current_trip_map.dart @@ -79,7 +79,7 @@ class _MapWidgetState extends State { cloudMapId: MAP_ID, mapToolbarEnabled: false, zoomControlsEnabled: false, - + myLocationEnabled: true, ); } } diff --git a/frontend/lib/modules/landmark_card.dart b/frontend/lib/modules/landmark_card.dart index 3c027e5..78b6a27 100644 --- a/frontend/lib/modules/landmark_card.dart +++ b/frontend/lib/modules/landmark_card.dart @@ -18,10 +18,6 @@ class _LandmarkCardState extends State { @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 { // 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 { ), 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!)); diff --git a/frontend/lib/modules/new_trip_button.dart b/frontend/lib/modules/new_trip_button.dart index 99491f2..d966abf 100644 --- a/frontend/lib/modules/new_trip_button.dart +++ b/frontend/lib/modules/new_trip_button.dart @@ -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 createState() => _NewTripButtonState(); @@ -23,42 +28,39 @@ class _NewTripButtonState extends State { 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 preferences = loadUserPreferences(); - Trip trip = widget.trip; - fetchTrip(trip, preferences); - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => BasePage(mainScreen: "map", trip: trip) - ) - ); - }, - icon: Icon(Icons.add), - label: FutureBuilder( - future: widget.trip.cityName, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - return AutoSizeText( - 'New trip to ${snapshot.data.toString()}', - style: TextStyle(fontSize: 18), - maxLines: 2, - ); - } else { - return AutoSizeText( - 'New trip to ...', - style: TextStyle(fontSize: 18), - maxLines: 2, - ); - } - }, - ) - ); - - } + onPressed: onPressed, + icon: const Icon(Icons.add), + label: AutoSizeText('Start planning!'), + ); + } ); } + + void onPressed() async { + // Check that the preferences are valid + UserPreferences preferences = widget.preferences; + if (preferences.nature.value == 0 && preferences.shopping.value == 0 && preferences.sightseeing.value == 0){ + rootScaffoldMessengerKey.currentState!.showSnackBar( + SnackBar(content: Text("Please specify at least one preference")) + ); + } else if (preferences.maxTime.value == 0){ + rootScaffoldMessengerKey.currentState!.showSnackBar( + SnackBar(content: Text("Please choose a longer duration")) + ); + } else { + Trip trip = widget.trip; + fetchTrip(trip, widget.preferences); + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => BasePage(mainScreen: "map", trip: trip) + ) + ); + } + } } diff --git a/frontend/lib/modules/new_trip_location_search.dart b/frontend/lib/modules/new_trip_location_search.dart index 088d08f..2aae71b 100644 --- a/frontend/lib/modules/new_trip_location_search.dart +++ b/frontend/lib/modules/new_trip_location_search.dart @@ -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 prefs = SharedPreferences.getInstance(); Trip trip; + NewTripLocationSearch( this.trip, ); @@ -45,20 +48,51 @@ class _NewTripLocationSearchState extends State { } } - @override - Widget build(BuildContext context) { - return SearchBar( - hintText: 'Enter a city name or long press on the map.', - onSubmitted: setTripLocation, - controller: _controller, - leading: Icon(Icons.search), - trailing: [ElevatedButton( + late Widget locationSearchBar = SearchBar( + hintText: 'Enter a city name or long press on the map.', + onSubmitted: setTripLocation, + controller: _controller, + leading: Icon(Icons.search), + trailing: [ + ElevatedButton( onPressed: () { setTripLocation(_controller.text); }, child: Text('Search'), - ),] + ) + ] + ); + + late Widget useCurrentLocationButton = ElevatedButton( + onPressed: () { + // 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; + } + }, ); } -} \ No newline at end of file +} diff --git a/frontend/lib/modules/new_trip_map.dart b/frontend/lib/modules/new_trip_map.dart index fc0a8c2..2966454 100644 --- a/frontend/lib/modules/new_trip_map.dart +++ b/frontend/lib/modules/new_trip_map.dart @@ -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 { - 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 { } - @override Widget build(BuildContext context) { widget.trip.addListener(updateTripDetails); @@ -82,6 +80,8 @@ class _NewTripMapState extends State { cloudMapId: MAP_ID, mapToolbarEnabled: false, zoomControlsEnabled: false, + // TODO: should be loaded from the sharedprefs + myLocationEnabled: true, ); } } \ No newline at end of file diff --git a/frontend/lib/modules/new_trip_options_button.dart b/frontend/lib/modules/new_trip_options_button.dart new file mode 100644 index 0000000..499c642 --- /dev/null +++ b/frontend/lib/modules/new_trip_options_button.dart @@ -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 createState() => _NewTripOptionsButtonState(); +} + +class _NewTripOptionsButtonState extends State { + + @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') + ); + } + ); + } +} + diff --git a/frontend/lib/modules/onboarding_card.dart b/frontend/lib/modules/onboarding_card.dart index 573c82d..4124754 100644 --- a/frontend/lib/modules/onboarding_card.dart +++ b/frontend/lib/modules/onboarding_card.dart @@ -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, ), ), - ], + + ] ), ) ); diff --git a/frontend/lib/modules/themed_marker.dart b/frontend/lib/modules/themed_marker.dart index c8b7ea2..de52739 100644 --- a/frontend/lib/modules/themed_marker.dart +++ b/frontend/lib/modules/themed_marker.dart @@ -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), ), diff --git a/frontend/lib/pages/new_trip.dart b/frontend/lib/pages/new_trip_location.dart similarity index 75% rename from frontend/lib/pages/new_trip.dart rename to frontend/lib/pages/new_trip_location.dart index 741b4dc..7fb3b4b 100644 --- a/frontend/lib/pages/new_trip.dart +++ b/frontend/lib/pages/new_trip_location.dart @@ -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 { - final GlobalKey _formKey = GlobalKey(); final TextEditingController latController = TextEditingController(); final TextEditingController lonController = TextEditingController(); Trip trip = Trip(); @@ -40,7 +35,7 @@ class _NewTripPageState extends State { ), ], ), - floatingActionButton: NewTripButton(trip: trip), + floatingActionButton: NewTripOptionsButton(trip: trip), ); } } diff --git a/frontend/lib/pages/new_trip_preferences.dart b/frontend/lib/pages/new_trip_preferences.dart new file mode 100644 index 0000000..a41b140 --- /dev/null +++ b/frontend/lib/pages/new_trip_preferences.dart @@ -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 { + 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 prefs) { + List 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 + ); + } +} + diff --git a/frontend/lib/pages/onboarding.dart b/frontend/lib/pages/onboarding.dart index 6412273..e23a016 100644 --- a/frontend/lib/pages/onboarding.dart +++ b/frontend/lib/pages/onboarding.dart @@ -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 { diff --git a/frontend/lib/pages/profile.dart b/frontend/lib/pages/profile.dart deleted file mode 100644 index 5ee1687..0000000 --- a/frontend/lib/pages/profile.dart +++ /dev/null @@ -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 { - Future _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 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 prefs; - - PreferenceSliders({required this.prefs}); - - @override - State createState() => _PreferenceSlidersState(); -} - - -class _PreferenceSlidersState extends State { - - @override - Widget build(BuildContext context) { - List 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); - } -} - diff --git a/frontend/lib/pages/settings.dart b/frontend/lib/pages/settings.dart new file mode 100644 index 0000000..ba4297d --- /dev/null +++ b/frontend/lib/pages/settings.dart @@ -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 { + + @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)); + } + ) + ], + ) + ); + } + +} diff --git a/frontend/lib/structs/preferences.dart b/frontend/lib/structs/preferences.dart index 074bbde..315b5c0 100644 --- a/frontend/lib/structs/preferences.dart +++ b/frontend/lib/structs/preferences.dart @@ -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 load() async { - for (SinglePreference pref in [sightseeing, shopping, nature, maxTime, maxDetour]) { - pref.load(); - } - } - Map 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 loadUserPreferences() async { - UserPreferences prefs = UserPreferences(); - await prefs.load(); - return prefs; } \ No newline at end of file diff --git a/frontend/lib/utils/fetch_trip.dart b/frontend/lib/utils/fetch_trip.dart index 8ed04d3..a497f2e 100644 --- a/frontend/lib/utils/fetch_trip.dart +++ b/frontend/lib/utils/fetch_trip.dart @@ -29,11 +29,10 @@ Dio dio = Dio( fetchTrip( Trip trip, - Future preferences, + UserPreferences preferences, ) async { - UserPreferences prefs = await preferences; Map 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 json = response.data; diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index e53d9d5..d6c1b56 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -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: diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index ffcce37..ad2961f 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -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: diff --git a/frontend/windows/flutter/generated_plugin_registrant.cc b/frontend/windows/flutter/generated_plugin_registrant.cc index 4f78848..a0d0bbe 100644 --- a/frontend/windows/flutter/generated_plugin_registrant.cc +++ b/frontend/windows/flutter/generated_plugin_registrant.cc @@ -6,9 +6,12 @@ #include "generated_plugin_registrant.h" +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/frontend/windows/flutter/generated_plugins.cmake b/frontend/windows/flutter/generated_plugins.cmake index 88b22e5..c20a586 100644 --- a/frontend/windows/flutter/generated_plugins.cmake +++ b/frontend/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + permission_handler_windows url_launcher_windows )