better help and onboarding
All checks were successful
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m40s
Build and release debug APK / Build APK (pull_request) Successful in 7m23s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 14s

This commit is contained in:
2024-11-18 17:42:52 +01:00
parent d58ef2562d
commit 3f1fe463bf
19 changed files with 326 additions and 242 deletions

View File

@@ -0,0 +1,143 @@
import 'package:anyway/modules/help_dialog.dart';
import 'package:anyway/pages/current_trip.dart';
import 'package:anyway/pages/settings.dart';
import 'package:flutter/material.dart';
import 'package:anyway/constants.dart';
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_location.dart';
import 'package:anyway/pages/onboarding.dart';
// BasePage is the scaffold that holds a child page and a side drawer
// The side drawer is the main way to switch between pages
class BasePage extends StatefulWidget {
final Widget mainScreen;
final Widget title;
final List<String> helpTexts;
const BasePage({
super.key,
required this.mainScreen,
this.title = const Text(APP_NAME),
this.helpTexts = const [],
});
@override
State<BasePage> createState() => _BasePageState();
}
class _BasePageState extends State<BasePage> {
@override
Widget build(BuildContext context) {
Future<List<Trip>> trips = loadTrips();
return Scaffold(
appBar: AppBar(
title: widget.title,
actions: [
IconButton(
icon: const Icon(Icons.help),
tooltip: 'Help',
onPressed: () {
if (widget.helpTexts.isNotEmpty) {
helpDialog(context, widget.helpTexts[0], widget.helpTexts[1]);
}
}
),
],
),
body: Center(child: widget.mainScreen),
drawer: Drawer(
child: Column(
children: [
Container(
decoration: BoxDecoration(
gradient: APP_GRADIENT,
),
height: 150,
child: Center(
child: Text(
APP_NAME,
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
),
ListTile(
title: const Text('Your Trips'),
leading: const Icon(Icons.map),
// TODO: this is not working!
selected: widget.mainScreen is TripPage,
onTap: () {},
trailing: ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const NewTripPage()
)
);
},
child: const Text('New'),
),
),
// Adds a ListView to the drawer. This ensures the user can scroll
// through the options in the drawer if there isn't enough vertical
// space to fit everything.
Expanded(
child: TripsOverview(trips: trips),
),
ElevatedButton(
onPressed: () async {
removeAllTripsFromPrefs();
},
child: const Text('Clear trips'),
),
const Divider(indent: 10, endIndent: 10),
ListTile(
title: const Text('How to use'),
leading: Icon(Icons.help),
// TODO: this is not working!
selected: widget.mainScreen is OnboardingPage,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => OnboardingPage()
)
);
},
),
// settings in the bottom of the drawer
ListTile(
title: const Text('Settings'),
leading: const Icon(Icons.settings),
// TODO: this is not working!
selected: widget.mainScreen is SettingsPage,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => SettingsPage()
)
);
},
),
],
),
),
);
}
}

View File

@@ -1,4 +1,5 @@
import 'package:anyway/constants.dart';
import 'package:anyway/pages/base_page.dart';
import 'package:flutter/material.dart';
import 'package:sliding_up_panel/sliding_up_panel.dart';
@@ -31,7 +32,8 @@ class _TripPageState extends State<TripPage> {
@override
Widget build(BuildContext context) {
return SlidingUpPanel(
return BasePage(
mainScreen: SlidingUpPanel(
// 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
@@ -52,6 +54,13 @@ class _TripPageState extends State<TripPage> {
color: Colors.black,
)
],
),
title: FutureBuilder(
future: widget.trip.cityName,
builder: (context, snapshot) => Text(
'Your trip to ${snapshot.hasData ? snapshot.data! : "..."}',
)
),
);
}
}

View File

@@ -1,5 +1,5 @@
import 'package:anyway/modules/new_trip_button.dart';
import 'package:anyway/modules/new_trip_options_button.dart';
import 'package:anyway/pages/base_page.dart';
import 'package:flutter/material.dart';
import "package:anyway/structs/trip.dart";
@@ -19,23 +19,28 @@ class _NewTripPageState extends State<NewTripPage> {
final TextEditingController lonController = TextEditingController();
Trip trip = Trip();
@override
Widget build(BuildContext context) {
// floating search bar and map as a background
return Scaffold(
appBar: AppBar(
title: const Text('New Trip'),
return BasePage(
mainScreen: Scaffold(
body: Stack(
children: [
NewTripMap(trip),
Padding(
padding: EdgeInsets.all(15),
child: NewTripLocationSearch(trip),
),
],
),
floatingActionButton: NewTripOptionsButton(trip: trip),
),
body: Stack(
children: [
NewTripMap(trip),
Padding(
padding: EdgeInsets.all(15),
child: NewTripLocationSearch(trip),
),
],
),
floatingActionButton: NewTripOptionsButton(trip: trip),
title: Text("New Trip"),
helpTexts: [
"Setting the start location",
"To set the starting point, type a city name in the search bar. You can also navigate the map like you're used to and long press anywhere to set a starting point."
],
);
}
}

View File

@@ -1,4 +1,5 @@
import 'package:anyway/modules/new_trip_button.dart';
import 'package:anyway/pages/base_page.dart';
import 'package:anyway/structs/preferences.dart';
import 'package:anyway/structs/trip.dart';
import 'package:flutter/cupertino.dart';
@@ -19,41 +20,54 @@ class _NewTripPreferencesPageState extends State<NewTripPreferencesPage> {
@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)
)
)
),
return BasePage(
mainScreen: 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))
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),
Divider(indent: 25, endIndent: 25, height: 50),
durationPicker(preferences.maxTime),
durationPicker(preferences.maxTime),
preferenceSliders([preferences.sightseeing, preferences.shopping, preferences.nature]),
]
preferenceSliders([preferences.sightseeing, preferences.shopping, preferences.nature]),
]
),
floatingActionButton: NewTripButton(trip: widget.trip, preferences: preferences),
),
floatingActionButton: NewTripButton(trip: widget.trip, preferences: preferences),
title: FutureBuilder(
future: widget.trip.cityName,
builder: (context, snapshot) => Text(
'Your trip to ${snapshot.hasData ? snapshot.data! : "..."}',
)
),
helpTexts: [
'Trip preferences',
'Set your preferences for this trip. These will be used to generate a custom itinerary.'
],
);
}

