Merge pull request 'Frontend UX improvements' (#37) from feature/frontend/image-loading into main
Reviewed-on: #37
This commit is contained in:
commit
d31ca9f81f
427
frontend/assets/confused.svg
Normal file
427
frontend/assets/confused.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 40 KiB |
@ -1,10 +1,12 @@
|
|||||||
|
import 'package:anyway/utils/get_first_page.dart';
|
||||||
|
import 'package:anyway/utils/load_trips.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:anyway/constants.dart';
|
import 'package:anyway/constants.dart';
|
||||||
import 'package:anyway/layout.dart';
|
|
||||||
|
|
||||||
void main() => runApp(const App());
|
void main() => runApp(const App());
|
||||||
|
|
||||||
final GlobalKey<ScaffoldMessengerState> rootScaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
final GlobalKey<ScaffoldMessengerState> rootScaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||||
|
final SavedTrips savedTrips = SavedTrips();
|
||||||
|
|
||||||
class App extends StatelessWidget {
|
class App extends StatelessWidget {
|
||||||
const App({super.key});
|
const App({super.key});
|
||||||
@ -14,7 +16,7 @@ class App extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: APP_NAME,
|
title: APP_NAME,
|
||||||
home: BasePage(mainScreen: "map"),
|
home: getFirstPage(),
|
||||||
theme: APP_THEME,
|
theme: APP_THEME,
|
||||||
scaffoldMessengerKey: rootScaffoldMessengerKey
|
scaffoldMessengerKey: rootScaffoldMessengerKey
|
||||||
);
|
);
|
||||||
|
@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:anyway/modules/landmark_card.dart';
|
import 'package:anyway/modules/landmark_card.dart';
|
||||||
import 'package:anyway/structs/landmark.dart';
|
import 'package:anyway/structs/landmark.dart';
|
||||||
import 'package:anyway/structs/trip.dart';
|
import 'package:anyway/structs/trip.dart';
|
||||||
import 'package:anyway/main.dart';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -25,30 +24,7 @@ List<Widget> landmarksList(Trip trip) {
|
|||||||
|
|
||||||
for (Landmark landmark in trip.landmarks) {
|
for (Landmark landmark in trip.landmarks) {
|
||||||
children.add(
|
children.add(
|
||||||
Dismissible(
|
LandmarkCard(landmark, trip),
|
||||||
key: ValueKey<int>(landmark.hashCode),
|
|
||||||
child: LandmarkCard(landmark),
|
|
||||||
dismissThresholds: {DismissDirection.endToStart: 0.95, DismissDirection.startToEnd: 0.95},
|
|
||||||
onDismissed: (direction) {
|
|
||||||
log('Removing ${landmark.name}');
|
|
||||||
trip.removeLandmark(landmark);
|
|
||||||
|
|
||||||
rootScaffoldMessengerKey.currentState!.showSnackBar(
|
|
||||||
SnackBar(content: Text("We won't show ${landmark.name} again"))
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
background: Container(color: Colors.red),
|
|
||||||
secondaryBackground: Container(
|
|
||||||
color: Colors.red,
|
|
||||||
child: Icon(
|
|
||||||
Icons.delete,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
padding: EdgeInsets.all(15),
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (landmark.next != null) {
|
if (landmark.next != null) {
|
||||||
|
@ -1,9 +1,20 @@
|
|||||||
|
import 'package:anyway/constants.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:auto_size_text/auto_size_text.dart';
|
import 'package:auto_size_text/auto_size_text.dart';
|
||||||
|
|
||||||
import 'package:anyway/structs/trip.dart';
|
import 'package:anyway/structs/trip.dart';
|
||||||
import 'package:anyway/pages/current_trip.dart';
|
import 'package:anyway/pages/current_trip.dart';
|
||||||
|
|
||||||
|
|
||||||
|
final List<String> statusTexts = [
|
||||||
|
'Parsing your preferences...',
|
||||||
|
'Finding the best places...',
|
||||||
|
'Crunching the numbers...',
|
||||||
|
'Calculating the best route...',
|
||||||
|
'Making sure you have a great time...',
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
class CurrentTripLoadingIndicator extends StatefulWidget {
|
class CurrentTripLoadingIndicator extends StatefulWidget {
|
||||||
final Trip trip;
|
final Trip trip;
|
||||||
const CurrentTripLoadingIndicator({
|
const CurrentTripLoadingIndicator({
|
||||||
@ -15,46 +26,137 @@ class CurrentTripLoadingIndicator extends StatefulWidget {
|
|||||||
State<CurrentTripLoadingIndicator> createState() => _CurrentTripLoadingIndicatorState();
|
State<CurrentTripLoadingIndicator> createState() => _CurrentTripLoadingIndicatorState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class _CurrentTripLoadingIndicatorState extends State<CurrentTripLoadingIndicator> {
|
class _CurrentTripLoadingIndicatorState extends State<CurrentTripLoadingIndicator> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Center(
|
Widget build(BuildContext context) => Stack(
|
||||||
child: FutureBuilder(
|
fit: StackFit.expand,
|
||||||
future: widget.trip.cityName,
|
children: [
|
||||||
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
|
// In the very center of the panel, show the greeter which tells the user that the trip is being generated
|
||||||
Widget greeter;
|
Center(child: loadingText(widget.trip)),
|
||||||
Widget loadingIndicator = const Padding(
|
// As a gimmick, and a way to show that the app is still working, show a few loading dots
|
||||||
padding: EdgeInsets.only(top: 10),
|
Align(
|
||||||
child: CircularProgressIndicator()
|
alignment: Alignment.bottomCenter,
|
||||||
);
|
child: statusText(),
|
||||||
|
)
|
||||||
if (snapshot.hasData) {
|
],
|
||||||
greeter = AutoSizeText(
|
|
||||||
maxLines: 1,
|
|
||||||
'Generating your trip to ${snapshot.data}...',
|
|
||||||
style: greeterStyle,
|
|
||||||
);
|
|
||||||
} else if (snapshot.hasError) {
|
|
||||||
// the exact error is shown in the central part of the trip overview. No need to show it here
|
|
||||||
greeter = AutoSizeText(
|
|
||||||
maxLines: 1,
|
|
||||||
'Error while loading trip.',
|
|
||||||
style: greeterStyle,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
greeter = AutoSizeText(
|
|
||||||
maxLines: 1,
|
|
||||||
'Generating your trip...',
|
|
||||||
style: greeterStyle,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
greeter,
|
|
||||||
loadingIndicator,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// automatically cycle through the greeter texts
|
||||||
|
class statusText extends StatefulWidget {
|
||||||
|
const statusText({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_statusTextState createState() => _statusTextState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _statusTextState extends State<statusText> {
|
||||||
|
int statusIndex = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
Future.delayed(Duration(seconds: 5), () {
|
||||||
|
setState(() {
|
||||||
|
statusIndex = (statusIndex + 1) % statusTexts.length;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AutoSizeText(
|
||||||
|
statusTexts[statusIndex],
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Widget loadingText(Trip trip) => FutureBuilder(
|
||||||
|
future: trip.cityName,
|
||||||
|
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
|
||||||
|
Widget greeter;
|
||||||
|
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
greeter = AnimatedGradientText(
|
||||||
|
text: 'Creating your trip to ${snapshot.data}...',
|
||||||
|
style: greeterStyle,
|
||||||
|
);
|
||||||
|
} else if (snapshot.hasError) {
|
||||||
|
// the exact error is shown in the central part of the trip overview. No need to show it here
|
||||||
|
greeter = AnimatedGradientText(
|
||||||
|
text: 'Error while loading trip.',
|
||||||
|
style: greeterStyle,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
greeter = AnimatedGradientText(
|
||||||
|
text: 'Creating your trip...',
|
||||||
|
style: greeterStyle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return greeter;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
class AnimatedGradientText extends StatefulWidget {
|
||||||
|
final String text;
|
||||||
|
final TextStyle style;
|
||||||
|
|
||||||
|
const AnimatedGradientText({
|
||||||
|
Key? key,
|
||||||
|
required this.text,
|
||||||
|
required this.style,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_AnimatedGradientTextState createState() => _AnimatedGradientTextState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AnimatedGradientTextState extends State<AnimatedGradientText> with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
duration: const Duration(seconds: 1),
|
||||||
|
vsync: this,
|
||||||
|
)..repeat();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _controller,
|
||||||
|
builder: (context, child) {
|
||||||
|
return ShaderMask(
|
||||||
|
shaderCallback: (bounds) {
|
||||||
|
return LinearGradient(
|
||||||
|
colors: [GRADIENT_START, GRADIENT_END, GRADIENT_START],
|
||||||
|
stops: [
|
||||||
|
_controller.value - 1.0,
|
||||||
|
_controller.value,
|
||||||
|
_controller.value + 1.0,
|
||||||
|
],
|
||||||
|
tileMode: TileMode.mirror,
|
||||||
|
).createShader(bounds);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
widget.text,
|
||||||
|
style: widget.style,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ class _CurrentTripPanelState extends State<CurrentTripPanel> {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
// reuse the exact same height as the panel has when collapsed
|
// reuse the exact same height as the panel has when collapsed
|
||||||
// this way the greeter will be centered when the panel is collapsed
|
// this way the greeter will be centered when the panel is collapsed
|
||||||
height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT - 20,
|
height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT,
|
||||||
child: CurrentTripErrorMessage(trip: widget.trip)
|
child: CurrentTripErrorMessage(trip: widget.trip)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -46,19 +46,20 @@ class _CurrentTripPanelState extends State<CurrentTripPanel> {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
// reuse the exact same height as the panel has when collapsed
|
// reuse the exact same height as the panel has when collapsed
|
||||||
// this way the greeter will be centered when the panel is collapsed
|
// this way the greeter will be centered when the panel is collapsed
|
||||||
height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT - 20,
|
height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT,
|
||||||
child: CurrentTripLoadingIndicator(trip: widget.trip),
|
child: CurrentTripLoadingIndicator(trip: widget.trip),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return ListView(
|
return ListView(
|
||||||
controller: widget.controller,
|
controller: widget.controller,
|
||||||
padding: const EdgeInsets.only(bottom: 30),
|
padding: const EdgeInsets.only(top: 10, left: 10, right: 10, bottom: 30),
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
// reuse the exact same height as the panel has when collapsed
|
// reuse the exact same height as the panel has when collapsed
|
||||||
// this way the greeter will be centered when the panel is collapsed
|
// this way the greeter will be centered when the panel is collapsed
|
||||||
height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT - 20,
|
// note that we need to account for the padding above
|
||||||
|
height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT - 10,
|
||||||
child: CurrentTripGreeter(trip: widget.trip),
|
child: CurrentTripGreeter(trip: widget.trip),
|
||||||
),
|
),
|
||||||
|
|
||||||
@ -72,7 +73,7 @@ class _CurrentTripPanelState extends State<CurrentTripPanel> {
|
|||||||
|
|
||||||
const Padding(padding: EdgeInsets.only(top: 10)),
|
const Padding(padding: EdgeInsets.only(top: 10)),
|
||||||
|
|
||||||
Center(child: saveButton(widget.trip)),
|
Center(child: saveButton(trip: widget.trip)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,39 +3,53 @@ import 'package:anyway/main.dart';
|
|||||||
import 'package:anyway/structs/trip.dart';
|
import 'package:anyway/structs/trip.dart';
|
||||||
import 'package:auto_size_text/auto_size_text.dart';
|
import 'package:auto_size_text/auto_size_text.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
|
|
||||||
Widget saveButton(Trip trip) => ElevatedButton(
|
|
||||||
onPressed: () async {
|
class saveButton extends StatefulWidget {
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
Trip trip;
|
||||||
trip.toPrefs(prefs);
|
saveButton({super.key, required this.trip});
|
||||||
rootScaffoldMessengerKey.currentState!.showSnackBar(
|
|
||||||
SnackBar(
|
@override
|
||||||
content: Text('Trip saved'),
|
State<saveButton> createState() => _saveButtonState();
|
||||||
duration: Duration(seconds: 2),
|
}
|
||||||
dismissDirection: DismissDirection.horizontal
|
|
||||||
|
class _saveButtonState extends State<saveButton> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
savedTrips.addTrip(widget.trip);
|
||||||
|
// SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
// setState(() => widget.trip.toPrefs(prefs));
|
||||||
|
rootScaffoldMessengerKey.currentState!.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Trip saved'),
|
||||||
|
duration: Duration(seconds: 2),
|
||||||
|
dismissDirection: DismissDirection.horizontal
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: SizedBox(
|
||||||
|
width: 100,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.save,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(left: 10, top: 5, bottom: 5, right: 5),
|
||||||
|
child: AutoSizeText(
|
||||||
|
'Save trip',
|
||||||
|
maxLines: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
child: SizedBox(
|
}
|
||||||
width: 100,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.save,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.only(left: 10, top: 5, bottom: 5, right: 5),
|
|
||||||
child: AutoSizeText(
|
|
||||||
'Save trip',
|
|
||||||
maxLines: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
|
25
frontend/lib/modules/help_dialog.dart
Normal file
25
frontend/lib/modules/help_dialog.dart
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
Future<void> helpDialog(BuildContext context, String title, String content) {
|
||||||
|
return showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(title),
|
||||||
|
content: Text(content),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
textStyle: Theme.of(context).textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
child: const Text('Got it!'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'package:anyway/main.dart';
|
||||||
|
import 'package:anyway/structs/trip.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
@ -6,8 +8,12 @@ import 'package:anyway/structs/landmark.dart';
|
|||||||
|
|
||||||
class LandmarkCard extends StatefulWidget {
|
class LandmarkCard extends StatefulWidget {
|
||||||
final Landmark landmark;
|
final Landmark landmark;
|
||||||
|
final Trip parentTrip;
|
||||||
|
|
||||||
LandmarkCard(this.landmark);
|
LandmarkCard(
|
||||||
|
this.landmark,
|
||||||
|
this.parentTrip,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_LandmarkCardState createState() => _LandmarkCardState();
|
_LandmarkCardState createState() => _LandmarkCardState();
|
||||||
@ -17,110 +23,149 @@ class LandmarkCard extends StatefulWidget {
|
|||||||
class _LandmarkCardState extends State<LandmarkCard> {
|
class _LandmarkCardState extends State<LandmarkCard> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
ThemeData theme = Theme.of(context);
|
if (widget.landmark.type == typeStart || widget.landmark.type == typeFinish) {
|
||||||
|
return TextButton.icon(
|
||||||
|
onPressed: () {},
|
||||||
|
icon: widget.landmark.type.icon,
|
||||||
|
label: Text(widget.landmark.name),
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
// else:
|
||||||
return Container(
|
return Container(
|
||||||
height: 160,
|
|
||||||
child: Card(
|
child: Card(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(15.0),
|
borderRadius: BorderRadius.circular(15.0),
|
||||||
),
|
),
|
||||||
elevation: 5,
|
elevation: 5,
|
||||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||||
child: Row(
|
// if the image is available, display it on the left side of the card, otherwise only display the text
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: widget.landmark.imageURL != null ? splitLayout() : textLayout(),
|
||||||
children: [
|
),
|
||||||
Container( // the image on the left
|
);
|
||||||
// inherit the height of the parent container
|
}
|
||||||
height: double.infinity,
|
|
||||||
// force a fixed width
|
Widget splitLayout() {
|
||||||
width: 160,
|
// If an image is available, display it on the left side of the card
|
||||||
child: CachedNetworkImage(
|
return Row(
|
||||||
imageUrl: widget.landmark.imageURL ?? '',
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
placeholder: (context, url) => Center(child: CircularProgressIndicator()),
|
children: [
|
||||||
errorWidget: (context, error, stackTrace) => Icon(Icons.question_mark_outlined),
|
Container(
|
||||||
// TODO: make this a switch statement to load a placeholder if null
|
// the image on the left
|
||||||
// cover the whole container meaning the image will be cropped
|
width: 160,
|
||||||
fit: BoxFit.cover,
|
height: 160,
|
||||||
),
|
|
||||||
),
|
child: CachedNetworkImage(
|
||||||
Flexible(
|
imageUrl: widget.landmark.imageURL ?? '',
|
||||||
child: Padding(
|
placeholder: (context, url) => Center(child: CircularProgressIndicator()),
|
||||||
padding: EdgeInsets.all(10),
|
errorWidget: (context, error, stackTrace) => Icon(Icons.question_mark_outlined),
|
||||||
child: Column(
|
fit: BoxFit.cover,
|
||||||
children: [
|
),
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
child: Text(
|
|
||||||
widget.landmark.name,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
maxLines: 2,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (widget.landmark.nameEN != null)
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
child: Text(
|
|
||||||
widget.landmark.nameEN!,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SingleChildScrollView(
|
|
||||||
// allows the buttons to be scrolled
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
child: Wrap(
|
|
||||||
spacing: 10,
|
|
||||||
// show the type, the website, and the wikipedia link as buttons/labels in a row
|
|
||||||
children: [
|
|
||||||
TextButton.icon(
|
|
||||||
onPressed: () {},
|
|
||||||
icon: widget.landmark.type.icon,
|
|
||||||
label: Text(widget.landmark.type.name),
|
|
||||||
),
|
|
||||||
if (widget.landmark.duration != null && widget.landmark.duration!.inMinutes > 0)
|
|
||||||
TextButton.icon(
|
|
||||||
onPressed: () {},
|
|
||||||
icon: Icon(Icons.hourglass_bottom),
|
|
||||||
label: Text('${widget.landmark.duration!.inMinutes} minutes'),
|
|
||||||
),
|
|
||||||
if (widget.landmark.websiteURL != null)
|
|
||||||
TextButton.icon(
|
|
||||||
onPressed: () async {
|
|
||||||
// open a browser with the website link
|
|
||||||
await launchUrl(Uri.parse(widget.landmark.websiteURL!));
|
|
||||||
},
|
|
||||||
icon: Icon(Icons.link),
|
|
||||||
label: Text('Website'),
|
|
||||||
),
|
|
||||||
if (widget.landmark.wikipediaURL != null)
|
|
||||||
TextButton.icon(
|
|
||||||
onPressed: () async {
|
|
||||||
// open a browser with the wikipedia link
|
|
||||||
await launchUrl(Uri.parse(widget.landmark.wikipediaURL!));
|
|
||||||
},
|
|
||||||
icon: Icon(Icons.book),
|
|
||||||
label: Text('Wikipedia'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
Flexible(
|
||||||
|
child: textLayout(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget textLayout() {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.all(10),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
widget.landmark.name,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (widget.landmark.nameEN != null)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
widget.landmark.nameEN!,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Padding(padding: EdgeInsets.only(top: 10)),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
// allows the buttons to be scrolled
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 10,
|
||||||
|
// show the type, the website, and the wikipedia link as buttons/labels in a row
|
||||||
|
children: [
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () {},
|
||||||
|
icon: widget.landmark.type.icon,
|
||||||
|
label: Text(widget.landmark.type.name),
|
||||||
|
),
|
||||||
|
if (widget.landmark.duration != null && widget.landmark.duration!.inMinutes > 0)
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () {},
|
||||||
|
icon: Icon(Icons.hourglass_bottom),
|
||||||
|
label: Text('${widget.landmark.duration!.inMinutes} minutes'),
|
||||||
|
),
|
||||||
|
if (widget.landmark.websiteURL != null)
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () async {
|
||||||
|
// open a browser with the website link
|
||||||
|
await launchUrl(Uri.parse(widget.landmark.websiteURL!));
|
||||||
|
},
|
||||||
|
icon: Icon(Icons.link),
|
||||||
|
label: Text('Website'),
|
||||||
|
),
|
||||||
|
PopupMenuButton(
|
||||||
|
icon: Icon(Icons.settings),
|
||||||
|
style: TextButtonTheme.of(context).style,
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
PopupMenuItem(
|
||||||
|
child: ListTile(
|
||||||
|
leading: Icon(Icons.delete),
|
||||||
|
title: Text('Delete'),
|
||||||
|
onTap: () async {
|
||||||
|
widget.parentTrip.removeLandmark(widget.landmark);
|
||||||
|
rootScaffoldMessengerKey.currentState!.showSnackBar(
|
||||||
|
SnackBar(content: Text("We won't show ${widget.landmark.name} again"))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: ListTile(
|
||||||
|
leading: Icon(Icons.star),
|
||||||
|
title: Text('Favorite'),
|
||||||
|
onTap: () async {
|
||||||
|
// delete the landmark
|
||||||
|
// await deleteLandmark(widget.landmark);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import 'package:anyway/layout.dart';
|
|
||||||
import 'package:anyway/main.dart';
|
import 'package:anyway/main.dart';
|
||||||
|
import 'package:anyway/pages/current_trip.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';
|
||||||
@ -57,7 +57,7 @@ class _NewTripButtonState extends State<NewTripButton> {
|
|||||||
fetchTrip(trip, widget.preferences);
|
fetchTrip(trip, widget.preferences);
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => BasePage(mainScreen: "map", trip: trip)
|
builder: (context) => TripPage(trip: trip)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,15 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
const Map<String, List> debugLocations = {
|
||||||
|
'paris': [48.8575, 2.3514],
|
||||||
|
'london': [51.5074, -0.1278],
|
||||||
|
'new york': [40.7128, -74.0060],
|
||||||
|
'tokyo': [35.6895, 139.6917],
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class NewTripLocationSearch extends StatefulWidget {
|
class NewTripLocationSearch extends StatefulWidget {
|
||||||
Future<SharedPreferences> prefs = SharedPreferences.getInstance();
|
Future<SharedPreferences> prefs = SharedPreferences.getInstance();
|
||||||
Trip trip;
|
Trip trip;
|
||||||
@ -27,26 +36,35 @@ class _NewTripLocationSearchState extends State<NewTripLocationSearch> {
|
|||||||
|
|
||||||
setTripLocation (String query) async {
|
setTripLocation (String query) async {
|
||||||
List<Location> locations = [];
|
List<Location> locations = [];
|
||||||
|
Location startLocation;
|
||||||
log('Searching for: $query');
|
log('Searching for: $query');
|
||||||
|
if (GeocodingPlatform.instance != null) {
|
||||||
try{
|
locations.addAll(await locationFromAddress(query));
|
||||||
locations = await locationFromAddress(query);
|
|
||||||
} catch (e) {
|
|
||||||
log('No results found for: $query : $e');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (locations.isNotEmpty) {
|
if (locations.isNotEmpty) {
|
||||||
Location location = locations.first;
|
startLocation = locations.first;
|
||||||
widget.trip.landmarks.clear();
|
} else {
|
||||||
widget.trip.addLandmark(
|
log('No results found for: $query. Is geocoding available?');
|
||||||
Landmark(
|
log('Setting Fallback location');
|
||||||
uuid: 'pending',
|
List coordinates = debugLocations[query.toLowerCase()] ?? [48.8575, 2.3514];
|
||||||
name: query,
|
startLocation = Location(
|
||||||
location: [location.latitude, location.longitude],
|
latitude: coordinates[0],
|
||||||
type: typeStart
|
longitude: coordinates[1],
|
||||||
)
|
timestamp: DateTime.now(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
widget.trip.landmarks.clear();
|
||||||
|
widget.trip.addLandmark(
|
||||||
|
Landmark(
|
||||||
|
uuid: 'pending',
|
||||||
|
name: query,
|
||||||
|
location: [startLocation.latitude, startLocation.longitude],
|
||||||
|
type: typeStart
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
late Widget locationSearchBar = SearchBar(
|
late Widget locationSearchBar = SearchBar(
|
||||||
|
@ -26,7 +26,7 @@ class _NewTripMapState extends State<NewTripMap> {
|
|||||||
target: LatLng(48.8566, 2.3522),
|
target: LatLng(48.8566, 2.3522),
|
||||||
zoom: 11.0,
|
zoom: 11.0,
|
||||||
);
|
);
|
||||||
late GoogleMapController _mapController;
|
GoogleMapController? _mapController;
|
||||||
final Set<Marker> _markers = <Marker>{};
|
final Set<Marker> _markers = <Marker>{};
|
||||||
|
|
||||||
_onLongPress(LatLng location) {
|
_onLongPress(LatLng location) {
|
||||||
@ -56,11 +56,15 @@ class _NewTripMapState extends State<NewTripMap> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
_mapController.moveCamera(
|
// check if the controller is ready
|
||||||
CameraUpdate.newLatLng(
|
|
||||||
LatLng(landmark.location[0], landmark.location[1])
|
if (_mapController != null) {
|
||||||
)
|
_mapController!.animateCamera(
|
||||||
);
|
CameraUpdate.newLatLng(
|
||||||
|
LatLng(landmark.location[0], landmark.location[1])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,11 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
|
||||||
class OnboardingCard extends StatelessWidget {
|
class OnboardingCard extends StatelessWidget {
|
||||||
int index;
|
final String title;
|
||||||
String title;
|
final String description;
|
||||||
String description;
|
final String imagePath;
|
||||||
String imagePath;
|
|
||||||
|
|
||||||
OnboardingCard({
|
const OnboardingCard({
|
||||||
required this.index,
|
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.description,
|
required this.description,
|
||||||
required this.imagePath,
|
required this.imagePath,
|
||||||
@ -16,41 +14,35 @@ class OnboardingCard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Color baseColor = Theme.of(context).colorScheme.secondary;
|
|
||||||
// have a different color for each card, incrementing the hue
|
return Padding(
|
||||||
Color currentColor = baseColor.withAlpha(baseColor.alpha - index * 30);
|
padding: EdgeInsets.all(20),
|
||||||
return Container(
|
child: Column(
|
||||||
color: currentColor,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
alignment: Alignment.center,
|
children: [
|
||||||
child: Padding(
|
Text(
|
||||||
padding: EdgeInsets.all(20),
|
title,
|
||||||
child: Column(
|
style: TextStyle(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
fontSize: 24,
|
||||||
children: [
|
fontWeight: FontWeight.bold,
|
||||||
Text(
|
color: Colors.white,
|
||||||
title,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Padding(padding: EdgeInsets.only(top: 20)),
|
),
|
||||||
SvgPicture.asset(
|
Padding(padding: EdgeInsets.only(top: 20)),
|
||||||
imagePath,
|
SvgPicture.asset(
|
||||||
height: 200,
|
imagePath,
|
||||||
),
|
height: 200,
|
||||||
Padding(padding: EdgeInsets.only(top: 20)),
|
),
|
||||||
Text(
|
Padding(padding: EdgeInsets.only(top: 20)),
|
||||||
description,
|
Text(
|
||||||
style: TextStyle(
|
description,
|
||||||
fontSize: 16,
|
style: TextStyle(
|
||||||
),
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,11 +1,12 @@
|
|||||||
|
import 'package:anyway/pages/current_trip.dart';
|
||||||
|
import 'package:anyway/utils/load_trips.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:anyway/layout.dart';
|
|
||||||
import 'package:anyway/structs/trip.dart';
|
import 'package:anyway/structs/trip.dart';
|
||||||
|
|
||||||
|
|
||||||
class TripsOverview extends StatefulWidget {
|
class TripsOverview extends StatefulWidget {
|
||||||
final Future<List<Trip>> trips;
|
final SavedTrips trips;
|
||||||
const TripsOverview({
|
const TripsOverview({
|
||||||
super.key,
|
super.key,
|
||||||
required this.trips,
|
required this.trips,
|
||||||
@ -16,50 +17,34 @@ class TripsOverview extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _TripsOverviewState extends State<TripsOverview> {
|
class _TripsOverviewState extends State<TripsOverview> {
|
||||||
|
Widget listBuild (BuildContext context, SavedTrips trips) {
|
||||||
Widget listBuild (BuildContext context, AsyncSnapshot<List<Trip>> snapshot) {
|
|
||||||
List<Widget> children;
|
List<Widget> children;
|
||||||
if (snapshot.hasData) {
|
List<Trip> items = trips.trips;
|
||||||
children = List<Widget>.generate(snapshot.data!.length, (index) {
|
children = List<Widget>.generate(items.length, (index) {
|
||||||
Trip trip = snapshot.data![index];
|
Trip trip = items[index];
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: FutureBuilder(
|
title: FutureBuilder(
|
||||||
future: trip.cityName,
|
future: trip.cityName,
|
||||||
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
|
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
|
||||||
if (snapshot.hasData) {
|
if (snapshot.hasData) {
|
||||||
return Text("Trip to ${snapshot.data}");
|
return Text("Trip to ${snapshot.data}");
|
||||||
} else if (snapshot.hasError) {
|
} else if (snapshot.hasError) {
|
||||||
return Text("Error: ${snapshot.error}");
|
return Text("Error: ${snapshot.error}");
|
||||||
} else {
|
} else {
|
||||||
return const Text("Trip to ...");
|
return const Text("Trip to ...");
|
||||||
}
|
}
|
||||||
},
|
|
||||||
),
|
|
||||||
leading: Icon(Icons.pin_drop),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => BasePage(mainScreen: "map", trip: trip)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
|
||||||
});
|
|
||||||
} else if (snapshot.hasError) {
|
|
||||||
children = [
|
|
||||||
const Icon(
|
|
||||||
Icons.error_outline,
|
|
||||||
color: Colors.red,
|
|
||||||
size: 60,
|
|
||||||
),
|
),
|
||||||
Padding(
|
leading: Icon(Icons.pin_drop),
|
||||||
padding: const EdgeInsets.only(top: 16),
|
onTap: () {
|
||||||
child: Text('Error: ${snapshot.error}'),
|
Navigator.of(context).push(
|
||||||
),
|
MaterialPageRoute(
|
||||||
];
|
builder: (context) => TripPage(trip: trip)
|
||||||
} else {
|
)
|
||||||
children = [Center(child: CircularProgressIndicator())];
|
);
|
||||||
}
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return ListView(
|
return ListView(
|
||||||
children: children,
|
children: children,
|
||||||
@ -69,9 +54,11 @@ class _TripsOverviewState extends State<TripsOverview> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FutureBuilder(
|
return ListenableBuilder(
|
||||||
future: widget.trips,
|
listenable: widget.trips,
|
||||||
builder: listBuild,
|
builder: (BuildContext context, Widget? child) {
|
||||||
|
return listBuild(context, widget.trips);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import 'package:anyway/main.dart';
|
||||||
|
import 'package:anyway/modules/help_dialog.dart';
|
||||||
|
import 'package:anyway/pages/current_trip.dart';
|
||||||
import 'package:anyway/pages/settings.dart';
|
import 'package:anyway/pages/settings.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
@ -8,22 +11,24 @@ 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_location.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/onboarding.dart';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// BasePage is the scaffold that holds all other pages
|
// BasePage is the scaffold that holds a child page and a side drawer
|
||||||
// A side drawer is used to switch between pages
|
// The side drawer is the main way to switch between pages
|
||||||
|
|
||||||
class BasePage extends StatefulWidget {
|
class BasePage extends StatefulWidget {
|
||||||
final String mainScreen;
|
final Widget mainScreen;
|
||||||
final Trip? trip;
|
final Widget title;
|
||||||
|
final List<String> helpTexts;
|
||||||
|
|
||||||
const BasePage({
|
const BasePage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.mainScreen,
|
required this.mainScreen,
|
||||||
this.trip,
|
this.title = const Text(APP_NAME),
|
||||||
|
this.helpTexts = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -34,53 +39,25 @@ class _BasePageState extends State<BasePage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget currentView = const Text("loading...");
|
savedTrips.loadTrips();
|
||||||
Future<List<Trip>> trips = loadTrips();
|
|
||||||
|
|
||||||
|
|
||||||
if (widget.mainScreen == "map") {
|
|
||||||
if (widget.trip != null) {
|
|
||||||
currentView = TripPage(trip: widget.trip!);
|
|
||||||
} else {
|
|
||||||
currentView = FutureBuilder(
|
|
||||||
future: trips,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.hasData) {
|
|
||||||
List<Trip> availableTrips = snapshot.data!;
|
|
||||||
if (availableTrips.isNotEmpty) {
|
|
||||||
return TripPage(trip: availableTrips[0]);
|
|
||||||
} else {
|
|
||||||
return Scaffold(
|
|
||||||
body: Center(
|
|
||||||
child: Text("Wow, so empty!"),
|
|
||||||
),
|
|
||||||
floatingActionButton: FloatingActionButton.extended(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => const NewTripPage()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
label: Text("Plan a trip"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return const Text("loading...");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (widget.mainScreen == "tutorial") {
|
|
||||||
currentView = OnboardingPage();
|
|
||||||
} else if (widget.mainScreen == "settings") {
|
|
||||||
currentView = SettingsPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: Text(APP_NAME)),
|
appBar: AppBar(
|
||||||
body: Center(child: currentView),
|
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(
|
drawer: Drawer(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@ -104,7 +81,8 @@ class _BasePageState extends State<BasePage> {
|
|||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('Your Trips'),
|
title: const Text('Your Trips'),
|
||||||
leading: const Icon(Icons.map),
|
leading: const Icon(Icons.map),
|
||||||
selected: widget.mainScreen == "map",
|
// TODO: this is not working!
|
||||||
|
selected: widget.mainScreen is TripPage,
|
||||||
onTap: () {},
|
onTap: () {},
|
||||||
trailing: ElevatedButton(
|
trailing: ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@ -122,11 +100,11 @@ class _BasePageState extends State<BasePage> {
|
|||||||
// through the options in the drawer if there isn't enough vertical
|
// through the options in the drawer if there isn't enough vertical
|
||||||
// space to fit everything.
|
// space to fit everything.
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TripsOverview(trips: trips),
|
child: TripsOverview(trips: savedTrips),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
removeAllTripsFromPrefs();
|
savedTrips.clearTrips();
|
||||||
},
|
},
|
||||||
child: const Text('Clear trips'),
|
child: const Text('Clear trips'),
|
||||||
),
|
),
|
||||||
@ -134,11 +112,12 @@ class _BasePageState extends State<BasePage> {
|
|||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('How to use'),
|
title: const Text('How to use'),
|
||||||
leading: Icon(Icons.help),
|
leading: Icon(Icons.help),
|
||||||
selected: widget.mainScreen == "tutorial",
|
// TODO: this is not working!
|
||||||
|
selected: widget.mainScreen is OnboardingPage,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => BasePage(mainScreen: "tutorial")
|
builder: (context) => OnboardingPage()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -148,11 +127,12 @@ 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 == "settings",
|
// TODO: this is not working!
|
||||||
|
selected: widget.mainScreen is SettingsPage,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => BasePage(mainScreen: "settings")
|
builder: (context) => SettingsPage()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
},
|
},
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:anyway/constants.dart';
|
import 'package:anyway/constants.dart';
|
||||||
|
import 'package:anyway/pages/base_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:sliding_up_panel/sliding_up_panel.dart';
|
import 'package:sliding_up_panel/sliding_up_panel.dart';
|
||||||
|
|
||||||
@ -10,7 +11,7 @@ final Shader textGradient = APP_GRADIENT.createShader(Rect.fromLTWH(0.0, 0.0, 20
|
|||||||
TextStyle greeterStyle = TextStyle(
|
TextStyle greeterStyle = TextStyle(
|
||||||
foreground: Paint()..shader = textGradient,
|
foreground: Paint()..shader = textGradient,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 26
|
fontSize: 25
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
@ -31,7 +32,8 @@ class _TripPageState extends State<TripPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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
|
// use panelBuilder instead of panel so that we can reuse the scrollcontroller for the listview
|
||||||
panelBuilder: (scrollcontroller) => CurrentTripPanel(controller: scrollcontroller, trip: widget.trip),
|
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
|
// using collapsed and panelBuilder seems to show both at the same time, so we include the greeter in the panelBuilder
|
||||||
@ -41,7 +43,7 @@ class _TripPageState extends State<TripPage> {
|
|||||||
maxHeight: MediaQuery.of(context).size.height * TRIP_PANEL_MAX_HEIGHT,
|
maxHeight: MediaQuery.of(context).size.height * TRIP_PANEL_MAX_HEIGHT,
|
||||||
// padding in this context is annoying: it offsets the notion of vertical alignment.
|
// 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
|
// children that want to be centered vertically need to have their size adjusted by 2x the padding
|
||||||
padding: const EdgeInsets.all(10.0),
|
// padding: const EdgeInsets.all(10.0),
|
||||||
// Panel snapping should not be disabled because it significantly improves the user experience
|
// Panel snapping should not be disabled because it significantly improves the user experience
|
||||||
// panelSnapping: false
|
// panelSnapping: false
|
||||||
borderRadius: const BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)),
|
borderRadius: const BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)),
|
||||||
@ -52,6 +54,13 @@ class _TripPageState extends State<TripPage> {
|
|||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
title: FutureBuilder(
|
||||||
|
future: widget.trip.cityName,
|
||||||
|
builder: (context, snapshot) => Text(
|
||||||
|
'Your trip to ${snapshot.hasData ? snapshot.data! : "..."}',
|
||||||
|
)
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import 'package:anyway/modules/new_trip_button.dart';
|
|
||||||
import 'package:anyway/modules/new_trip_options_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:flutter/material.dart';
|
||||||
|
|
||||||
import "package:anyway/structs/trip.dart";
|
import "package:anyway/structs/trip.dart";
|
||||||
@ -19,23 +19,28 @@ class _NewTripPageState extends State<NewTripPage> {
|
|||||||
final TextEditingController lonController = TextEditingController();
|
final TextEditingController lonController = TextEditingController();
|
||||||
Trip trip = Trip();
|
Trip trip = Trip();
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// floating search bar and map as a background
|
// floating search bar and map as a background
|
||||||
return Scaffold(
|
return BasePage(
|
||||||
appBar: AppBar(
|
mainScreen: Scaffold(
|
||||||
title: const Text('New Trip'),
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
NewTripMap(trip),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.all(15),
|
||||||
|
child: NewTripLocationSearch(trip),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
floatingActionButton: NewTripOptionsButton(trip: trip),
|
||||||
),
|
),
|
||||||
body: Stack(
|
title: Text("New Trip"),
|
||||||
children: [
|
helpTexts: [
|
||||||
NewTripMap(trip),
|
"Setting the start location",
|
||||||
Padding(
|
"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."
|
||||||
padding: EdgeInsets.all(15),
|
],
|
||||||
child: NewTripLocationSearch(trip),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
floatingActionButton: NewTripOptionsButton(trip: trip),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:anyway/modules/new_trip_button.dart';
|
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/preferences.dart';
|
||||||
import 'package:anyway/structs/trip.dart';
|
import 'package:anyway/structs/trip.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
@ -19,41 +20,54 @@ class _NewTripPreferencesPageState extends State<NewTripPreferencesPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return BasePage(
|
||||||
body: ListView(
|
mainScreen: Scaffold(
|
||||||
children: [
|
body: ListView(
|
||||||
// Center(
|
children: [
|
||||||
// child: CircleAvatar(
|
// Center(
|
||||||
// radius: 100,
|
// child: CircleAvatar(
|
||||||
// child: Icon(Icons.person, size: 100),
|
// radius: 100,
|
||||||
// )
|
// child: Icon(Icons.person, size: 100),
|
||||||
// ),
|
// )
|
||||||
Padding(padding: EdgeInsets.only(top: 30)),
|
// ),
|
||||||
Center(
|
// Padding(padding: EdgeInsets.only(top: 30)),
|
||||||
child: FutureBuilder(
|
// Center(
|
||||||
future: widget.trip.cityName,
|
// child: FutureBuilder(
|
||||||
builder: (context, snapshot) => Text(
|
// future: widget.trip.cityName,
|
||||||
'Your trip to ${snapshot.hasData ? snapshot.data! : "..."}',
|
// builder: (context, snapshot) => Text(
|
||||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)
|
// 'Your trip to ${snapshot.hasData ? snapshot.data! : "..."}',
|
||||||
)
|
// style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)
|
||||||
)
|
// )
|
||||||
),
|
// )
|
||||||
|
// ),
|
||||||
|
|
||||||
Center(
|
Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.only(left: 10, right: 10, top: 20, bottom: 0),
|
padding: EdgeInsets.only(left: 10, right: 10, top: 20, bottom: 0),
|
||||||
child: Text('Tell us about your ideal trip.', style: TextStyle(fontSize: 18))
|
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.'
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,33 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:anyway/constants.dart';
|
||||||
import 'package:anyway/modules/onboarding_card.dart';
|
import 'package:anyway/modules/onboarding_card.dart';
|
||||||
import 'package:anyway/pages/new_trip_location.dart';
|
import 'package:anyway/pages/new_trip_location.dart';
|
||||||
import 'package:flutter/material.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"
|
||||||
|
),
|
||||||
|
OnboardingCard(
|
||||||
|
title: "Feeling lost?",
|
||||||
|
description: "Whenever you are confused or need help with the app, look out for the question mark in the top right corner. Help is just a tap away!",
|
||||||
|
imagePath: "assets/confused.svg"
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
class OnboardingPage extends StatefulWidget {
|
class OnboardingPage extends StatefulWidget {
|
||||||
const OnboardingPage({super.key});
|
const OnboardingPage({super.key});
|
||||||
|
|
||||||
@ -10,37 +36,83 @@ class OnboardingPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _OnboardingPageState extends State<OnboardingPage> {
|
class _OnboardingPageState extends State<OnboardingPage> {
|
||||||
|
final PageController _controller = PageController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final PageController _controller = PageController();
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
|
AnimatedBuilder(
|
||||||
|
animation: _controller,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: APP_GRADIENT.colors,
|
||||||
|
stops: [
|
||||||
|
(_controller.hasClients ? _controller.page ?? _controller.initialPage : _controller.initialPage) / onboardingCards.length,
|
||||||
|
(_controller.hasClients ? _controller.page ?? _controller.initialPage + 1 : _controller.initialPage + 1) / onboardingCards.length,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(sigmaX: 100, sigmaY: 100),
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black.withOpacity(0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
PageView(
|
PageView(
|
||||||
// horizontally scrollable list of pages
|
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
|
children: List.generate(
|
||||||
children: [
|
onboardingCards.length,
|
||||||
OnboardingCard(index: 1, title: "Welcome to anyway!", description: "Anyway helps you plan a city trip that suits your wishes.", imagePath: "assets/city.svg"),
|
(index) {
|
||||||
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"),
|
return Container(
|
||||||
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"),
|
alignment: Alignment.center,
|
||||||
],
|
child: onboardingCards[index],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (_controller.page == 2) {
|
if (_controller.page == onboardingCards.length - 1) {
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => const NewTripPage()
|
builder: (context) => const NewTripPage()
|
||||||
)
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_controller.nextPage(duration: Duration(milliseconds: 500), curve: Curves.ease);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label: AnimatedBuilder(
|
||||||
|
animation: _controller,
|
||||||
|
builder: (context, child) {
|
||||||
|
if ((_controller.page ?? _controller.initialPage) == onboardingCards.length - 1) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
const Text("Start planning!"),
|
||||||
|
Padding(padding: const EdgeInsets.only(right: 8.0)),
|
||||||
|
const Icon(Icons.map_outlined)
|
||||||
|
],
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
_controller.nextPage(duration: Duration(milliseconds: 500), curve: Curves.ease);
|
return const Icon(Icons.arrow_forward);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
child: Icon(Icons.arrow_forward),
|
)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:anyway/constants.dart';
|
import 'package:anyway/constants.dart';
|
||||||
import 'package:anyway/main.dart';
|
import 'package:anyway/main.dart';
|
||||||
|
import 'package:anyway/pages/base_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
@ -16,30 +17,37 @@ class SettingsPage extends StatefulWidget {
|
|||||||
class _SettingsPageState extends State<SettingsPage> {
|
class _SettingsPageState extends State<SettingsPage> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListView(
|
return BasePage(
|
||||||
padding: EdgeInsets.all(15),
|
mainScreen: ListView(
|
||||||
children: [
|
padding: EdgeInsets.all(15),
|
||||||
// First a round, centered image
|
children: [
|
||||||
Center(
|
// First a round, centered image
|
||||||
child: CircleAvatar(
|
Center(
|
||||||
radius: 75,
|
child: CircleAvatar(
|
||||||
child: Icon(Icons.settings, size: 100),
|
radius: 75,
|
||||||
)
|
child: Icon(Icons.settings, size: 100),
|
||||||
),
|
)
|
||||||
Center(
|
),
|
||||||
child: Text('Global settings', style: TextStyle(fontSize: 24))
|
Center(
|
||||||
),
|
child: Text('Global settings', style: TextStyle(fontSize: 24))
|
||||||
|
),
|
||||||
|
|
||||||
Divider(indent: 25, endIndent: 25, height: 50),
|
Divider(indent: 25, endIndent: 25, height: 50),
|
||||||
|
|
||||||
darkMode(),
|
darkMode(),
|
||||||
setLocationUsage(),
|
setLocationUsage(),
|
||||||
setDebugMode(),
|
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(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
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(
|
TextButton.icon(
|
||||||
icon: Icon(Icons.info),
|
icon: Icon(Icons.info),
|
||||||
|
@ -24,8 +24,7 @@ final class Landmark extends LinkedListEntry<Landmark>{
|
|||||||
// description to be shown in the overview
|
// description to be shown in the overview
|
||||||
final String? nameEN;
|
final String? nameEN;
|
||||||
final String? websiteURL;
|
final String? websiteURL;
|
||||||
final String? wikipediaURL;
|
String? imageURL; // not final because it can be patched
|
||||||
final String? imageURL;
|
|
||||||
final String? description;
|
final String? description;
|
||||||
final Duration? duration;
|
final Duration? duration;
|
||||||
final bool? visited;
|
final bool? visited;
|
||||||
@ -44,7 +43,6 @@ final class Landmark extends LinkedListEntry<Landmark>{
|
|||||||
|
|
||||||
this.nameEN,
|
this.nameEN,
|
||||||
this.websiteURL,
|
this.websiteURL,
|
||||||
this.wikipediaURL,
|
|
||||||
this.imageURL,
|
this.imageURL,
|
||||||
this.description,
|
this.description,
|
||||||
this.duration,
|
this.duration,
|
||||||
@ -70,7 +68,6 @@ final class Landmark extends LinkedListEntry<Landmark>{
|
|||||||
final isSecondary = json['is_secondary'] as bool?;
|
final isSecondary = json['is_secondary'] as bool?;
|
||||||
final nameEN = json['name_en'] as String?;
|
final nameEN = json['name_en'] as String?;
|
||||||
final websiteURL = json['website_url'] as String?;
|
final websiteURL = json['website_url'] as String?;
|
||||||
final wikipediaURL = json['wikipedia_url'] as String?;
|
|
||||||
final imageURL = json['image_url'] as String?;
|
final imageURL = json['image_url'] as String?;
|
||||||
final description = json['description'] as String?;
|
final description = json['description'] as String?;
|
||||||
var duration = Duration(minutes: json['duration'] ?? 0) as Duration?;
|
var duration = Duration(minutes: json['duration'] ?? 0) as Duration?;
|
||||||
@ -85,7 +82,6 @@ final class Landmark extends LinkedListEntry<Landmark>{
|
|||||||
isSecondary: isSecondary,
|
isSecondary: isSecondary,
|
||||||
nameEN: nameEN,
|
nameEN: nameEN,
|
||||||
websiteURL: websiteURL,
|
websiteURL: websiteURL,
|
||||||
wikipediaURL: wikipediaURL,
|
|
||||||
imageURL: imageURL,
|
imageURL: imageURL,
|
||||||
description: description,
|
description: description,
|
||||||
duration: duration,
|
duration: duration,
|
||||||
@ -112,7 +108,6 @@ final class Landmark extends LinkedListEntry<Landmark>{
|
|||||||
'is_secondary': isSecondary,
|
'is_secondary': isSecondary,
|
||||||
'name_en': nameEN,
|
'name_en': nameEN,
|
||||||
'website_url': websiteURL,
|
'website_url': websiteURL,
|
||||||
'wikipedia_url': wikipediaURL,
|
|
||||||
'image_url': imageURL,
|
'image_url': imageURL,
|
||||||
'description': description,
|
'description': description,
|
||||||
'duration': duration?.inMinutes,
|
'duration': duration?.inMinutes,
|
||||||
@ -130,7 +125,7 @@ class LandmarkType {
|
|||||||
LandmarkType({required this.name, this.icon = const Icon(Icons.location_on)}) {
|
LandmarkType({required this.name, this.icon = const Icon(Icons.location_on)}) {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case 'sightseeing':
|
case 'sightseeing':
|
||||||
icon = const Icon(Icons.church);
|
icon = const Icon(Icons.castle);
|
||||||
break;
|
break;
|
||||||
case 'nature':
|
case 'nature':
|
||||||
icon = const Icon(Icons.eco);
|
icon = const Icon(Icons.eco);
|
||||||
|
@ -113,10 +113,3 @@ LinkedList<Landmark> readLandmarks(SharedPreferences prefs, String? firstUUID) {
|
|||||||
}
|
}
|
||||||
return landmarks;
|
return landmarks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void removeAllTripsFromPrefs () async {
|
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
||||||
prefs.clear();
|
|
||||||
}
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import "dart:convert";
|
import "dart:convert";
|
||||||
import "dart:developer";
|
import "dart:developer";
|
||||||
|
import "package:anyway/utils/load_landmark_image.dart";
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
import 'package:anyway/constants.dart';
|
import 'package:anyway/constants.dart';
|
||||||
@ -85,6 +86,20 @@ fetchTrip(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
patchLandmarkImage(Landmark landmark) async {
|
||||||
|
// patch the landmark to include an image from an external source
|
||||||
|
if (landmark.imageURL == null) {
|
||||||
|
String? newUrl = await getImageUrlFromName(landmark.name);
|
||||||
|
if (newUrl != null) {
|
||||||
|
landmark.imageURL = newUrl;
|
||||||
|
}
|
||||||
|
} else if (landmark.imageURL!.contains("photos.app.goo.gl")) {
|
||||||
|
// the image is a google photos link, we should get the image behind the link
|
||||||
|
String? newUrl = await getImageUrlFromGooglePhotos(landmark.imageURL!);
|
||||||
|
// also set the new url if it is null
|
||||||
|
landmark.imageURL = newUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<(Landmark, String?)> fetchLandmark(String uuid) async {
|
Future<(Landmark, String?)> fetchLandmark(String uuid) async {
|
||||||
final response = await dio.get(
|
final response = await dio.get(
|
||||||
@ -101,5 +116,7 @@ Future<(Landmark, String?)> fetchLandmark(String uuid) async {
|
|||||||
log(response.data.toString());
|
log(response.data.toString());
|
||||||
Map<String, dynamic> json = response.data;
|
Map<String, dynamic> json = response.data;
|
||||||
String? nextUUID = json["next_uuid"];
|
String? nextUUID = json["next_uuid"];
|
||||||
return (Landmark.fromJson(json), nextUUID);
|
Landmark landmark = Landmark.fromJson(json);
|
||||||
|
patchLandmarkImage(landmark);
|
||||||
|
return (landmark, nextUUID);
|
||||||
}
|
}
|
||||||
|
41
frontend/lib/utils/get_first_page.dart
Normal file
41
frontend/lib/utils/get_first_page.dart
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import 'package:anyway/pages/current_trip.dart';
|
||||||
|
import 'package:anyway/pages/onboarding.dart';
|
||||||
|
import 'package:anyway/structs/trip.dart';
|
||||||
|
import 'package:anyway/utils/load_trips.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
Widget getFirstPage() {
|
||||||
|
SavedTrips trips = SavedTrips();
|
||||||
|
trips.loadTrips();
|
||||||
|
|
||||||
|
return ListenableBuilder(
|
||||||
|
listenable: trips,
|
||||||
|
builder: (BuildContext context, Widget? child) {
|
||||||
|
List<Trip> items = trips.trips;
|
||||||
|
if (items.isNotEmpty) {
|
||||||
|
return TripPage(trip: items[0]);
|
||||||
|
} else {
|
||||||
|
return OnboardingPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// Future<List<Trip>> trips = loadTrips();
|
||||||
|
// // test if there are any active trips
|
||||||
|
// // if there are, return the trip list
|
||||||
|
// // if there are not, return the onboarding page
|
||||||
|
// return FutureBuilder(
|
||||||
|
// future: trips,
|
||||||
|
// builder: (context, snapshot) {
|
||||||
|
// if (snapshot.hasData) {
|
||||||
|
// List<Trip> availableTrips = snapshot.data!;
|
||||||
|
// if (availableTrips.isNotEmpty) {
|
||||||
|
// return TripPage(trip: availableTrips[0]);
|
||||||
|
// } else {
|
||||||
|
// return OnboardingPage();
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// return CircularProgressIndicator();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
}
|
71
frontend/lib/utils/load_landmark_image.dart
Normal file
71
frontend/lib/utils/load_landmark_image.dart
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:fuzzywuzzy/model/extracted_result.dart';
|
||||||
|
|
||||||
|
const String baseUrl = "https://en.wikipedia.org/w/api.php";
|
||||||
|
final Dio dio = Dio();
|
||||||
|
|
||||||
|
Future<int?> bestPageMatch(String title) async {
|
||||||
|
final response = await dio.get(baseUrl, queryParameters: {
|
||||||
|
"action": "query",
|
||||||
|
"format": "json",
|
||||||
|
"list": "prefixsearch",
|
||||||
|
"pssearch": title,
|
||||||
|
});
|
||||||
|
|
||||||
|
final data = jsonDecode(response.toString());
|
||||||
|
log(data.toString());
|
||||||
|
final List<dynamic> results = data["query"]["prefixsearch"] ?? {};
|
||||||
|
final Map<String, int> titlesAndIds = {
|
||||||
|
for (var d in results) d["title"]: d["pageid"]
|
||||||
|
};
|
||||||
|
if (titlesAndIds.isEmpty) {
|
||||||
|
log("No pages found for $title");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// after the empty check, we can safely assume that there is a best match
|
||||||
|
final ExtractedResult<String> bestMatch = extractOne(
|
||||||
|
query: title,
|
||||||
|
choices: titlesAndIds.keys.toList(),
|
||||||
|
cutoff: 70,
|
||||||
|
);
|
||||||
|
return titlesAndIds[bestMatch.choice];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> getImageUrl(int pageId) async {
|
||||||
|
final response = await dio.get(baseUrl, queryParameters: {
|
||||||
|
"action": "query",
|
||||||
|
"format": "json",
|
||||||
|
"prop": "pageimages",
|
||||||
|
"pageids": pageId,
|
||||||
|
"pithumbsize": 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
final data = jsonDecode(response.toString());
|
||||||
|
final pageData = data["query"]["pages"][pageId.toString()];
|
||||||
|
return pageData["thumbnail"]?["source"];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> getImageUrlFromName(String title) async {
|
||||||
|
int? pageId = await bestPageMatch(title);
|
||||||
|
if (pageId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await getImageUrl(pageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Future<String?> getImageUrlFromGooglePhotos(String url) async {
|
||||||
|
// this is a very simple implementation that just gets the image behind the link
|
||||||
|
// it is not guaranteed to work for all google photos links
|
||||||
|
final response = await dio.get(url);
|
||||||
|
final data = response.toString();
|
||||||
|
final int start = data.indexOf("https://lh3.googleusercontent.com");
|
||||||
|
final int end = data.indexOf('"', start);
|
||||||
|
return data.substring(start, end);
|
||||||
|
}
|
@ -1,19 +1,39 @@
|
|||||||
import 'dart:collection';
|
|
||||||
|
|
||||||
import 'package:anyway/structs/trip.dart';
|
import 'package:anyway/structs/trip.dart';
|
||||||
import 'package:anyway/structs/landmark.dart';
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
Future<List<Trip>> loadTrips() async {
|
import 'package:flutter/foundation.dart';
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
||||||
|
|
||||||
List<Trip> trips = [];
|
class SavedTrips extends ChangeNotifier {
|
||||||
Set<String> keys = prefs.getKeys();
|
List<Trip> _trips = [];
|
||||||
for (String key in keys) {
|
|
||||||
if (key.startsWith('trip_')) {
|
List<Trip> get trips => _trips;
|
||||||
String uuid = key.replaceFirst('trip_', '');
|
|
||||||
trips.add(Trip.fromPrefs(prefs, uuid));
|
void loadTrips() async {
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
|
List<Trip> trips = [];
|
||||||
|
Set<String> keys = prefs.getKeys();
|
||||||
|
for (String key in keys) {
|
||||||
|
if (key.startsWith('trip_')) {
|
||||||
|
String uuid = key.replaceFirst('trip_', '');
|
||||||
|
trips.add(Trip.fromPrefs(prefs, uuid));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
_trips = trips;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void addTrip(Trip trip) async {
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
trip.toPrefs(prefs);
|
||||||
|
_trips.add(trip);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearTrips () async {
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
prefs.clear();
|
||||||
|
_trips = [];
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
return trips;
|
|
||||||
}
|
}
|
||||||
|
@ -101,10 +101,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: collection
|
name: collection
|
||||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.18.0"
|
version: "1.19.0"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -232,6 +232,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
fuzzywuzzy:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: fuzzywuzzy
|
||||||
|
sha256: "3004379ffd6e7f476a0c2091f38f16588dc45f67de7adf7c41aa85dec06b432c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.0"
|
||||||
geocoding:
|
geocoding:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -404,18 +412,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
|
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.5"
|
version: "10.0.7"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_flutter_testing
|
name: leak_tracker_flutter_testing
|
||||||
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
|
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.5"
|
version: "3.0.8"
|
||||||
leak_tracker_testing:
|
leak_tracker_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -700,7 +708,7 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.99"
|
version: "0.0.0"
|
||||||
sliding_up_panel:
|
sliding_up_panel:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -745,10 +753,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: stack_trace
|
name: stack_trace
|
||||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.11.1"
|
version: "1.12.0"
|
||||||
stream_channel:
|
stream_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -769,10 +777,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: string_scanner
|
name: string_scanner
|
||||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.3.0"
|
||||||
synchronized:
|
synchronized:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -793,10 +801,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
|
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.2"
|
version: "0.7.3"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -913,10 +921,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.2.5"
|
version: "14.3.0"
|
||||||
web:
|
web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -51,6 +51,7 @@ dependencies:
|
|||||||
flutter_launcher_icons: ^0.13.1
|
flutter_launcher_icons: ^0.13.1
|
||||||
permission_handler: ^11.3.1
|
permission_handler: ^11.3.1
|
||||||
geolocator: ^13.0.1
|
geolocator: ^13.0.1
|
||||||
|
fuzzywuzzy: ^1.2.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
// This is a basic Flutter widget test.
|
|
||||||
//
|
|
||||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
|
||||||
// utility in the flutter_test package. For example, you can send tap and scroll
|
|
||||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
|
||||||
// tree, read text, and verify that the values of widget properties are correct.
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
|
|
||||||
// import 'package:anyway/main.dart';
|
|
||||||
import 'package:anyway/layout.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
|
||||||
// Build our app and trigger a frame.
|
|
||||||
await tester.pumpWidget(BasePage(mainScreen: "map",));
|
|
||||||
|
|
||||||
// Verfiy that the title is displayed
|
|
||||||
expect(find.text('City Nav'), findsOneWidget);
|
|
||||||
|
|
||||||
// Tap the '+' icon and trigger a frame.
|
|
||||||
await tester.tap(find.byIcon(Icons.add));
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
// Verify that our counter has incremented.
|
|
||||||
expect(find.text('0'), findsNothing);
|
|
||||||
expect(find.text('1'), findsOneWidget);
|
|
||||||
});
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user