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/android/settings.gradle b/frontend/android/settings.gradle index 1d6d19b..c9fb5ba 100644 --- a/frontend/android/settings.gradle +++ b/frontend/android/settings.gradle @@ -20,7 +20,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "7.3.0" apply false - id "org.jetbrains.kotlin.android" version "1.7.10" apply false + id "org.jetbrains.kotlin.android" version "2.0.20" apply false } include ":app" 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..d4e756c 100644 --- a/frontend/lib/constants.dart +++ b/frontend/lib/constants.dart @@ -1,6 +1,85 @@ +import 'package:flutter/material.dart'; + const String APP_NAME = 'AnyWay'; String API_URL_BASE = 'https://anyway.anydev.info'; String PRIVACY_URL = 'https://anydev.info/privacy'; const String MAP_ID = '41c21ac9b81dbfd8'; + + +const Color GRADIENT_START = Color(0xFFF9B208); +const Color GRADIENT_END = Color(0xFFE72E77); + +const Color PRIMARY_COLOR = Color(0xFFF38F1A); + + + +const double TRIP_PANEL_MAX_HEIGHT = 0.8; +const double TRIP_PANEL_MIN_HEIGHT = 0.12; + +ThemeData APP_THEME = ThemeData( + primaryColor: PRIMARY_COLOR, + + scaffoldBackgroundColor: Colors.white, + cardColor: Colors.white, + useMaterial3: true, + + colorScheme: ColorScheme.light( + primary: PRIMARY_COLOR, + secondary: GRADIENT_END, + surface: Colors.white, + error: Colors.red, + onPrimary: Colors.white, + onSecondary: const Color.fromARGB(255, 30, 22, 22), + onSurface: Colors.black, + onError: Colors.white, + brightness: Brightness.light, + ), + + + textButtonTheme: const TextButtonThemeData( + style: ButtonStyle( + foregroundColor: WidgetStatePropertyAll(PRIMARY_COLOR), + side: WidgetStatePropertyAll( + BorderSide( + color: PRIMARY_COLOR, + width: 1, + ), + ), + ) + ), + + elevatedButtonTheme: const ElevatedButtonThemeData( + style: ButtonStyle( + foregroundColor: WidgetStatePropertyAll(PRIMARY_COLOR), + ) + ), + + outlinedButtonTheme: const OutlinedButtonThemeData( + style: ButtonStyle( + foregroundColor: WidgetStatePropertyAll(PRIMARY_COLOR), + ) + ), + + + cardTheme: const CardTheme( + shadowColor: Colors.grey, + elevation: 2, + margin: EdgeInsets.all(10), + ), + + sliderTheme: const SliderThemeData( + trackHeight: 15, + inactiveTrackColor: Colors.grey, + thumbColor: PRIMARY_COLOR, + activeTrackColor: GRADIENT_END + ) +); + + +const Gradient APP_GRADIENT = LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [GRADIENT_START, GRADIENT_END], +); \ 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_greeter.dart b/frontend/lib/modules/current_trip_greeter.dart index b0771d5..6a2118a 100644 --- a/frontend/lib/modules/current_trip_greeter.dart +++ b/frontend/lib/modules/current_trip_greeter.dart @@ -1,5 +1,6 @@ import 'dart:developer'; +import 'package:anyway/constants.dart'; import 'package:anyway/structs/trip.dart'; import 'package:auto_size_text/auto_size_text.dart'; @@ -20,8 +21,12 @@ class Greeter extends StatefulWidget { class _GreeterState extends State { Widget greeterBuilder (BuildContext context, Widget? child) { - ThemeData theme = Theme.of(context); - TextStyle greeterStyle = TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24); + final Shader textGradient = APP_GRADIENT.createShader(Rect.fromLTWH(0.0, 0.0, 200.0, 70.0)); + TextStyle greeterStyle = TextStyle( + foreground: Paint()..shader = textGradient, + fontWeight: FontWeight.bold, + fontSize: 26 + ); Widget topGreeter; @@ -91,26 +96,10 @@ class _GreeterState extends State { } return Center( - child: Column( - children: [ - // Padding(padding: EdgeInsets.only(top: 20)), - topGreeter, - Padding( - padding: EdgeInsets.all(20), - child: bottomGreeter - ), - ], - ) + child: topGreeter, ); } - Widget bottomGreeter = const Text( - "Busy day ahead? Here is how to make the most of it!", - style: TextStyle(color: Colors.black, fontSize: 18), - textAlign: TextAlign.center, - ); - - @override Widget build(BuildContext context) { diff --git a/frontend/lib/modules/current_trip_landmarks_list.dart b/frontend/lib/modules/current_trip_landmarks_list.dart index 097b700..b486a97 100644 --- a/frontend/lib/modules/current_trip_landmarks_list.dart +++ b/frontend/lib/modules/current_trip_landmarks_list.dart @@ -16,7 +16,7 @@ List landmarksList(Trip trip) { log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks"); - if (trip.landmarks.isEmpty || trip.landmarks.length <= 1 && trip.landmarks.first.type == start ) { + if (trip.landmarks.isEmpty || trip.landmarks.length <= 1 && trip.landmarks.first.type == typeStart ) { children.add( const Text("No landmarks in this trip"), ); @@ -32,7 +32,6 @@ List 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..9efdc4c 100644 --- a/frontend/lib/modules/current_trip_map.dart +++ b/frontend/lib/modules/current_trip_map.dart @@ -1,27 +1,28 @@ import 'dart:collection'; import 'package:anyway/constants.dart'; -import 'package:anyway/modules/themed_marker.dart'; +import 'package:anyway/modules/landmark_map_marker.dart'; import 'package:flutter/material.dart'; import 'package:anyway/structs/landmark.dart'; import 'package:anyway/structs/trip.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:widget_to_marker/widget_to_marker.dart'; -class MapWidget extends StatefulWidget { +class CurrentTripMap extends StatefulWidget { final Trip? trip; - MapWidget({ + CurrentTripMap({ this.trip }); @override - State createState() => _MapWidgetState(); + State createState() => _CurrentTripMapState(); } -class _MapWidgetState extends State { +class _CurrentTripMapState extends State { late GoogleMapController mapController; CameraPosition _cameraPosition = CameraPosition( @@ -67,9 +68,27 @@ class _MapWidgetState extends State { }); } + @override Widget build(BuildContext context) { widget.trip?.addListener(setMapMarkers); + Future preferences = SharedPreferences.getInstance(); + + return FutureBuilder( + future: preferences, + builder: (context, snapshot) { + if (snapshot.hasData) { + SharedPreferences prefs = snapshot.data as SharedPreferences; + bool useLocation = prefs.getBool('useLocation') ?? true; + return _buildMap(useLocation); + } else { + return const CircularProgressIndicator(); + } + } + ); + } + + Widget _buildMap(bool useLocation) { return GoogleMap( onMapCreated: _onMapCreated, initialCameraPosition: _cameraPosition, @@ -79,7 +98,9 @@ class _MapWidgetState extends State { cloudMapId: MAP_ID, mapToolbarEnabled: false, zoomControlsEnabled: false, - + myLocationEnabled: useLocation, + myLocationButtonEnabled: false, ); } + } diff --git a/frontend/lib/modules/current_trip_panel.dart b/frontend/lib/modules/current_trip_panel.dart new file mode 100644 index 0000000..cd73f28 --- /dev/null +++ b/frontend/lib/modules/current_trip_panel.dart @@ -0,0 +1,82 @@ +import 'package:anyway/constants.dart'; +import 'package:flutter/material.dart'; + +import 'package:anyway/structs/trip.dart'; +import 'package:anyway/modules/current_trip_summary.dart'; +import 'package:anyway/modules/current_trip_save_button.dart'; +import 'package:anyway/modules/current_trip_landmarks_list.dart'; +import 'package:anyway/modules/current_trip_greeter.dart'; + + +class CurrentTripPanel extends StatefulWidget { + final ScrollController controller; + final Trip trip; + + const CurrentTripPanel({ + super.key, + required this.controller, + required this.trip, + }); + + @override + State createState() => _CurrentTripPanelState(); +} + +class _CurrentTripPanelState extends State { + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: widget.trip, + builder: (context, child) { + if (widget.trip.uuid != 'pending' && widget.trip.uuid != 'error') { + return ListView( + controller: widget.controller, + padding: const EdgeInsets.only(bottom: 30, left: 5, right: 5), + children: [ + SizedBox( + // reuse the exact same height as the panel has when collapsed + // this way the greeter will be centered when the panel is collapsed + height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT - 20, + child: Greeter(trip: widget.trip), + ), + + const Padding(padding: EdgeInsets.only(top: 10)), + + // CurrentTripSummary(trip: widget.trip), + + // const Divider(), + + ...landmarksList(widget.trip), + + const Padding(padding: EdgeInsets.only(top: 10)), + + Center(child: saveButton(widget.trip)), + ], + ); + } else if(widget.trip.uuid == 'pending') { + return SizedBox( + // reuse the exact same height as the panel has when collapsed + // this way the greeter will be centered when the panel is collapsed + height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT - 20, + child: Greeter(trip: widget.trip) + ); + } else { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.error_outline, + color: Colors.red, + size: 50, + ), + Padding( + padding: const EdgeInsets.only(left: 10), + child: Text('Error: ${widget.trip.errorDescription}'), + ), + ], + ); + } + } + ); + } +} diff --git a/frontend/lib/modules/current_trip_save_button.dart b/frontend/lib/modules/current_trip_save_button.dart index d95c580..f4587cf 100644 --- a/frontend/lib/modules/current_trip_save_button.dart +++ b/frontend/lib/modules/current_trip_save_button.dart @@ -1,4 +1,5 @@ +import 'package:anyway/main.dart'; import 'package:anyway/structs/trip.dart'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; @@ -8,6 +9,13 @@ Widget saveButton(Trip trip) => ElevatedButton( onPressed: () async { SharedPreferences prefs = await SharedPreferences.getInstance(); trip.toPrefs(prefs); + rootScaffoldMessengerKey.currentState!.showSnackBar( + SnackBar( + content: Text('Trip saved'), + duration: Duration(seconds: 2), + dismissDirection: DismissDirection.horizontal + ) + ); }, child: SizedBox( width: 100, diff --git a/frontend/lib/modules/current_trip_summary.dart b/frontend/lib/modules/current_trip_summary.dart new file mode 100644 index 0000000..77c9461 --- /dev/null +++ b/frontend/lib/modules/current_trip_summary.dart @@ -0,0 +1,31 @@ +import 'package:anyway/structs/trip.dart'; +import 'package:flutter/material.dart'; + +class CurrentTripSummary extends StatefulWidget { + final Trip trip; + const CurrentTripSummary({ + super.key, + required this.trip, + }); + + @override + State createState() => _CurrentTripSummaryState(); +} + +class _CurrentTripSummaryState extends State { + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text('Summary'), + // Text('Start: ${widget.trip.start}'), + // Text('End: ${widget.trip.end}'), + Text('Total duration: ${widget.trip.totalTime}'), + Text('Total distance: ${widget.trip.totalTime}'), + // Text('Fuel: ${widget.trip.fuel}'), + // Text('Cost: ${widget.trip.cost}'), + ], + ); + + } +} \ No newline at end of file diff --git a/frontend/lib/modules/landmark_card.dart b/frontend/lib/modules/landmark_card.dart index 3c027e5..43d42ef 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( @@ -40,7 +36,7 @@ class _LandmarkCardState extends State { width: 160, child: CachedNetworkImage( imageUrl: widget.landmark.imageURL ?? '', - placeholder: (context, url) => CircularProgressIndicator(), + placeholder: (context, url) => Center(child: CircularProgressIndicator()), errorWidget: (context, error, stackTrace) => Icon(Icons.question_mark_outlined), // TODO: make this a switch statement to load a placeholder if null // cover the whole container meaning the image will be cropped @@ -88,21 +84,18 @@ class _LandmarkCardState extends State { // show the type, the website, and the wikipedia link as buttons/labels in a row children: [ TextButton.icon( - style: buttonStyle, onPressed: () {}, icon: widget.landmark.type.icon, label: Text(widget.landmark.type.name), ), if (widget.landmark.duration != null && widget.landmark.duration!.inMinutes > 0) TextButton.icon( - style: buttonStyle, onPressed: () {}, icon: Icon(Icons.hourglass_bottom), label: Text('${widget.landmark.duration!.inMinutes} minutes'), ), if (widget.landmark.websiteURL != null) TextButton.icon( - style: buttonStyle, onPressed: () async { // open a browser with the website link await launchUrl(Uri.parse(widget.landmark.websiteURL!)); @@ -112,7 +105,6 @@ class _LandmarkCardState extends State { ), if (widget.landmark.wikipediaURL != null) TextButton.icon( - style: buttonStyle, onPressed: () async { // open a browser with the wikipedia link await launchUrl(Uri.parse(widget.landmark.wikipediaURL!)); diff --git a/frontend/lib/modules/themed_marker.dart b/frontend/lib/modules/landmark_map_marker.dart similarity index 62% rename from frontend/lib/modules/themed_marker.dart rename to frontend/lib/modules/landmark_map_marker.dart index c8b7ea2..2e8f5d7 100644 --- a/frontend/lib/modules/themed_marker.dart +++ b/frontend/lib/modules/landmark_map_marker.dart @@ -1,3 +1,4 @@ +import 'package:anyway/constants.dart'; import 'package:anyway/structs/landmark.dart'; import 'package:flutter/material.dart'; @@ -16,21 +17,9 @@ class ThemedMarker extends StatelessWidget { Widget build(BuildContext context) { // This returns an outlined circle, with an icon corresponding to the landmark type // As a small dot, the number of the landmark is displayed in the top right - Icon icon; - if (landmark.type == sightseeing) { - icon = Icon(Icons.church, color: Colors.black, size: 50); - } else if (landmark.type == nature) { - icon = Icon(Icons.park, color: Colors.black, size: 50); - } else if (landmark.type == shopping) { - icon = Icon(Icons.shopping_cart, color: Colors.black, size: 50); - } else if (landmark.type == start || landmark.type == finish) { - icon = Icon(Icons.flag, color: Colors.black, size: 50); - } else { - icon = Icon(Icons.location_on, color: Colors.black, size: 50); - } Widget? positionIndicator; - if (landmark.type != start && landmark.type != finish) { + if (landmark.type != typeStart && landmark.type != typeFinish) { positionIndicator = Positioned( top: 0, right: 0, @@ -51,14 +40,14 @@ class ThemedMarker extends StatelessWidget { children: [ Container( decoration: BoxDecoration( - gradient: LinearGradient( - colors: [Colors.red, Colors.yellow] - ), + gradient: APP_GRADIENT, shape: BoxShape.circle, border: Border.all(color: Colors.black, width: 5), ), - padding: EdgeInsets.all(5), - child: icon + width: 70, + height: 70, + padding: const EdgeInsets.all(5), + child: Icon(landmark.type.icon.icon, size: 50), ), if (positionIndicator != null) positionIndicator, ], 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..2f285c1 100644 --- a/frontend/lib/modules/new_trip_location_search.dart +++ b/frontend/lib/modules/new_trip_location_search.dart @@ -6,9 +6,13 @@ import 'dart:developer'; import 'package:anyway/structs/trip.dart'; import 'package:flutter/material.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class NewTripLocationSearch extends StatefulWidget { + Future prefs = SharedPreferences.getInstance(); Trip trip; + NewTripLocationSearch( this.trip, ); @@ -39,26 +43,66 @@ class _NewTripLocationSearchState extends State { uuid: 'pending', name: query, location: [location.latitude, location.longitude], - type: start + type: typeStart ) ); } } - @override - Widget build(BuildContext context) { - return SearchBar( - hintText: 'Enter a city name or long press on the map.', - onSubmitted: setTripLocation, - controller: _controller, - leading: Icon(Icons.search), - trailing: [ElevatedButton( + late Widget locationSearchBar = SearchBar( + hintText: 'Enter a city name or long press on the map.', + onSubmitted: setTripLocation, + controller: _controller, + leading: Icon(Icons.search), + trailing: [ + ElevatedButton( onPressed: () { setTripLocation(_controller.text); }, child: Text('Search'), - ),] + ) + ] + ); + + late Widget useCurrentLocationButton = ElevatedButton( + onPressed: () async { + // this widget is only shown if the user has already granted location permissions + Position position = await Geolocator.getCurrentPosition(); + widget.trip.landmarks.clear(); + widget.trip.addLandmark( + Landmark( + uuid: 'pending', + name: 'start', + location: [position.latitude, position.longitude], + type: typeStart + ) + ); + }, + child: Text('Use current location'), + ); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: widget.prefs, + builder: (context, snapshot) { + if (snapshot.hasData) { + final useLocation = snapshot.data!.getBool('useLocation') ?? false; + if (useLocation) { + return Column( + children: [ + locationSearchBar, + useCurrentLocationButton, + ], + ); + } else { + return locationSearchBar; + } + } else { + return locationSearchBar; + } + }, ); } -} \ 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..49445fc 100644 --- a/frontend/lib/modules/new_trip_map.dart +++ b/frontend/lib/modules/new_trip_map.dart @@ -1,13 +1,13 @@ - // A map that allows the user to select a location for a new trip. import 'dart:developer'; import 'package:anyway/constants.dart'; -import 'package:anyway/modules/themed_marker.dart'; +import 'package:anyway/modules/landmark_map_marker.dart'; import 'package:anyway/structs/landmark.dart'; import 'package:anyway/structs/trip.dart'; import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:widget_to_marker/widget_to_marker.dart'; @@ -22,7 +22,7 @@ class NewTripMap extends StatefulWidget { } class _NewTripMapState extends State { - final CameraPosition _cameraPosition = CameraPosition( + final CameraPosition _cameraPosition = const CameraPosition( target: LatLng(48.8566, 2.3522), zoom: 11.0, ); @@ -37,7 +37,7 @@ class _NewTripMapState extends State { uuid: 'pending', name: 'start', location: [location.latitude, location.longitude], - type: start + type: typeStart ) ); } @@ -70,10 +70,26 @@ class _NewTripMapState extends State { } - @override Widget build(BuildContext context) { widget.trip.addListener(updateTripDetails); + Future preferences = SharedPreferences.getInstance(); + + return FutureBuilder( + future: preferences, + builder: (context, snapshot) { + if (snapshot.hasData) { + SharedPreferences prefs = snapshot.data as SharedPreferences; + bool useLocation = prefs.getBool('useLocation') ?? true; + return _buildMap(useLocation); + } else { + return const CircularProgressIndicator(); + } + } + ); + } + + Widget _buildMap(bool useLocation) { return GoogleMap( onMapCreated: _onMapCreated, initialCameraPosition: _cameraPosition, @@ -82,6 +98,8 @@ class _NewTripMapState extends State { cloudMapId: MAP_ID, mapToolbarEnabled: false, zoomControlsEnabled: false, + myLocationButtonEnabled: false, + myLocationEnabled: useLocation, ); } } \ 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/pages/current_trip.dart b/frontend/lib/pages/current_trip.dart index 793f22f..834b3f6 100644 --- a/frontend/lib/pages/current_trip.dart +++ b/frontend/lib/pages/current_trip.dart @@ -1,11 +1,10 @@ -import 'package:anyway/modules/current_trip_save_button.dart'; +import 'package:anyway/constants.dart'; import 'package:flutter/material.dart'; import 'package:sliding_up_panel/sliding_up_panel.dart'; import 'package:anyway/structs/trip.dart'; -import 'package:anyway/modules/current_trip_landmarks_list.dart'; -import 'package:anyway/modules/current_trip_greeter.dart'; import 'package:anyway/modules/current_trip_map.dart'; +import 'package:anyway/modules/current_trip_panel.dart'; @@ -27,16 +26,21 @@ class _TripPageState extends State { @override Widget build(BuildContext context) { return SlidingUpPanel( - panelBuilder: (sc) => _panelFull(sc), - // collapsed: _floatingCollapsed(), - body: MapWidget(trip: widget.trip), - // renderPanelSheet: false, - // backdropEnabled: true, - maxHeight: MediaQuery.of(context).size.height * 0.8, - padding: EdgeInsets.only(left: 10, right: 10, top: 25, bottom: 10), - // panelSnapping: false, - borderRadius: BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)), - boxShadow: [ + // use panelBuilder instead of panel so that we can reuse the scrollcontroller for the listview + panelBuilder: (scrollcontroller) => CurrentTripPanel(controller: scrollcontroller, trip: widget.trip), + // using collapsed and panelBuilder seems to show both at the same time, so we include the greeter in the panelBuilder + // collapsed: Greeter(trip: widget.trip), + body: CurrentTripMap(trip: widget.trip), + minHeight: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT, + maxHeight: MediaQuery.of(context).size.height * TRIP_PANEL_MAX_HEIGHT, + // padding in this context is annoying: it offsets the notion of vertical alignment. + // children that want to be centered vertically need to have their size adjusted by 2x the padding + padding: const EdgeInsets.only(top: 10), + // Panel snapping should not be disabled because it significantly improves the user experience + // panelSnapping: false + borderRadius: const BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)), + parallaxEnabled: true, + boxShadow: const [ BoxShadow( blurRadius: 20.0, color: Colors.black, @@ -44,41 +48,4 @@ class _TripPageState extends State { ], ); } - - - Widget _panelFull(ScrollController sc) { - return ListenableBuilder( - listenable: widget.trip, - builder: (context, child) { - if (widget.trip.uuid != 'pending' && widget.trip.uuid != 'error') { - return ListView( - controller: sc, - padding: EdgeInsets.only(bottom: 35), - children: [ - Greeter(trip: widget.trip), - ...landmarksList(widget.trip), - Padding(padding: EdgeInsets.only(top: 10)), - Center(child: saveButton(widget.trip)), - ], - ); - } else if(widget.trip.uuid == 'pending') { - return Greeter(trip: widget.trip); - } else { - return Column( - children: [ - const Icon( - Icons.error_outline, - color: Colors.red, - size: 60, - ), - Padding( - padding: const EdgeInsets.only(top: 16), - child: Text('Error: ${widget.trip.errorDescription}'), - ), - ], - ); - } - } - ); - } } 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..9f60055 --- /dev/null +++ b/frontend/lib/pages/new_trip_preferences.dart @@ -0,0 +1,113 @@ +import 'package:anyway/modules/new_trip_button.dart'; +import 'package:anyway/structs/preferences.dart'; +import 'package:anyway/structs/trip.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + + + +class NewTripPreferencesPage extends StatefulWidget { + final Trip trip; + const NewTripPreferencesPage({required this.trip}); + + @override + _NewTripPreferencesPageState createState() => _NewTripPreferencesPageState(); +} + +class _NewTripPreferencesPageState extends State { + UserPreferences preferences = UserPreferences(); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: ListView( + children: [ + // Center( + // child: CircleAvatar( + // radius: 100, + // child: Icon(Icons.person, size: 100), + // ) + // ), + Padding(padding: EdgeInsets.only(top: 30)), + Center( + child: FutureBuilder( + future: widget.trip.cityName, + builder: (context, snapshot) => Text( + 'Your trip to ${snapshot.hasData ? snapshot.data! : "..."}', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold) + ) + ) + ), + + Center( + child: Padding( + padding: EdgeInsets.only(left: 10, right: 10, top: 20, bottom: 0), + child: Text('Tell us about your ideal trip.', style: TextStyle(fontSize: 18)) + ), + ), + + Divider(indent: 25, endIndent: 25, height: 50), + + durationPicker(preferences.maxTime), + + preferenceSliders([preferences.sightseeing, preferences.shopping, preferences.nature]), + ] + ), + floatingActionButton: NewTripButton(trip: widget.trip, preferences: preferences), + ); + } + + + Widget durationPicker(SinglePreference maxTime) { + return Card( + margin: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0), + shadowColor: Colors.grey, + child: ListTile( + leading: Icon(Icons.timer), + title: Text(preferences.maxTime.description), + subtitle: CupertinoTimerPicker( + mode: CupertinoTimerPickerMode.hm, + initialTimerDuration: Duration(minutes: 90), + minuteInterval: 15, + onTimerDurationChanged: (Duration newDuration) { + setState(() { + preferences.maxTime.value = newDuration.inMinutes; + }); + }, + ) + ), + ); + } + + + Widget preferenceSliders(List 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(); + }); + }, + ) + ), + ) + ); + } + + 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..8a97c90 --- /dev/null +++ b/frontend/lib/pages/settings.dart @@ -0,0 +1,188 @@ +import 'package:anyway/constants.dart'; +import 'package:anyway/main.dart'; +import 'package:flutter/material.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:url_launcher/url_launcher.dart'; + + +bool debugMode = false; + +class SettingsPage extends StatefulWidget { + @override + _SettingsPageState createState() => _SettingsPageState(); +} + +class _SettingsPageState extends State { + @override + Widget build(BuildContext context) { + return ListView( + padding: EdgeInsets.all(15), + children: [ + // First a round, centered image + Center( + child: CircleAvatar( + radius: 75, + child: Icon(Icons.settings, size: 100), + ) + ), + Center( + child: Text('Global settings', style: TextStyle(fontSize: 24)) + ), + + Divider(indent: 25, endIndent: 25, height: 50), + + darkMode(), + setLocationUsage(), + setDebugMode(), + + Divider(indent: 25, endIndent: 25, height: 50), + + privacyInfo(), + ] + ); + } + + Widget setDebugMode() { + return Row( + children: [ + Text('Debugging: use a custom API URL'), + // white space + Spacer(), + Switch( + value: debugMode, + onChanged: (bool? newValue) { + setState(() { + debugMode = newValue!; + if (debugMode) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text('Debug mode - use a custom API endpoint'), + content: TextField( + decoration: InputDecoration( + hintText: 'https://anyway-stg.anydev.info' + ), + onChanged: (value) { + setState(() { + API_URL_BASE = value; + }); + }, + ), + actions: [ + TextButton( + child: Text('OK'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + } + ); + } + }); + } + ) + ], + ); + } + + Widget darkMode() { + return Row( + children: [ + Text('Dark mode'), + Spacer(), + Switch( + value: Theme.of(context).brightness == Brightness.dark, + onChanged: (bool? newValue) { + setState(() { + rootScaffoldMessengerKey.currentState!.showSnackBar( + SnackBar(content: Text('Dark mode is not implemented yet')) + ); + // if (newValue!) { + // // Dark mode + // Theme.of(context).brightness = Brightness.dark; + // } else { + // // Light mode + // Theme.of(context).brightness = Brightness.light; + // } + }); + } + ) + ], + ); + } + + Widget setLocationUsage() { + Future preferences = SharedPreferences.getInstance(); + return Row( + children: [ + Text('Use location services'), + // white space + Spacer(), + FutureBuilder( + future: preferences, + builder: (context, snapshot) { + if (snapshot.hasData) { + bool useLocation = snapshot.data!.getBool('useLocation') ?? false; + return Switch( + value: useLocation, + onChanged: setUseLocation, + ); + } else { + return CircularProgressIndicator(); + } + } + ) + ], + ); + } + + void setUseLocation(bool newValue) async { + await Permission.locationWhenInUse + .onDeniedCallback(() { + rootScaffoldMessengerKey.currentState!.showSnackBar( + SnackBar(content: Text('Location services are required for this feature')) + ); + }) + .onGrantedCallback(() { + rootScaffoldMessengerKey.currentState!.showSnackBar( + SnackBar(content: Text('Location services are now enabled')) + ); + SharedPreferences.getInstance().then( + (SharedPreferences prefs) { + setState(() { + prefs.setBool('useLocation', newValue); + }); + } + ); + }) + .onPermanentlyDeniedCallback(() { + rootScaffoldMessengerKey.currentState!.showSnackBar( + SnackBar(content: Text('Location services are required for this feature')) + ); + }) + .request(); + } + + Widget privacyInfo() { + return Center( + child: Column( + children: [ + Text('Our privacy policy is available under:'), + + TextButton.icon( + icon: Icon(Icons.info), + label: Text(PRIVACY_URL), + onPressed: () async{ + await launchUrl(Uri.parse(PRIVACY_URL)); + } + ) + ], + ) + ); + } + +} diff --git a/frontend/lib/structs/landmark.dart b/frontend/lib/structs/landmark.dart index e6d3734..6729a65 100644 --- a/frontend/lib/structs/landmark.dart +++ b/frontend/lib/structs/landmark.dart @@ -4,13 +4,13 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; -LandmarkType sightseeing = LandmarkType(name: 'sightseeing'); -LandmarkType nature = LandmarkType(name: 'nature'); -LandmarkType shopping = LandmarkType(name: 'shopping'); +LandmarkType typeSightseeing = LandmarkType(name: 'sightseeing'); +LandmarkType typeNature = LandmarkType(name: 'nature'); +LandmarkType typeShopping = LandmarkType(name: 'shopping'); // LandmarkType museum = LandmarkType(name: 'Museum'); // LandmarkType restaurant = LandmarkType(name: 'Restaurant'); -LandmarkType start = LandmarkType(name: 'start'); -LandmarkType finish = LandmarkType(name: 'finish'); +LandmarkType typeStart = LandmarkType(name: 'start'); +LandmarkType typeFinish = LandmarkType(name: 'finish'); final class Landmark extends LinkedListEntry{ @@ -130,22 +130,22 @@ class LandmarkType { LandmarkType({required this.name, this.icon = const Icon(Icons.location_on)}) { switch (name) { case 'sightseeing': - icon = Icon(Icons.church); + icon = const Icon(Icons.church); break; case 'nature': - icon = Icon(Icons.eco); + icon = const Icon(Icons.eco); break; case 'shopping': - icon = Icon(Icons.shopping_cart); + icon = const Icon(Icons.shopping_cart); break; case 'start': - icon = Icon(Icons.play_arrow); + icon = const Icon(Icons.play_arrow); break; case 'finish': - icon = Icon(Icons.flag); + icon = const Icon(Icons.flag); break; default: - icon = Icon(Icons.location_on); + icon = const Icon(Icons.location_on); } } @override diff --git a/frontend/lib/structs/preferences.dart b/frontend/lib/structs/preferences.dart index 074bbde..e36f35d 100644 --- a/frontend/lib/structs/preferences.dart +++ b/frontend/lib/structs/preferences.dart @@ -1,5 +1,5 @@ +import 'package:anyway/structs/landmark.dart'; import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; class SinglePreference { @@ -20,16 +20,6 @@ class SinglePreference { this.minVal = 0, this.maxVal = 5, }); - - void save() async { - SharedPreferences sharedPrefs = await SharedPreferences.getInstance(); - sharedPrefs.setInt('pref_$slug', value); - } - - void load() async { - SharedPreferences sharedPrefs = await SharedPreferences.getInstance(); - value = sharedPrefs.getInt('pref_$slug') ?? minVal; - } } @@ -39,64 +29,41 @@ class UserPreferences { slug: "sightseeing", description: "How much do you like sightseeing?", value: 0, - icon: Icon(Icons.church), + icon: typeSightseeing.icon, ); SinglePreference shopping = SinglePreference( name: "Shopping", slug: "shopping", description: "How much do you like shopping?", value: 0, - icon: Icon(Icons.shopping_bag), + icon: typeShopping.icon, ); SinglePreference nature = SinglePreference( name: "Nature", slug: "nature", description: "How much do you like nature?", value: 0, - icon: Icon(Icons.landscape), + icon: typeNature.icon, ); SinglePreference maxTime = SinglePreference( name: "Trip duration", slug: "duration", - description: "How long do you want your trip to be?", + description: "How long should your trip be?", value: 30, minVal: 30, maxVal: 720, icon: Icon(Icons.timer), ); - SinglePreference maxDetour = SinglePreference( - name: "Trip detours", - slug: "detours", - description: "Are you okay with roaming even if makes the trip longer?", - value: 0, - maxVal: 30, - icon: Icon(Icons.loupe_sharp), - ); - - Future 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/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/macos/Flutter/GeneratedPluginRegistrant.swift index 63ad0d1..c0164f1 100644 --- a/frontend/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/frontend/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,12 +5,14 @@ import FlutterMacOS import Foundation +import geolocator_apple import path_provider_foundation import shared_preferences_foundation import sqflite import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index e53d9d5..7532658 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -264,6 +264,54 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.0" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: "0ec58b731776bc43097fcf751f79681b6a8f6d3bc737c94779fe9f1ad73c1a81" + url: "https://pub.dev" + source: hosted + version: "13.0.1" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: "7aefc530db47d90d0580b552df3242440a10fe60814496a979aa67aa98b1fd47" + url: "https://pub.dev" + source: hosted + version: "4.6.1" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: bc2aca02423ad429cb0556121f56e60360a2b7d694c8570301d06ea0c00732fd + url: "https://pub.dev" + source: hosted + version: "2.3.7" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "386ce3d9cce47838355000070b1d0b13efb5bc430f8ecda7e9238c8409ace012" + url: "https://pub.dev" + source: hosted + version: "4.2.4" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: "2ed69328e05cd94e7eb48bb0535f5fc0c0c44d1c4fa1e9737267484d05c29b5e" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "53da08937d07c24b0d9952eb57a3b474e29aae2abf9dd717f7e1230995f13f0e" + url: "https://pub.dev" + source: hosted + version: "0.2.3" google_maps: dependency: transitive description: @@ -496,6 +544,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" + url: "https://pub.dev" + source: hosted + version: "11.3.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "76e4ab092c1b240d31177bb64d2b0bea43f43d0e23541ec866151b9f7b2490fa" + url: "https://pub.dev" + source: hosted + version: "12.0.12" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0 + url: "https://pub.dev" + source: hosted + version: "9.4.5" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851 + url: "https://pub.dev" + source: hosted + version: "0.1.3+2" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9 + url: "https://pub.dev" + source: hosted + version: "4.2.3" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" petitparser: dependency: transitive description: @@ -588,10 +684,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: "3a293170d4d9403c3254ee05b84e62e8a9b3c5808ebd17de6a33fe9ea6457936" + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.2" shared_preferences_windows: dependency: transitive description: @@ -825,10 +921,10 @@ packages: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "1.1.0" widget_to_marker: dependency: "direct main" description: diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index ffcce37..98c7fc2 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -49,6 +49,8 @@ dependencies: flutter_svg: ^2.0.10+1 url_launcher: ^6.3.0 flutter_launcher_icons: ^0.13.1 + permission_handler: ^11.3.1 + geolocator: ^13.0.1 dev_dependencies: flutter_test: diff --git a/frontend/windows/flutter/generated_plugin_registrant.cc b/frontend/windows/flutter/generated_plugin_registrant.cc index 4f78848..ce843bc 100644 --- a/frontend/windows/flutter/generated_plugin_registrant.cc +++ b/frontend/windows/flutter/generated_plugin_registrant.cc @@ -6,9 +6,15 @@ #include "generated_plugin_registrant.h" +#include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + GeolocatorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("GeolocatorWindows")); + 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..b3ea692 100644 --- a/frontend/windows/flutter/generated_plugins.cmake +++ b/frontend/windows/flutter/generated_plugins.cmake @@ -3,6 +3,8 @@ # list(APPEND FLUTTER_PLUGIN_LIST + geolocator_windows + permission_handler_windows url_launcher_windows )