revamp new trip flow

This commit is contained in:
Remy Moll 2024-09-24 22:58:28 +02:00
parent eaa2334942
commit ed60fcba06
26 changed files with 663 additions and 302 deletions

View File

@ -1,6 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Required to fetch data from the internet. --> <!-- Required to fetch data from the internet. -->
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<!-- Required to show user location -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application <application
android:label="anyway" android:label="anyway"

90
frontend/android/gradlew.bat vendored Executable file
View File

@ -0,0 +1,90 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

View File

@ -1,6 +1,64 @@
import 'package:flutter/material.dart';
const String APP_NAME = 'AnyWay'; const String APP_NAME = 'AnyWay';
String API_URL_BASE = 'https://anyway.anydev.info'; String API_URL_BASE = 'https://anyway.anydev.info';
String PRIVACY_URL = 'https://anydev.info/privacy'; String PRIVACY_URL = 'https://anydev.info/privacy';
const String MAP_ID = '41c21ac9b81dbfd8'; const String MAP_ID = '41c21ac9b81dbfd8';
const Color GRADIENT_START = Color(0xFFF9B208);
const Color GRADIENT_END = Color(0xFFE72E77);
const Color PRIMARY_COLOR = Color(0xFFF38F1A);
ThemeData APP_THEME = ThemeData(
primaryColor: PRIMARY_COLOR,
scaffoldBackgroundColor: Colors.white,
cardColor: Colors.white,
useMaterial3: true,
colorScheme: ColorScheme.light(
primary: PRIMARY_COLOR,
secondary: GRADIENT_END,
surface: Colors.white,
error: Colors.red,
onPrimary: Colors.white,
onSecondary: const Color.fromARGB(255, 30, 22, 22),
onSurface: Colors.black,
onError: Colors.white,
brightness: Brightness.light,
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
backgroundColor: PRIMARY_COLOR,
textStyle: TextStyle(
color: Colors.black,
),
),
),
iconButtonTheme: IconButtonThemeData(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(PRIMARY_COLOR),
)
),
buttonTheme: ButtonThemeData(
buttonColor: PRIMARY_COLOR,
textTheme: ButtonTextTheme.primary,
),
);
const Gradient APP_GRADIENT = LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [GRADIENT_START, GRADIENT_END],
);

View File