View File

@@ -2,6 +2,26 @@ import 'package:anyway/modules/onboarding_card.dart';
import 'package:anyway/pages/new_trip_location.dart';
import 'package:flutter/material.dart';
const List<Widget> onboardingCards = [
OnboardingCard(
title: "Welcome to anyway!",
description: "Anyway helps you plan a city trip that suits your wishes.",
imagePath: "assets/city.svg"
),
OnboardingCard(
title: "Find your way",
description: "Bored by churches? No problem! Hate shopping? No worries! Instead of suggesting the generic trips that bore you, anyway will try to give you recommendations that really suit you.",
imagePath: "assets/plan.svg"
),
OnboardingCard(
title: "Change your mind",
description: "Feet get sore, the weather changes. Anyway understands that! Move or remove destinations, visit hidden gems along your journey, do your own thing. Anyway adapts to your spontaneous decisions.",
imagePath: "assets/cat.svg"
),
];
class OnboardingPage extends StatefulWidget {
const OnboardingPage({super.key});
@@ -13,24 +33,32 @@ class _OnboardingPageState extends State<OnboardingPage> {
@override
Widget build(BuildContext context) {
final PageController _controller = PageController();
return Scaffold(
body: Stack(
children: [
// horizontally scrollable list of pages
PageView(
// horizontally scrollable list of pages
controller: _controller,
children: [
OnboardingCard(index: 1, title: "Welcome to anyway!", description: "Anyway helps you plan a city trip that suits your wishes.", imagePath: "assets/city.svg"),
OnboardingCard(index: 2, title: "Find your way", description: "Bored by churches? No problem! Hate shopping? No worries! More than showing you the typical 'must-sees' of a city, anyway will try to give you recommendations that really suit you.", imagePath: "assets/plan.svg"),
OnboardingCard(index: 3, title: "Change your mind", description: "Life happens when you're busy making plans. Anyway understands that! Move or remove destinations, visit hidden gems along your journey, do your own thing. Anyway adapts to your spontaneous decisions.", imagePath: "assets/cat.svg"),
],
children: List.generate(
onboardingCards.length,
(index) {
Color currentColor = Colors.red.withAlpha(Colors.red.alpha - index * 30);
return Container(
color: currentColor,
alignment: Alignment.center,
child: onboardingCards[index],
);
}
)
),
],
),
floatingActionButton: FloatingActionButton(
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
if (_controller.page == 2) {
if (_controller.page == onboardingCards.length - 1) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const NewTripPage()
@@ -40,7 +68,24 @@ class _OnboardingPageState extends State<OnboardingPage> {
_controller.nextPage(duration: Duration(milliseconds: 500), curve: Curves.ease);
}
},
child: Icon(Icons.arrow_forward),
label: ListenableBuilder(
listenable: _controller,
builder: (context, child) {
if (_controller.page == onboardingCards.length - 1) {
// icon and text side by side
return Row(
children: [
const Text("Start planning!"),
Padding(padding: const EdgeInsets.only(right: 8.0)),
const Icon(Icons.map_outlined)
],
);
} else {
return const Icon(Icons.arrow_forward);
}
}
)
),
);
}

View File

@@ -1,5 +1,6 @@
import 'package:anyway/constants.dart';
import 'package:anyway/main.dart';
import 'package:anyway/pages/base_page.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -16,30 +17,37 @@ class SettingsPage extends StatefulWidget {
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))
),
return BasePage(
mainScreen: 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),
Divider(indent: 25, endIndent: 25, height: 50),
darkMode(),
setLocationUsage(),
setDebugMode(),
darkMode(),
setLocationUsage(),
setDebugMode(),
Divider(indent: 25, endIndent: 25, height: 50),
Divider(indent: 25, endIndent: 25, height: 50),
privacyInfo(),
]
privacyInfo(),
]
),
title: Text('Settings'),
helpTexts: [
'Settings',
'Preferences set in this page are global and will affect the entire application.'
],
);
}
@@ -169,7 +177,9 @@ class _SettingsPageState extends State<SettingsPage> {
return Center(
child: Column(
children: [
Text('Our privacy policy is available under:'),
Text('AnyWay does not collect or store any of the data that is submitted via the app. The location of your trip is not stored. The location feature is only used to show your current location on the map, it is not transmitted to our servers.', textAlign: TextAlign.center),
Padding(padding: EdgeInsets.only(top: 3)),
Text('Our full privacy policy is available under:', textAlign: TextAlign.center),
TextButton.icon(
icon: Icon(Icons.info),