@ -1,3 +1,4 @@
import 'package:anyway/pages/settings.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:anyway/constants.dart'; import 'package:anyway/constants.dart';
@ -6,10 +7,9 @@ import 'package:anyway/structs/trip.dart';
import 'package:anyway/modules/trips_saved_list.dart'; import 'package:anyway/modules/trips_saved_list.dart';
import 'package:anyway/utils/load_trips.dart'; import 'package:anyway/utils/load_trips.dart';
import 'package:anyway/pages/new_trip.dart'; import 'package:anyway/pages/new_trip_location.dart';
import 'package:anyway/pages/current_trip.dart'; import 'package:anyway/pages/current_trip.dart';
import 'package:anyway/pages/onboarding.dart'; import 'package:anyway/pages/onboarding.dart';
import 'package:anyway/pages/profile.dart';
@ -62,7 +62,7 @@ class _BasePageState extends State<BasePage> {
) )
); );
}, },
label: Text("Plan a trip now"), label: Text("Plan a trip"),
), ),
); );
} }
@ -74,33 +74,33 @@ class _BasePageState extends State<BasePage> {
} }
} else if (widget.mainScreen == "tutorial") { } else if (widget.mainScreen == "tutorial") {
currentView = OnboardingPage(); currentView = OnboardingPage();
} else if (widget.mainScreen == "profile") { } else if (widget.mainScreen == "settings") {
currentView = ProfilePage(); currentView = SettingsPage();
} }
final ThemeData theme = Theme.of(context);
return Scaffold( return Scaffold(
appBar: AppBar(title: Text(APP_NAME)), appBar: AppBar(title: Text(APP_NAME)),
body: Center(child: currentView), body: Center(child: currentView),
drawer: Drawer( drawer: Drawer(
child: Column( child: Column(
children: [ children: [
DrawerHeader( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient(colors: [Colors.red, Colors.yellow]) gradient: APP_GRADIENT,
), ),
height: 150,
child: Center( child: Center(
child: Text( child: Text(
APP_NAME, APP_NAME,
style: TextStyle( style: TextStyle(
color: Colors.grey[800], color: Colors.white,
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
), ),
), ),
ListTile( ListTile(
title: const Text('Your Trips'), title: const Text('Your Trips'),
leading: const Icon(Icons.map), leading: const Icon(Icons.map),
@ -130,7 +130,7 @@ class _BasePageState extends State<BasePage> {
}, },
child: const Text('Clear trips'), child: const Text('Clear trips'),
), ),
const Divider(), const Divider(indent: 10, endIndent: 10),
ListTile( ListTile(
title: const Text('How to use'), title: const Text('How to use'),
leading: Icon(Icons.help), leading: Icon(Icons.help),
@ -148,11 +148,11 @@ class _BasePageState extends State<BasePage> {
ListTile( ListTile(
title: const Text('Settings'), title: const Text('Settings'),
leading: const Icon(Icons.settings), leading: const Icon(Icons.settings),
selected: widget.mainScreen == "profile", selected: widget.mainScreen == "settings",
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => BasePage(mainScreen: "profile") builder: (context) => BasePage(mainScreen: "settings")
) )
); );
}, },

View File

@ -15,7 +15,7 @@ class App extends StatelessWidget {
return MaterialApp( return MaterialApp(
title: APP_NAME, title: APP_NAME,
home: BasePage(mainScreen: "map"), home: BasePage(mainScreen: "map"),
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.red[600]), theme: APP_THEME,
scaffoldMessengerKey: rootScaffoldMessengerKey scaffoldMessengerKey: rootScaffoldMessengerKey
); );
} }

View File

@ -32,7 +32,6 @@ List<Widget> landmarksList(Trip trip) {
onDismissed: (direction) { onDismissed: (direction) {
log('Removing ${landmark.name}'); log('Removing ${landmark.name}');
trip.removeLandmark(landmark); trip.removeLandmark(landmark);
// Then show a snackbar
rootScaffoldMessengerKey.currentState!.showSnackBar( rootScaffoldMessengerKey.currentState!.showSnackBar(
SnackBar(content: Text("We won't show ${landmark.name} again")) SnackBar(content: Text("We won't show ${landmark.name} again"))

View File

@ -79,7 +79,7 @@ class _MapWidgetState extends State<MapWidget> {
cloudMapId: MAP_ID, cloudMapId: MAP_ID,
mapToolbarEnabled: false, mapToolbarEnabled: false,
zoomControlsEnabled: false, zoomControlsEnabled: false,
myLocationEnabled: true,
); );
} }
} }

View File

@ -18,10 +18,6 @@ class _LandmarkCardState extends State<LandmarkCard> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
ThemeData theme = Theme.of(context); ThemeData theme = Theme.of(context);
ButtonStyle buttonStyle = TextButton.styleFrom(
backgroundColor: Colors.orange,
fixedSize: Size.fromHeight(20)
);
return Container( return Container(
height: 160, height: 160,
child: Card( child: Card(
@ -88,21 +84,21 @@ class _LandmarkCardState extends State<LandmarkCard> {
// show the type, the website, and the wikipedia link as buttons/labels in a row // show the type, the website, and the wikipedia link as buttons/labels in a row
children: [ children: [
TextButton.icon( TextButton.icon(
style: buttonStyle, style: theme.iconButtonTheme.style,
onPressed: () {}, onPressed: () {},
icon: widget.landmark.type.icon, icon: widget.landmark.type.icon,
label: Text(widget.landmark.type.name), label: Text(widget.landmark.type.name),
), ),
if (widget.landmark.duration != null && widget.landmark.duration!.inMinutes > 0) if (widget.landmark.duration != null && widget.landmark.duration!.inMinutes > 0)
TextButton.icon( TextButton.icon(
style: buttonStyle, style: theme.iconButtonTheme.style,
onPressed: () {}, onPressed: () {},
icon: Icon(Icons.hourglass_bottom), icon: Icon(Icons.hourglass_bottom),
label: Text('${widget.landmark.duration!.inMinutes} minutes'), label: Text('${widget.landmark.duration!.inMinutes} minutes'),
), ),
if (widget.landmark.websiteURL != null) if (widget.landmark.websiteURL != null)
TextButton.icon( TextButton.icon(
style: buttonStyle, style: theme.iconButtonTheme.style,
onPressed: () async { onPressed: () async {
// open a browser with the website link // open a browser with the website link
await launchUrl(Uri.parse(widget.landmark.websiteURL!)); await launchUrl(Uri.parse(widget.landmark.websiteURL!));
@ -112,7 +108,7 @@ class _LandmarkCardState extends State<LandmarkCard> {
), ),
if (widget.landmark.wikipediaURL != null) if (widget.landmark.wikipediaURL != null)
TextButton.icon( TextButton.icon(
style: buttonStyle, style: theme.iconButtonTheme.style,
onPressed: () async { onPressed: () async {
// open a browser with the wikipedia link // open a browser with the wikipedia link
await launchUrl(Uri.parse(widget.landmark.wikipediaURL!)); await launchUrl(Uri.parse(widget.landmark.wikipediaURL!));

View File

@ -1,4 +1,5 @@
import 'package:anyway/layout.dart'; import 'package:anyway/layout.dart';
import 'package:anyway/main.dart';
import 'package:anyway/structs/preferences.dart'; import 'package:anyway/structs/preferences.dart';
import 'package:anyway/structs/trip.dart'; import 'package:anyway/structs/trip.dart';
import 'package:anyway/utils/fetch_trip.dart'; import 'package:anyway/utils/fetch_trip.dart';
@ -8,8 +9,12 @@ import 'package:flutter/material.dart';
class NewTripButton extends StatefulWidget { class NewTripButton extends StatefulWidget {
final Trip trip; final Trip trip;
final UserPreferences preferences;
const NewTripButton({required this.trip}); const NewTripButton({
required this.trip,
required this.preferences,
});
@override @override
State<NewTripButton> createState() => _NewTripButtonState(); State<NewTripButton> createState() => _NewTripButtonState();
@ -23,42 +28,39 @@ class _NewTripButtonState extends State<NewTripButton> {
listenable: widget.trip, listenable: widget.trip,
builder: (BuildContext context, Widget? child) { builder: (BuildContext context, Widget? child) {
if (widget.trip.landmarks.isEmpty){ if (widget.trip.landmarks.isEmpty){
// Fallback if the trip setup is lagging behind
// This should in theory never happen
return Container(); return Container();
} }
return FloatingActionButton.extended( return FloatingActionButton.extended(
onPressed: () async { onPressed: onPressed,
Future<UserPreferences> preferences = loadUserPreferences(); icon: const Icon(Icons.add),
Trip trip = widget.trip; label: AutoSizeText('Start planning!'),
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,
);
}
},
)
);
}
); );
} }
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)
)
);
}
}
} }

View File

@ -6,9 +6,12 @@ import 'dart:developer';
import 'package:anyway/structs/trip.dart'; import 'package:anyway/structs/trip.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class NewTripLocationSearch extends StatefulWidget { class NewTripLocationSearch extends StatefulWidget {
Future<SharedPreferences> prefs = SharedPreferences.getInstance();
Trip trip; Trip trip;
NewTripLocationSearch( NewTripLocationSearch(
this.trip, this.trip,
); );
@ -45,20 +48,51 @@ class _NewTripLocationSearchState extends State<NewTripLocationSearch> {
} }
} }
@override late Widget locationSearchBar = SearchBar(
Widget build(BuildContext context) { hintText: 'Enter a city name or long press on the map.',
return SearchBar( onSubmitted: setTripLocation,
hintText: 'Enter a city name or long press on the map.', controller: _controller,
onSubmitted: setTripLocation, leading: Icon(Icons.search),
controller: _controller, trailing: [
leading: Icon(Icons.search), ElevatedButton(
trailing: [ElevatedButton(
onPressed: () { onPressed: () {
setTripLocation(_controller.text); setTripLocation(_controller.text);
}, },
child: Text('Search'), child: Text('Search'),
),] )
]
);
late Widget useCurrentLocationButton = ElevatedButton(
onPressed: () {
// setTripLocation(location);
// TODO: get current location
},
child: Text('Use current location'),
);
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: widget.prefs,
builder: (context, snapshot) {
if (snapshot.hasData) {
final useLocation = snapshot.data!.getBool('useLocation') ?? false;
if (useLocation) {
return Column(
children: [
locationSearchBar,
useCurrentLocationButton,
],
);
} else {
return locationSearchBar;
}
} else {
return locationSearchBar;
}
},
); );
} }
} }

View File

@ -1,4 +1,3 @@
// A map that allows the user to select a location for a new trip. // A map that allows the user to select a location for a new trip.
import 'dart:developer'; import 'dart:developer';
@ -22,7 +21,7 @@ class NewTripMap extends StatefulWidget {
} }
class _NewTripMapState extends State<NewTripMap> { class _NewTripMapState extends State<NewTripMap> {
final CameraPosition _cameraPosition = CameraPosition( final CameraPosition _cameraPosition = const CameraPosition(
target: LatLng(48.8566, 2.3522), target: LatLng(48.8566, 2.3522),
zoom: 11.0, zoom: 11.0,
); );
@ -70,7 +69,6 @@ class _NewTripMapState extends State<NewTripMap> {
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
widget.trip.addListener(updateTripDetails); widget.trip.addListener(updateTripDetails);
@ -82,6 +80,8 @@ class _NewTripMapState extends State<NewTripMap> {
cloudMapId: MAP_ID, cloudMapId: MAP_ID,
mapToolbarEnabled: false, mapToolbarEnabled: false,
zoomControlsEnabled: false, zoomControlsEnabled: false,
// TODO: should be loaded from the sharedprefs
myLocationEnabled: true,
); );
} }
} }

View File

@ -0,0 +1,41 @@
import 'package:anyway/pages/new_trip_preferences.dart';
import 'package:anyway/structs/trip.dart';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
class NewTripOptionsButton extends StatefulWidget {
final Trip trip;
const NewTripOptionsButton({required this.trip});
@override
State<NewTripOptionsButton> createState() => _NewTripOptionsButtonState();
}
class _NewTripOptionsButtonState extends State<NewTripOptionsButton> {
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: widget.trip,
builder: (BuildContext context, Widget? child) {
if (widget.trip.landmarks.isEmpty){
return Container();
}
return FloatingActionButton.extended(
onPressed: () async {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => NewTripPreferencesPage(trip: widget.trip)
)
);
},
icon: const Icon(Icons.add),
label: const AutoSizeText('Set preferences')
);
}
);
}
}

View File

@ -16,20 +16,23 @@ class OnboardingCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Color baseColor = Theme.of(context).primaryColor; Color baseColor = Theme.of(context).colorScheme.secondary;
// have a different color for each card, incrementing the hue // have a different color for each card, incrementing the hue
Color currentColor = baseColor.withAlpha(baseColor.alpha - index * 30); Color currentColor = baseColor.withAlpha(baseColor.alpha - index * 30);
return Container( return Container(
color: currentColor, color: currentColor,
alignment: Alignment.center,
child: Padding( child: Padding(
padding: EdgeInsets.all(20), padding: EdgeInsets.all(20),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
title, title,
style: TextStyle( style: TextStyle(
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white,
), ),
), ),
Padding(padding: EdgeInsets.only(top: 20)), Padding(padding: EdgeInsets.only(top: 20)),
@ -44,7 +47,8 @@ class OnboardingCard extends StatelessWidget {
fontSize: 16, fontSize: 16,
), ),
), ),
],
]
), ),
) )
); );

View File

@ -1,3 +1,4 @@
import 'package:anyway/constants.dart';
import 'package:anyway/structs/landmark.dart'; import 'package:anyway/structs/landmark.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -51,9 +52,7 @@ class ThemedMarker extends StatelessWidget {
children: [ children: [
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: APP_GRADIENT,
colors: [Colors.red, Colors.yellow]
),
shape: BoxShape.circle, shape: BoxShape.circle,
border: Border.all(color: Colors.black, width: 5), border: Border.all(color: Colors.black, width: 5),
), ),

View File

@ -1,11 +1,7 @@
import 'package:anyway/modules/new_trip_button.dart'; import 'package:anyway/modules/new_trip_button.dart';
import 'package:anyway/structs/landmark.dart'; import 'package:anyway/modules/new_trip_options_button.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:geocoding/geocoding.dart';
import 'package:anyway/layout.dart';
import 'package:anyway/utils/fetch_trip.dart';
import 'package:anyway/structs/preferences.dart';
import "package:anyway/structs/trip.dart"; import "package:anyway/structs/trip.dart";
import 'package:anyway/modules/new_trip_location_search.dart'; import 'package:anyway/modules/new_trip_location_search.dart';
import 'package:anyway/modules/new_trip_map.dart'; import 'package:anyway/modules/new_trip_map.dart';
@ -19,7 +15,6 @@ class NewTripPage extends StatefulWidget {
} }
class _NewTripPageState extends State<NewTripPage> { class _NewTripPageState extends State<NewTripPage> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final TextEditingController latController = TextEditingController(); final TextEditingController latController = TextEditingController();
final TextEditingController lonController = TextEditingController(); final TextEditingController lonController = TextEditingController();
Trip trip = Trip(); Trip trip = Trip();
@ -40,7 +35,7 @@ class _NewTripPageState extends State<NewTripPage> {
), ),
], ],
), ),
floatingActionButton: NewTripButton(trip: trip), floatingActionButton: NewTripOptionsButton(trip: trip),
); );
} }
} }

View File

@ -0,0 +1,114 @@
import 'package:anyway/modules/new_trip_button.dart';
import 'package:anyway/structs/preferences.dart';
import 'package:anyway/structs/trip.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class NewTripPreferencesPage extends StatefulWidget {
final Trip trip;
const NewTripPreferencesPage({required this.trip});
@override
_NewTripPreferencesPageState createState() => _NewTripPreferencesPageState();
}
class _NewTripPreferencesPageState extends State<NewTripPreferencesPage> {
UserPreferences preferences = UserPreferences();
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
// Center(
// child: CircleAvatar(
// radius: 100,
// child: Icon(Icons.person, size: 100),
// )
// ),
Center(
child: FutureBuilder(
future: widget.trip.cityName,
builder: (context, snapshot) => Text(
'New trip to ${snapshot.hasData ? snapshot.data! : "..."}',
style: TextStyle(fontSize: 24)
)
)
),
Divider(indent: 25, endIndent: 25, height: 50),
Center(
child: Padding(
padding: EdgeInsets.only(left: 10, right: 10, top: 0, bottom: 10),
child: Text('Tell us about your ideal trip.', style: TextStyle(fontSize: 18))
),
),
durationPicker(preferences.maxTime),
preferenceSliders([preferences.sightseeing, preferences.shopping, preferences.nature]),
]
),
floatingActionButton: NewTripButton(trip: widget.trip, preferences: preferences),
);
}
Widget durationPicker(SinglePreference maxTime) {
return Card(
margin: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0),
shadowColor: Colors.grey,
child: ListTile(
leading: Icon(Icons.timer),
title: Text('How long should the trip be?'),
subtitle: CupertinoTimerPicker(
mode: CupertinoTimerPickerMode.hm,
initialTimerDuration: Duration(minutes: 90),
minuteInterval: 15,
onTimerDurationChanged: (Duration newDuration) {
setState(() {
preferences.maxTime.value = newDuration.inMinutes;
});
},
)
),
);
}
Widget preferenceSliders(List<SinglePreference> prefs) {
List<Card> sliders = [];
for (SinglePreference pref in prefs) {
sliders.add(
Card(
child: ListTile(
leading: pref.icon,
title: Text(pref.name),
subtitle: Slider(
value: pref.value.toDouble(),
min: pref.minVal.toDouble(),
max: pref.maxVal.toDouble(),
divisions: pref.maxVal - pref.minVal,
label: pref.value.toString(),
onChanged: (double newValue) {
setState(() {
pref.value = newValue.toInt();
});
},
)
),
margin: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0),
shadowColor: Colors.grey,
)
);
}
return Column(
children: sliders
);
}
}

View File

@ -1,5 +1,5 @@
import 'package:anyway/modules/onboarding_card.dart'; import 'package:anyway/modules/onboarding_card.dart';
import 'package:anyway/pages/new_trip.dart'; import 'package:anyway/pages/new_trip_location.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class OnboardingPage extends StatefulWidget { class OnboardingPage extends StatefulWidget {

View File

@ -1,177 +0,0 @@
import 'package:anyway/constants.dart';
import 'package:anyway/structs/preferences.dart';
import 'package:flutter/material.dart';
bool debugMode = false;
class ProfilePage extends StatefulWidget {
@override
_ProfilePageState createState() => _ProfilePageState();
}
class _ProfilePageState extends State<ProfilePage> {
Future<UserPreferences> _prefs = loadUserPreferences();
@override
Widget build(BuildContext context) {
return ListView(
children: [
// First a round, centered image
Center(
child: CircleAvatar(
radius: 100,
child: Icon(Icons.person, size: 100),
)
),
Center(
child: Text('Curious traveler', style: TextStyle(fontSize: 24))
),
Divider(indent: 25, endIndent: 25, height: 50),
Center(
child: Padding(
padding: EdgeInsets.only(left: 10, right: 10, top: 0, bottom: 10),
child: Text('For a tailored experience, please rate your discovery preferences.', style: TextStyle(fontSize: 18))
),
),
FutureBuilder(future: _prefs, builder: futureSliders),
Divider(indent: 25, endIndent: 25, height: 50),
privacyInfo(),
debugButton(),
]
);
}
Widget debugButton() {
return Padding(
padding: EdgeInsets.only(top: 20),
child: Row(
children: [
Text('Debug mode'),
Switch(
value: debugMode,
onChanged: (bool? newValue) {
setState(() {
debugMode = newValue!;
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Debug mode - custom API'),
content: TextField(
decoration: InputDecoration(
hintText: 'http://localhost:8000'
),
onChanged: (value) {
setState(() {
API_URL_BASE = value;
});
},
),
actions: [
TextButton(
child: Text('OK'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
}
);
});
}
)
],
)
);
}
Widget futureSliders(BuildContext context, AsyncSnapshot<UserPreferences> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
UserPreferences prefs = snapshot.data!;
return Column(
children: [
PreferenceSliders(prefs: [prefs.maxTime, prefs.maxDetour]),
Divider(indent: 25, endIndent: 25, height: 50),
PreferenceSliders(prefs: [prefs.sightseeing, prefs.shopping, prefs.nature])
]
);
} else {
return CircularProgressIndicator();
}
}
Widget privacyInfo() {
return Padding(
padding: EdgeInsets.only(top: 20),
child: Row(
children: [
Text('Privacy policy is available at '),
TextButton.icon(
icon: Icon(Icons.info),
label: Text(PRIVACY_URL),
onPressed: () {
}
)
],
)
);
}
}
class PreferenceSliders extends StatefulWidget {
final List<SinglePreference> prefs;
PreferenceSliders({required this.prefs});
@override
State<PreferenceSliders> createState() => _PreferenceSlidersState();
}
class _PreferenceSlidersState extends State<PreferenceSliders> {
@override
Widget build(BuildContext context) {
List<Card> sliders = [];
for (SinglePreference pref in widget.prefs) {
sliders.add(
Card(
child: ListTile(
leading: pref.icon,
title: Text(pref.name),
subtitle: Slider(
value: pref.value.toDouble(),
min: pref.minVal.toDouble(),
max: pref.maxVal.toDouble(),
divisions: pref.maxVal - pref.minVal,
label: pref.value.toString(),
onChanged: (double newValue) {
setState(() {
pref.value = newValue.toInt();
pref.save();
});
},
)
),
margin: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0),
shadowColor: Colors.grey,
)
);
}
return Column(
children: sliders);
}
}

View File

@ -0,0 +1,177 @@
import 'package:anyway/constants.dart';
import 'package:anyway/main.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.dart';
bool debugMode = false;
bool useLocation = false;
class SettingsPage extends StatefulWidget {
@override
_SettingsPageState createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
@override
Widget build(BuildContext context) {
return ListView(
padding: EdgeInsets.all(15),
children: [
// First a round, centered image
Center(
child: CircleAvatar(
radius: 75,
child: Icon(Icons.settings, size: 100),
)
),
Center(
child: Text('Global settings', style: TextStyle(fontSize: 24))
),
Divider(indent: 25, endIndent: 25, height: 50),
darkMode(),
setLocationUsage(),
setDebugMode(),
Divider(indent: 25, endIndent: 25, height: 50),
privacyInfo(),
]
);
}
Widget setDebugMode() {
return Row(
children: [
Text('Debugging: use a custom API URL'),
// white space
Spacer(),
Switch(
value: debugMode,
onChanged: (bool? newValue) {
setState(() {
debugMode = newValue!;
if (debugMode) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Debug mode - use a custom API endpoint'),
content: TextField(
decoration: InputDecoration(
hintText: 'https://anyway-stg.anydev.info'
),
onChanged: (value) {
setState(() {
API_URL_BASE = value;
});
},
),
actions: [
TextButton(
child: Text('OK'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
}
);
}
});
}
)
],
);
}
Widget darkMode() {
return Row(
children: [
Text('Dark mode'),
Spacer(),
Switch(
value: Theme.of(context).brightness == Brightness.dark,
onChanged: (bool? newValue) {
setState(() {
rootScaffoldMessengerKey.currentState!.showSnackBar(
SnackBar(content: Text('Dark mode is not implemented yet'))
);
// if (newValue!) {
// // Dark mode
// Theme.of(context).brightness = Brightness.dark;
// } else {
// // Light mode
// Theme.of(context).brightness = Brightness.light;
// }
});
}
)
],
);
}
Widget setLocationUsage() {
return Row(
children: [
Text('Use location services'),
// white space
Spacer(),
Switch(
value: useLocation,
onChanged: (bool? newValue) async {
await Permission.locationWhenInUse
.onDeniedCallback(() {
rootScaffoldMessengerKey.currentState!.showSnackBar(
SnackBar(content: Text('Location services are required for this feature'))
);
})
.onGrantedCallback(() {
rootScaffoldMessengerKey.currentState!.showSnackBar(
SnackBar(content: Text('Location services are now enabled'))
);
setState(() {
useLocation = newValue!;
});
SharedPreferences.getInstance().then(
(SharedPreferences prefs) {
prefs.setBool('useLocation', useLocation);
}
);
})
.onPermanentlyDeniedCallback(() {
rootScaffoldMessengerKey.currentState!.showSnackBar(
SnackBar(content: Text('Location services are required for this feature'))
);
})
.request();
}
)
],
);
}
Widget privacyInfo() {
return Center(
child: Column(
children: [
Text('Our privacy policy is available under:'),
TextButton.icon(
icon: Icon(Icons.info),
label: Text(PRIVACY_URL),
onPressed: () async{
await launchUrl(Uri.parse(PRIVACY_URL));
}
)
],
)
);
}
}

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class SinglePreference { class SinglePreference {
@ -20,16 +19,6 @@ class SinglePreference {
this.minVal = 0, this.minVal = 0,
this.maxVal = 5, this.maxVal = 5,
}); });
void save() async {
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
sharedPrefs.setInt('pref_$slug', value);
}
void load() async {
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
value = sharedPrefs.getInt('pref_$slug') ?? minVal;
}
} }
@ -65,38 +54,15 @@ class UserPreferences {
maxVal: 720, maxVal: 720,
icon: Icon(Icons.timer), icon: Icon(Icons.timer),
); );
SinglePreference maxDetour = SinglePreference(
name: "Trip detours",
slug: "detours",
description: "Are you okay with roaming even if makes the trip longer?",
value: 0,
maxVal: 30,
icon: Icon(Icons.loupe_sharp),
);
Future<void> load() async {
for (SinglePreference pref in [sightseeing, shopping, nature, maxTime, maxDetour]) {
pref.load();
}
}
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
// This is "opinionated" JSON, corresponding to the backend's expectations // This is "opinionated" JSON, corresponding to the backend's expectations
return { return {
"sightseeing": {"type": "sightseeing", "score": sightseeing.value}, "sightseeing": {"type": "sightseeing", "score": sightseeing.value},
"shopping": {"type": "shopping", "score": shopping.value}, "shopping": {"type": "shopping", "score": shopping.value},
"nature": {"type": "nature", "score": nature.value}, "nature": {"type": "nature", "score": nature.value},
"max_time_minute": maxTime.value, "max_time_minute": maxTime.value
"detour_tolerance_minute": maxDetour.value
}; };
} }
}
Future<UserPreferences> loadUserPreferences() async {
UserPreferences prefs = UserPreferences();
await prefs.load();
return prefs;
} }

View File

@ -29,11 +29,10 @@ Dio dio = Dio(
fetchTrip( fetchTrip(
Trip trip, Trip trip,
Future<UserPreferences> preferences, UserPreferences preferences,
) async { ) async {
UserPreferences prefs = await preferences;
Map<String, dynamic> data = { Map<String, dynamic> data = {
"preferences": prefs.toJson(), "preferences": preferences.toJson(),
"start": trip.landmarks!.first.location, "start": trip.landmarks!.first.location,
}; };
String dataString = jsonEncode(data); String dataString = jsonEncode(data);
@ -47,11 +46,16 @@ fetchTrip(
// handle errors // handle errors
if (response.statusCode != 200) { if (response.statusCode != 200) {
trip.updateUUID("error"); trip.updateUUID("error");
if (response.data["detail"] != null) { String errorDetail;
trip.updateError(response.data["detail"]); if (response.data.runtimeType == String) {
log(response.data["detail"]); errorDetail = response.data;
// throw Exception(response.data["detail"]); } else {
errorDetail = response.data["detail"] ?? "Unknown error";
} }
trip.updateError(errorDetail);
log(errorDetail);
// Actualy no need to throw an exception, we can just log the error and let the user retry
// throw Exception(errorDetail);
} else { } else {
Map<String, dynamic> json = response.data; Map<String, dynamic> json = response.data;

View File

@ -496,6 +496,54 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.0"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb"
url: "https://pub.dev"
source: hosted
version: "11.3.1"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: "76e4ab092c1b240d31177bb64d2b0bea43f43d0e23541ec866151b9f7b2490fa"
url: "https://pub.dev"
source: hosted
version: "12.0.12"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0
url: "https://pub.dev"
source: hosted
version: "9.4.5"
permission_handler_html:
dependency: transitive
description:
name: permission_handler_html
sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851
url: "https://pub.dev"
source: hosted
version: "0.1.3+2"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9
url: "https://pub.dev"
source: hosted
version: "4.2.3"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
url: "https://pub.dev"
source: hosted
version: "0.2.1"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:

View File

@ -49,6 +49,7 @@ dependencies:
flutter_svg: ^2.0.10+1 flutter_svg: ^2.0.10+1
url_launcher: ^6.3.0 url_launcher: ^6.3.0
flutter_launcher_icons: ^0.13.1 flutter_launcher_icons: ^0.13.1
permission_handler: ^11.3.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -6,9 +6,12 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar( UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows")); registry->GetRegistrarForPlugin("UrlLauncherWindows"));
} }

View File

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
permission_handler_windows
url_launcher_windows url_launcher_windows
) )