chore(wip): upgrade dependencies, begin refactor
This commit is contained in:
80
frontend/lib/old/constants.dart
Normal file
80
frontend/lib/old/constants.dart
Normal file
@@ -0,0 +1,80 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const String APP_NAME = 'AnyWay';
|
||||
|
||||
String API_URL_BASE = 'https://anyway.anydev.info';
|
||||
String API_URL_DEBUG = 'https://anyway.anydev.info';
|
||||
String PRIVACY_URL = 'https://anydev.info/privacy';
|
||||
|
||||
const String MAP_ID = '41c21ac9b81dbfd8';
|
||||
|
||||
|
||||
const Color GRADIENT_START = Color(0xFFF9B208);
|
||||
const Color GRADIENT_END = Color(0xFFE72E77);
|
||||
|
||||
const Color PRIMARY_COLOR = Color(0xFFF38F1A);
|
||||
|
||||
|
||||
|
||||
const double TRIP_PANEL_MAX_HEIGHT = 0.8;
|
||||
const double TRIP_PANEL_MIN_HEIGHT = 0.12;
|
||||
|
||||
ThemeData APP_THEME = ThemeData(
|
||||
primaryColor: PRIMARY_COLOR,
|
||||
|
||||
scaffoldBackgroundColor: Colors.white,
|
||||
cardColor: Colors.white,
|
||||
useMaterial3: true,
|
||||
|
||||
colorScheme: const ColorScheme.light(
|
||||
primary: PRIMARY_COLOR,
|
||||
secondary: GRADIENT_END,
|
||||
surface: Colors.white,
|
||||
error: Colors.red,
|
||||
onPrimary: Colors.white,
|
||||
onSecondary: Color.fromARGB(255, 30, 22, 22),
|
||||
onSurface: Colors.black,
|
||||
onError: Colors.white,
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
|
||||
|
||||
textButtonTheme: const TextButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
foregroundColor: WidgetStatePropertyAll(PRIMARY_COLOR),
|
||||
side: WidgetStatePropertyAll(
|
||||
BorderSide(
|
||||
color: PRIMARY_COLOR,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
|
||||
elevatedButtonTheme: const ElevatedButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
foregroundColor: WidgetStatePropertyAll(PRIMARY_COLOR),
|
||||
)
|
||||
),
|
||||
|
||||
outlinedButtonTheme: const OutlinedButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
foregroundColor: WidgetStatePropertyAll(PRIMARY_COLOR),
|
||||
)
|
||||
),
|
||||
|
||||
|
||||
sliderTheme: const SliderThemeData(
|
||||
trackHeight: 15,
|
||||
inactiveTrackColor: Colors.grey,
|
||||
thumbColor: PRIMARY_COLOR,
|
||||
activeTrackColor: GRADIENT_END
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
const Gradient APP_GRADIENT = LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [GRADIENT_START, GRADIENT_END],
|
||||
);
|
||||
121
frontend/lib/old/layouts/scaffold.dart
Normal file
121
frontend/lib/old/layouts/scaffold.dart
Normal file
@@ -0,0 +1,121 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:anyway/constants.dart';
|
||||
|
||||
import 'package:anyway/main.dart';
|
||||
import 'package:anyway/modules/help_dialog.dart';
|
||||
import 'package:anyway/modules/trips_saved_list.dart';
|
||||
|
||||
import 'package:anyway/pages/onboarding.dart';
|
||||
import 'package:anyway/pages/current_trip.dart';
|
||||
import 'package:anyway/pages/settings.dart';
|
||||
import 'package:anyway/pages/new_trip_location.dart';
|
||||
|
||||
|
||||
mixin ScaffoldLayout<T extends StatefulWidget> on State<T> {
|
||||
Widget mainScaffold(
|
||||
BuildContext context,
|
||||
{
|
||||
Widget child = const Text("emptiness"),
|
||||
Widget title = const Text(APP_NAME),
|
||||
List<String> helpTexts = const []
|
||||
}
|
||||
) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: title,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.help),
|
||||
tooltip: 'Help',
|
||||
onPressed: () {
|
||||
if (helpTexts.isNotEmpty) {
|
||||
helpDialog(context, helpTexts[0], helpTexts[1]);
|
||||
}
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Center(child: child),
|
||||
drawer: Drawer(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: APP_GRADIENT,
|
||||
),
|
||||
height: 150,
|
||||
child: const 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),
|
||||
selected: widget 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: savedTrips),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
savedTrips.clearTrips();
|
||||
},
|
||||
child: const Text('Clear trips'),
|
||||
),
|
||||
const Divider(indent: 10, endIndent: 10),
|
||||
ListTile(
|
||||
title: const Text('How to use'),
|
||||
leading: const Icon(Icons.help),
|
||||
selected: widget is OnboardingPage,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const OnboardingPage()
|
||||
)
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// settings in the bottom of the drawer
|
||||
ListTile(
|
||||
title: const Text('Settings'),
|
||||
leading: const Icon(Icons.settings),
|
||||
selected: widget is SettingsPage,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SettingsPage()
|
||||
)
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
42
frontend/lib/old/modules/current_trip_error_message.dart
Normal file
42
frontend/lib/old/modules/current_trip_error_message.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CurrentTripErrorMessage extends StatefulWidget {
|
||||
final Trip trip;
|
||||
const CurrentTripErrorMessage({
|
||||
super.key,
|
||||
required this.trip,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CurrentTripErrorMessage> createState() => _CurrentTripErrorMessageState();
|
||||
}
|
||||
|
||||
class _CurrentTripErrorMessageState extends State<CurrentTripErrorMessage> {
|
||||
@override
|
||||
Widget build(BuildContext context) => Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
"😢",
|
||||
style: TextStyle(
|
||||
fontSize: 40,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
AutoSizeText(
|
||||
// at this point the trip is guaranteed to have an error message
|
||||
widget.trip.errorDescription!,
|
||||
maxLines: 30,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
}
|
||||
50
frontend/lib/old/modules/current_trip_greeter.dart
Normal file
50
frontend/lib/old/modules/current_trip_greeter.dart
Normal file
@@ -0,0 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
|
||||
import 'package:anyway/pages/current_trip.dart';
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
|
||||
|
||||
class CurrentTripGreeter extends StatefulWidget {
|
||||
final Trip trip;
|
||||
|
||||
const CurrentTripGreeter({
|
||||
super.key,
|
||||
required this.trip,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CurrentTripGreeter> createState() => _CurrentTripGreeterState();
|
||||
}
|
||||
|
||||
|
||||
class _CurrentTripGreeterState extends State<CurrentTripGreeter> {
|
||||
@override
|
||||
Widget build(BuildContext context) => Center(
|
||||
child: FutureBuilder(
|
||||
future: widget.trip.cityName,
|
||||
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return AutoSizeText(
|
||||
maxLines: 1,
|
||||
'Welcome to ${snapshot.data}!',
|
||||
style: greeterStyle
|
||||
);
|
||||
} else if (snapshot.hasError) {
|
||||
return AutoSizeText(
|
||||
maxLines: 1,
|
||||
'Welcome to your trip!',
|
||||
style: greeterStyle
|
||||
);
|
||||
} else {
|
||||
return AutoSizeText(
|
||||
maxLines: 1,
|
||||
'Welcome to ...',
|
||||
style: greeterStyle
|
||||
);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
44
frontend/lib/old/modules/current_trip_landmarks_list.dart
Normal file
44
frontend/lib/old/modules/current_trip_landmarks_list.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:anyway/structs/landmark.dart';
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:anyway/modules/step_between_landmarks.dart';
|
||||
import 'package:anyway/modules/landmark_card.dart';
|
||||
|
||||
|
||||
// Returns a list of widgets that represent the landmarks matching the given selector
|
||||
List<Widget> landmarksList(Trip trip, {required bool Function(Landmark) selector}) {
|
||||
|
||||
List<Widget> children = [];
|
||||
|
||||
if (trip.landmarks.isEmpty || trip.landmarks.length <= 1 && trip.landmarks.first.type == typeStart ) {
|
||||
children.add(
|
||||
const Text("No landmarks in this trip"),
|
||||
);
|
||||
return children;
|
||||
}
|
||||
|
||||
for (Landmark landmark in trip.landmarks) {
|
||||
if (selector(landmark)) {
|
||||
children.add(
|
||||
LandmarkCard(landmark, trip),
|
||||
);
|
||||
|
||||
if (!landmark.visited) {
|
||||
Landmark? nextLandmark = landmark.next;
|
||||
while (nextLandmark != null && nextLandmark.visited) {
|
||||
nextLandmark = nextLandmark.next;
|
||||
}
|
||||
if (nextLandmark != null) {
|
||||
children.add(
|
||||
StepBetweenLandmarks(current: landmark, next: nextLandmark)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
149
frontend/lib/old/modules/current_trip_loading_indicator.dart
Normal file
149
frontend/lib/old/modules/current_trip_loading_indicator.dart
Normal file
@@ -0,0 +1,149 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
|
||||
import 'package:anyway/structs/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 {
|
||||
final Trip trip;
|
||||
const CurrentTripLoadingIndicator({
|
||||
super.key,
|
||||
required this.trip,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CurrentTripLoadingIndicator> createState() => _CurrentTripLoadingIndicatorState();
|
||||
}
|
||||
|
||||
|
||||
class _CurrentTripLoadingIndicatorState extends State<CurrentTripLoadingIndicator> {
|
||||
@override
|
||||
Widget build(BuildContext context) => Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// In the very center of the panel, show the greeter which tells the user that the trip is being generated
|
||||
Center(child: loadingText(widget.trip)),
|
||||
// As a gimmick, and a way to show that the app is still working, show a few loading dots
|
||||
const Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: 12),
|
||||
child: StatusText(),
|
||||
)
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// automatically cycle through the greeter texts
|
||||
class StatusText extends StatefulWidget {
|
||||
const StatusText({super.key});
|
||||
|
||||
@override
|
||||
_StatusTextState createState() => _StatusTextState();
|
||||
}
|
||||
|
||||
class _StatusTextState extends State<StatusText> {
|
||||
int statusIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Future.delayed(const 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 = AnimatedDotsText(
|
||||
baseText: '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 = Text(
|
||||
'Error while loading trip.',
|
||||
style: greeterStyle,
|
||||
);
|
||||
} else {
|
||||
greeter = AnimatedDotsText(
|
||||
baseText: 'Creating your trip',
|
||||
style: greeterStyle,
|
||||
);
|
||||
}
|
||||
return greeter;
|
||||
}
|
||||
);
|
||||
|
||||
class AnimatedDotsText extends StatefulWidget {
|
||||
final String baseText;
|
||||
final TextStyle style;
|
||||
|
||||
const AnimatedDotsText({
|
||||
super.key,
|
||||
required this.baseText,
|
||||
required this.style,
|
||||
});
|
||||
|
||||
@override
|
||||
_AnimatedDotsTextState createState() => _AnimatedDotsTextState();
|
||||
}
|
||||
|
||||
class _AnimatedDotsTextState extends State<AnimatedDotsText> {
|
||||
int dotCount = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
dotCount = (dotCount + 1) % 4;
|
||||
// show up to 3 dots
|
||||
});
|
||||
} else {
|
||||
timer.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String dots = '.' * dotCount;
|
||||
return AutoSizeText(
|
||||
'${widget.baseText}$dots',
|
||||
style: widget.style,
|
||||
maxLines: 2,
|
||||
);
|
||||
}
|
||||
}
|
||||
53
frontend/lib/old/modules/current_trip_locations.dart
Normal file
53
frontend/lib/old/modules/current_trip_locations.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
|
||||
|
||||
List<Map<String, dynamic>> locationActions = [
|
||||
{'name': 'Toilet', 'action': () {}},
|
||||
{'name': 'Food', 'action': () {}},
|
||||
{'name': 'Surrounding landmarks', 'action': () {}},
|
||||
|
||||
];
|
||||
|
||||
class CurrentTripLocations extends StatefulWidget {
|
||||
final Trip? trip;
|
||||
|
||||
const CurrentTripLocations({super.key, this.trip});
|
||||
|
||||
@override
|
||||
State<CurrentTripLocations> createState() => _CurrentTripLocationsState();
|
||||
}
|
||||
|
||||
class _CurrentTripLocationsState extends State<CurrentTripLocations> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// A horizontally scrolling list of buttons with predefined actions
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0),
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.trip != null)
|
||||
for (Map action in locationActions)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 3.0),
|
||||
child: ElevatedButton(
|
||||
onPressed: action['action'],
|
||||
child: AutoSizeText(
|
||||
action['name'],
|
||||
maxLines: 1,
|
||||
minFontSize: 8,
|
||||
maxFontSize: 16,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
150
frontend/lib/old/modules/current_trip_map.dart
Normal file
150
frontend/lib/old/modules/current_trip_map.dart
Normal file
@@ -0,0 +1,150 @@
|
||||
import 'dart:collection';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:widget_to_marker/widget_to_marker.dart';
|
||||
|
||||
import 'package:anyway/constants.dart';
|
||||
import 'package:anyway/structs/landmark.dart';
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:anyway/modules/landmark_map_marker.dart';
|
||||
|
||||
|
||||
class CurrentTripMap extends StatefulWidget {
|
||||
final Trip? trip;
|
||||
|
||||
const CurrentTripMap({super.key, this.trip});
|
||||
|
||||
@override
|
||||
State<CurrentTripMap> createState() => _CurrentTripMapState();
|
||||
}
|
||||
|
||||
class _CurrentTripMapState extends State<CurrentTripMap> {
|
||||
late GoogleMapController mapController;
|
||||
|
||||
final CameraPosition _cameraPosition = const CameraPosition(
|
||||
target: LatLng(48.8566, 2.3522),
|
||||
zoom: 11.0,
|
||||
);
|
||||
Set<Marker> mapMarkers = <Marker>{};
|
||||
Set<Polyline> mapPolylines = <Polyline>{};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.trip?.addListener(setMapMarkers);
|
||||
widget.trip?.addListener(setMapRoute);
|
||||
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.trip?.removeListener(setMapMarkers);
|
||||
widget.trip?.removeListener(setMapRoute);
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onMapCreated(GoogleMapController controller) async {
|
||||
mapController = controller;
|
||||
List<double>? newLocation = widget.trip?.landmarks.firstOrNull?.location;
|
||||
if (newLocation != null) {
|
||||
CameraUpdate update = CameraUpdate.newLatLng(LatLng(newLocation[0], newLocation[1]));
|
||||
controller.moveCamera(update);
|
||||
}
|
||||
setMapMarkers();
|
||||
setMapRoute();
|
||||
}
|
||||
|
||||
void _onCameraIdle() {
|
||||
// print(mapController.getLatLng(ScreenCoordinate(x: 0, y: 0)));
|
||||
}
|
||||
|
||||
void setMapMarkers() async {
|
||||
Iterator<(int, Landmark)> it = (widget.trip?.landmarks.toList() ?? []).indexed.iterator;
|
||||
|
||||
while (it.moveNext()) {
|
||||
int i = it.current.$1;
|
||||
Landmark landmark = it.current.$2;
|
||||
|
||||
MarkerId markerId = MarkerId("${landmark.uuid} - ${landmark.visited}");
|
||||
List<double> location = landmark.location;
|
||||
// only create a new marker, if there is no marker for this landmark
|
||||
if (!mapMarkers.any((Marker marker) => marker.markerId == markerId)) {
|
||||
Marker marker = Marker(
|
||||
markerId: markerId,
|
||||
position: LatLng(location[0], location[1]),
|
||||
icon: await ThemedMarker(landmark: landmark, position: i).toBitmapDescriptor(
|
||||
logicalSize: const Size(150, 150),
|
||||
imageSize: const Size(150, 150),
|
||||
)
|
||||
);
|
||||
setState(() {
|
||||
mapMarkers.add(marker);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setMapRoute() async {
|
||||
List<Landmark> landmarks = widget.trip?.landmarks.toList() ?? [];
|
||||
Set<Polyline> polyLines = <Polyline>{};
|
||||
|
||||
if (landmarks.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Landmark landmark in landmarks) {
|
||||
if (landmark.next != null) {
|
||||
List<LatLng> step = [
|
||||
LatLng(landmark.location[0], landmark.location[1]),
|
||||
LatLng(landmark.next!.location[0], landmark.next!.location[1])
|
||||
];
|
||||
Polyline stepLine = Polyline(
|
||||
polylineId: PolylineId('step-${landmark.uuid}'),
|
||||
points: step,
|
||||
color: landmark.visited || (landmark.next?.visited ?? false) ? Colors.grey : PRIMARY_COLOR,
|
||||
width: 5
|
||||
);
|
||||
polyLines.add(stepLine);
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
mapPolylines = polyLines;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Future<SharedPreferences> preferences = SharedPreferences.getInstance();
|
||||
|
||||
return FutureBuilder(
|
||||
future: preferences,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
SharedPreferences prefs = snapshot.data as SharedPreferences;
|
||||
bool useLocation = prefs.getBool('useLocation') ?? true;
|
||||
return _buildMap(useLocation);
|
||||
} else {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMap(bool useLocation) {
|
||||
return GoogleMap(
|
||||
onMapCreated: _onMapCreated,
|
||||
initialCameraPosition: _cameraPosition,
|
||||
onCameraIdle: _onCameraIdle,
|
||||
markers: mapMarkers,
|
||||
polylines: mapPolylines,
|
||||
cloudMapId: MAP_ID,
|
||||
mapToolbarEnabled: false,
|
||||
zoomControlsEnabled: false,
|
||||
myLocationEnabled: useLocation,
|
||||
myLocationButtonEnabled: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
31
frontend/lib/old/modules/current_trip_overview.dart
Normal file
31
frontend/lib/old/modules/current_trip_overview.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:anyway/modules/current_trip_map.dart';
|
||||
import 'package:anyway/modules/current_trip_locations.dart';
|
||||
|
||||
|
||||
class CurrentTripOverview extends StatefulWidget {
|
||||
final Trip? trip;
|
||||
|
||||
const CurrentTripOverview({super.key, this.trip});
|
||||
|
||||
|
||||
@override
|
||||
State<CurrentTripOverview> createState() => _CurrentTripOverviewState();
|
||||
}
|
||||
|
||||
class _CurrentTripOverviewState extends State<CurrentTripOverview> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// The background map has a horizontally scrolling list of rounded buttons overlaid
|
||||
return Stack(
|
||||
alignment: Alignment.topLeft,
|
||||
children: [
|
||||
CurrentTripMap(trip: widget.trip),
|
||||
CurrentTripLocations(trip: widget.trip),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
128
frontend/lib/old/modules/current_trip_panel.dart
Normal file
128
frontend/lib/old/modules/current_trip_panel.dart
Normal file
@@ -0,0 +1,128 @@
|
||||
import 'package:anyway/pages/current_trip.dart';
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:anyway/constants.dart';
|
||||
|
||||
import 'package:anyway/structs/landmark.dart';
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
|
||||
import 'package:anyway/modules/current_trip_error_message.dart';
|
||||
import 'package:anyway/modules/current_trip_loading_indicator.dart';
|
||||
import 'package:anyway/modules/current_trip_summary.dart';
|
||||
import 'package:anyway/modules/current_trip_save_button.dart';
|
||||
import 'package:anyway/modules/current_trip_landmarks_list.dart';
|
||||
import 'package:anyway/modules/current_trip_greeter.dart';
|
||||
|
||||
|
||||
class CurrentTripPanel extends StatefulWidget {
|
||||
final ScrollController controller;
|
||||
final Trip trip;
|
||||
|
||||
const CurrentTripPanel({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.trip,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CurrentTripPanel> createState() => _CurrentTripPanelState();
|
||||
}
|
||||
|
||||
class _CurrentTripPanelState extends State<CurrentTripPanel> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: widget.trip,
|
||||
builder: (context, child) {
|
||||
if (widget.trip.uuid == 'error') {
|
||||
return ListView(
|
||||
controller: widget.controller,
|
||||
padding: const EdgeInsets.only(top: 10, left: 10, right: 10, bottom: 30),
|
||||
children: [
|
||||
SizedBox(
|
||||
// reuse the exact same height as the panel has when collapsed
|
||||
// this way the greeter will be centered when the panel is collapsed
|
||||
// note that we need to account for the padding above
|
||||
height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT - 10,
|
||||
child: Center(child:
|
||||
AutoSizeText(
|
||||
maxLines: 1,
|
||||
'Error',
|
||||
style: greeterStyle
|
||||
)
|
||||
),
|
||||
),
|
||||
|
||||
CurrentTripErrorMessage(trip: widget.trip),
|
||||
],
|
||||
);
|
||||
} else if (widget.trip.uuid == 'pending') {
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: SizedBox(
|
||||
// reuse the exact same height as the panel has when collapsed
|
||||
// this way the greeter will be centered when the panel is collapsed
|
||||
height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT,
|
||||
child: CurrentTripLoadingIndicator(trip: widget.trip),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return ListView(
|
||||
controller: widget.controller,
|
||||
padding: const EdgeInsets.only(top: 10, left: 10, right: 10, bottom: 30),
|
||||
children: [
|
||||
SizedBox(
|
||||
// reuse the exact same height as the panel has when collapsed
|
||||
// this way the greeter will be centered when the panel is collapsed
|
||||
// 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),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
CurrentTripSummary(trip: widget.trip),
|
||||
if (widget.trip.landmarks.where((Landmark landmark) => landmark.visited).isNotEmpty)
|
||||
ExpansionTile(
|
||||
leading: const Icon(Icons.location_on),
|
||||
title: const Text('Visited Landmarks (tap to expand)'),
|
||||
visualDensity: VisualDensity.compact,
|
||||
collapsedShape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
children: [
|
||||
...landmarksList(widget.trip, selector: (Landmark landmark) => landmark.visited),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
const Padding(padding: EdgeInsets.only(top: 10)),
|
||||
|
||||
// upcoming landmarks
|
||||
...landmarksList(widget.trip, selector: (Landmark landmark) => landmark.visited == false),
|
||||
|
||||
const Padding(padding: EdgeInsets.only(top: 10)),
|
||||
|
||||
Center(child: saveButton(trip: widget.trip)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
52
frontend/lib/old/modules/current_trip_save_button.dart
Normal file
52
frontend/lib/old/modules/current_trip_save_button.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
|
||||
import 'package:anyway/main.dart';
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
|
||||
|
||||
class saveButton extends StatefulWidget {
|
||||
Trip trip;
|
||||
saveButton({super.key, required this.trip});
|
||||
|
||||
@override
|
||||
State<saveButton> createState() => _saveButtonState();
|
||||
}
|
||||
|
||||
class _saveButtonState extends State<saveButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ElevatedButton(
|
||||
onPressed: () async {
|
||||
savedTrips.addTrip(widget.trip);
|
||||
rootScaffoldMessengerKey.currentState!.showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Trip saved'),
|
||||
duration: Duration(seconds: 2),
|
||||
dismissDirection: DismissDirection.horizontal
|
||||
)
|
||||
);
|
||||
},
|
||||
child: const 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
43
frontend/lib/old/modules/current_trip_summary.dart
Normal file
43
frontend/lib/old/modules/current_trip_summary.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class CurrentTripSummary extends StatefulWidget {
|
||||
final Trip trip;
|
||||
const CurrentTripSummary({
|
||||
super.key,
|
||||
required this.trip,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CurrentTripSummary> createState() => _CurrentTripSummaryState();
|
||||
}
|
||||
|
||||
class _CurrentTripSummaryState extends State<CurrentTripSummary> {
|
||||
@override
|
||||
Widget build(BuildContext context) => ListenableBuilder(
|
||||
listenable: widget.trip,
|
||||
builder: (context, child) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.flag, size: 20),
|
||||
const Padding(padding: EdgeInsets.only(right: 10)),
|
||||
Text('${widget.trip.landmarks.length} stops', style: Theme.of(context).textTheme.bodyLarge),
|
||||
]
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.hourglass_bottom_rounded, size: 20),
|
||||
const Padding(padding: EdgeInsets.only(right: 10)),
|
||||
Text('${widget.trip.totalTime.inHours}h ${widget.trip.totalTime.inMinutes.remainder(60)}min', style: Theme.of(context).textTheme.bodyLarge),
|
||||
]
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
25
frontend/lib/old/modules/help_dialog.dart
Normal file
25
frontend/lib/old/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();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
209
frontend/lib/old/modules/landmark_card.dart
Normal file
209
frontend/lib/old/modules/landmark_card.dart
Normal file
@@ -0,0 +1,209 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
|
||||
import 'package:anyway/constants.dart';
|
||||
|
||||
import 'package:anyway/main.dart';
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:anyway/structs/landmark.dart';
|
||||
|
||||
|
||||
|
||||
class LandmarkCard extends StatefulWidget {
|
||||
final Landmark landmark;
|
||||
final Trip parentTrip;
|
||||
|
||||
const LandmarkCard(
|
||||
this.landmark,
|
||||
this.parentTrip,
|
||||
);
|
||||
|
||||
@override
|
||||
_LandmarkCardState createState() => _LandmarkCardState();
|
||||
}
|
||||
|
||||
|
||||
class _LandmarkCardState extends State<LandmarkCard> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
// express the max height in terms text lines
|
||||
maxHeight: 7 * (Theme.of(context).textTheme.titleMedium!.fontSize! + 10),
|
||||
),
|
||||
child: Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
),
|
||||
elevation: 5,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
|
||||
// if the image is available, display it on the left side of the card, otherwise only display the text
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Image and landmark "type" on the left
|
||||
AspectRatio(
|
||||
aspectRatio: 3 / 4,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (widget.landmark.imageURL != null && widget.landmark.imageURL!.isNotEmpty)
|
||||
Expanded(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: widget.landmark.imageURL!,
|
||||
placeholder: (context, url) => const Center(child: CircularProgressIndicator()),
|
||||
errorWidget: (context, url, error) => imagePlaceholder(widget.landmark),
|
||||
fit: BoxFit.cover
|
||||
)
|
||||
)
|
||||
else
|
||||
imagePlaceholder(widget.landmark),
|
||||
|
||||
if (widget.landmark.type != typeStart && widget.landmark.type != typeFinish)
|
||||
Container(
|
||||
color: PRIMARY_COLOR,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.timer_outlined, size: 16),
|
||||
Text("${widget.landmark.duration?.inMinutes} minutes"),
|
||||
],
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
|
||||
// Main information, useful buttons on the right
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.landmark.name,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
),
|
||||
|
||||
if (widget.landmark.nameEN != null)
|
||||
Text(
|
||||
widget.landmark.nameEN!,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
|
||||
// fill the vspace
|
||||
const Spacer(),
|
||||
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.only(left: 5, right: 5, bottom: 10),
|
||||
// the scroll view should be flush once the buttons are scrolled to the left
|
||||
// but initially there should be some padding
|
||||
child: Wrap(
|
||||
spacing: 10,
|
||||
// show the type, the website, and the wikipedia link as buttons/labels in a row
|
||||
children: [
|
||||
doneToggleButton(),
|
||||
if (widget.landmark.websiteURL != null)
|
||||
websiteButton(),
|
||||
|
||||
optionsButton()
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget doneToggleButton() {
|
||||
return TextButton.icon(
|
||||
onPressed: () async {
|
||||
widget.landmark.visited = !widget.landmark.visited;
|
||||
widget.parentTrip.notifyUpdate();
|
||||
},
|
||||
icon: Icon(widget.landmark.visited ? Icons.undo_sharp : Icons.check),
|
||||
label: Text(widget.landmark.visited ? "Add to overview" : "Done!"),
|
||||
);
|
||||
}
|
||||
|
||||
Widget websiteButton () => TextButton.icon(
|
||||
onPressed: () async {
|
||||
// open a browser with the website link
|
||||
await launchUrl(Uri.parse(widget.landmark.websiteURL!));
|
||||
},
|
||||
icon: const Icon(Icons.link),
|
||||
label: const Text('Website'),
|
||||
);
|
||||
|
||||
|
||||
Widget optionsButton () => PopupMenuButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
style: TextButtonTheme.of(context).style,
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.delete),
|
||||
title: const Text('Delete'),
|
||||
onTap: () async {
|
||||
widget.parentTrip.removeLandmark(widget.landmark);
|
||||
rootScaffoldMessengerKey.currentState!.showSnackBar(
|
||||
SnackBar(content: Text("${widget.landmark.name} won't be shown again"))
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.star),
|
||||
title: const Text('Favorite'),
|
||||
onTap: () async {
|
||||
rootScaffoldMessengerKey.currentState!.showSnackBar(
|
||||
const SnackBar(content: Text("Not implemented yet"))
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Widget imagePlaceholder (Landmark landmark) => Expanded(
|
||||
child:
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [GRADIENT_START, GRADIENT_END],
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(landmark.type.icon.icon, size: 50),
|
||||
),
|
||||
),
|
||||
);
|
||||
57
frontend/lib/old/modules/landmark_map_marker.dart
Normal file
57
frontend/lib/old/modules/landmark_map_marker.dart
Normal file
@@ -0,0 +1,57 @@
|
||||
import 'package:anyway/constants.dart';
|
||||
import 'package:anyway/structs/landmark.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class ThemedMarker extends StatelessWidget {
|
||||
final Landmark landmark;
|
||||
final int position;
|
||||
|
||||
const ThemedMarker({
|
||||
super.key,
|
||||
required this.landmark,
|
||||
required this.position
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// This returns an outlined circle, with an icon corresponding to the landmark type
|
||||
// As a small dot, the number of the landmark is displayed in the top right
|
||||
|
||||
Widget? positionIndicator;
|
||||
if (landmark.type != typeStart && landmark.type != typeFinish) {
|
||||
positionIndicator = Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Text('$position', style: const TextStyle(color: Colors.black, fontSize: 25)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RepaintBoundary(
|
||||
child: Stack(
|
||||
alignment: Alignment.topRight,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: landmark.visited ? const LinearGradient(colors: [Colors.grey, Colors.white]) : APP_GRADIENT,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.black, width: 5),
|
||||
),
|
||||
width: 70,
|
||||
height: 70,
|
||||
padding: const EdgeInsets.all(5),
|
||||
child: Icon(landmark.type.icon.icon, size: 50),
|
||||
),
|
||||
if (positionIndicator != null) positionIndicator,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
52
frontend/lib/old/modules/map_chooser.dart
Normal file
52
frontend/lib/old/modules/map_chooser.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
import 'package:anyway/structs/landmark.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:map_launcher/map_launcher.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
showMapChooser(BuildContext context, Landmark current, Landmark next) async {
|
||||
List availableMaps = [];
|
||||
try {
|
||||
availableMaps = await MapLauncher.installedMaps;
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
if (availableMaps.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Container(
|
||||
child: Wrap(
|
||||
children: <Widget>[
|
||||
for (var map in availableMaps)
|
||||
ListTile(
|
||||
onTap: () => map.showDirections(
|
||||
origin: Coords(current.location[0], current.location[1]),
|
||||
originTitle: current.name,
|
||||
destination: Coords(next.location[0], next.location[1]),
|
||||
destinationTitle: current.name,
|
||||
directionsMode: DirectionsMode.walking
|
||||
),
|
||||
title: Text(map.mapName),
|
||||
// rounded corners
|
||||
leading: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
child: SvgPicture.asset(
|
||||
map.icon,
|
||||
height: 30.0,
|
||||
width: 30.0,
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
65
frontend/lib/old/modules/new_trip_button.dart
Normal file
65
frontend/lib/old/modules/new_trip_button.dart
Normal file
@@ -0,0 +1,65 @@
|
||||
import 'package:anyway/main.dart';
|
||||
import 'package:anyway/pages/current_trip.dart';
|
||||
import 'package:anyway/structs/preferences.dart';
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:anyway/utils/fetch_trip.dart';
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class NewTripButton extends StatefulWidget {
|
||||
final Trip trip;
|
||||
final UserPreferences preferences;
|
||||
|
||||
const NewTripButton({super.key,
|
||||
required this.trip,
|
||||
required this.preferences,
|
||||
});
|
||||
|
||||
@override
|
||||
State<NewTripButton> createState() => _NewTripButtonState();
|
||||
}
|
||||
|
||||
class _NewTripButtonState extends State<NewTripButton> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: widget.trip,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
if (widget.trip.landmarks.isEmpty){
|
||||
// Fallback if the trip setup is lagging behind
|
||||
// This should in theory never happen
|
||||
return Container();
|
||||
}
|
||||
return FloatingActionButton.extended(
|
||||
onPressed: onPressed,
|
||||
icon: const Icon(Icons.directions),
|
||||
label: const AutoSizeText('Start planning!'),
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void onPressed() async {
|
||||
// Check that the preferences are valid
|
||||
UserPreferences preferences = widget.preferences;
|
||||
if (preferences.nature.value == 0 && preferences.shopping.value == 0 && preferences.sightseeing.value == 0){
|
||||
rootScaffoldMessengerKey.currentState!.showSnackBar(
|
||||
const SnackBar(content: Text("Please specify at least one preference"))
|
||||
);
|
||||
} else if (preferences.maxTime.value == 0){
|
||||
rootScaffoldMessengerKey.currentState!.showSnackBar(
|
||||
const SnackBar(content: Text("Please choose a longer duration"))
|
||||
);
|
||||
} else {
|
||||
Trip trip = widget.trip;
|
||||
fetchTrip(trip, widget.preferences);
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => TripPage(trip: trip)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
126
frontend/lib/old/modules/new_trip_location_search.dart
Normal file
126
frontend/lib/old/modules/new_trip_location_search.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
|
||||
// A search bar that allow the user to enter a city name
|
||||
import 'package:anyway/structs/landmark.dart';
|
||||
import 'package:geocoding/geocoding.dart';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
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 {
|
||||
Future<SharedPreferences> prefs = SharedPreferences.getInstance();
|
||||
Trip trip;
|
||||
|
||||
NewTripLocationSearch(
|
||||
this.trip,
|
||||
);
|
||||
|
||||
|
||||
@override
|
||||
State<NewTripLocationSearch> createState() => _NewTripLocationSearchState();
|
||||
}
|
||||
|
||||
class _NewTripLocationSearchState extends State<NewTripLocationSearch> {
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
|
||||
setTripLocation (String query) async {
|
||||
List<Location> locations = [];
|
||||
Location startLocation;
|
||||
log('Searching for: $query');
|
||||
if (GeocodingPlatform.instance != null) {
|
||||
locations.addAll(await locationFromAddress(query));
|
||||
}
|
||||
|
||||
if (locations.isNotEmpty) {
|
||||
startLocation = locations.first;
|
||||
} else {
|
||||
log('No results found for: $query. Is geocoding available?');
|
||||
log('Setting Fallback location');
|
||||
List coordinates = debugLocations[query.toLowerCase()] ?? [48.8575, 2.3514];
|
||||
startLocation = Location(
|
||||
latitude: coordinates[0],
|
||||
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(
|
||||
hintText: 'Enter a city name or long press on the map.',
|
||||
onSubmitted: setTripLocation,
|
||||
controller: _controller,
|
||||
leading: const Icon(Icons.search),
|
||||
trailing: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setTripLocation(_controller.text);
|
||||
},
|
||||
child: const Text('Search'),
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
late Widget useCurrentLocationButton = ElevatedButton(
|
||||
onPressed: () async {
|
||||
// this widget is only shown if the user has already granted location permissions
|
||||
Position position = await Geolocator.getCurrentPosition();
|
||||
widget.trip.landmarks.clear();
|
||||
widget.trip.addLandmark(
|
||||
Landmark(
|
||||
uuid: 'pending',
|
||||
name: 'start',
|
||||
location: [position.latitude, position.longitude],
|
||||
type: typeStart
|
||||
)
|
||||
);
|
||||
},
|
||||
child: const Text('Use current location'),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: widget.prefs,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final useLocation = snapshot.data!.getBool('useLocation') ?? false;
|
||||
if (useLocation) {
|
||||
return Column(
|
||||
children: [
|
||||
locationSearchBar,
|
||||
useCurrentLocationButton,
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return locationSearchBar;
|
||||
}
|
||||
} else {
|
||||
return locationSearchBar;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
109
frontend/lib/old/modules/new_trip_map.dart
Normal file
109
frontend/lib/old/modules/new_trip_map.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
// A map that allows the user to select a location for a new trip.
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:widget_to_marker/widget_to_marker.dart';
|
||||
|
||||
import 'package:anyway/constants.dart';
|
||||
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:anyway/structs/landmark.dart';
|
||||
import 'package:anyway/modules/landmark_map_marker.dart';
|
||||
|
||||
|
||||
class NewTripMap extends StatefulWidget {
|
||||
Trip trip;
|
||||
NewTripMap(
|
||||
this.trip,
|
||||
);
|
||||
|
||||
@override
|
||||
State<NewTripMap> createState() => _NewTripMapState();
|
||||
}
|
||||
|
||||
class _NewTripMapState extends State<NewTripMap> {
|
||||
final CameraPosition _cameraPosition = const CameraPosition(
|
||||
target: LatLng(48.8566, 2.3522),
|
||||
zoom: 11.0,
|
||||
);
|
||||
GoogleMapController? _mapController;
|
||||
final Set<Marker> _markers = <Marker>{};
|
||||
|
||||
_onLongPress(LatLng location) {
|
||||
widget.trip.landmarks.clear();
|
||||
widget.trip.addLandmark(
|
||||
Landmark(
|
||||
uuid: 'pending',
|
||||
name: 'start',
|
||||
location: [location.latitude, location.longitude],
|
||||
type: typeStart
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
updateTripDetails() async {
|
||||
_markers.clear();
|
||||
if (widget.trip.landmarks.isNotEmpty) {
|
||||
Landmark landmark = widget.trip.landmarks.first;
|
||||
_markers.add(
|
||||
Marker(
|
||||
markerId: MarkerId(landmark.uuid),
|
||||
position: LatLng(landmark.location[0], landmark.location[1]),
|
||||
icon: await ThemedMarker(landmark: landmark, position: 0).toBitmapDescriptor(
|
||||
logicalSize: const Size(150, 150),
|
||||
imageSize: const Size(150, 150)
|
||||
),
|
||||
)
|
||||
);
|
||||
// check if the controller is ready
|
||||
|
||||
if (_mapController != null) {
|
||||
_mapController!.animateCamera(
|
||||
CameraUpdate.newLatLng(
|
||||
LatLng(landmark.location[0], landmark.location[1])
|
||||
)
|
||||
);
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
void _onMapCreated(GoogleMapController controller) async {
|
||||
_mapController = controller;
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
widget.trip.addListener(updateTripDetails);
|
||||
Future<SharedPreferences> preferences = SharedPreferences.getInstance();
|
||||
|
||||
|
||||
return FutureBuilder(
|
||||
future: preferences,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
SharedPreferences prefs = snapshot.data as SharedPreferences;
|
||||
bool useLocation = prefs.getBool('useLocation') ?? true;
|
||||
return _buildMap(useLocation);
|
||||
} else {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMap(bool useLocation) {
|
||||
return GoogleMap(
|
||||
onMapCreated: _onMapCreated,
|
||||
initialCameraPosition: _cameraPosition,
|
||||
onLongPress: _onLongPress,
|
||||
markers: _markers,
|
||||
cloudMapId: MAP_ID,
|
||||
mapToolbarEnabled: false,
|
||||
zoomControlsEnabled: false,
|
||||
myLocationButtonEnabled: false,
|
||||
myLocationEnabled: useLocation,
|
||||
);
|
||||
}
|
||||
}
|
||||
41
frontend/lib/old/modules/new_trip_options_button.dart
Normal file
41
frontend/lib/old/modules/new_trip_options_button.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import 'package:anyway/pages/new_trip_preferences.dart';
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class NewTripOptionsButton extends StatefulWidget {
|
||||
final Trip trip;
|
||||
|
||||
const NewTripOptionsButton({super.key, required this.trip});
|
||||
|
||||
@override
|
||||
State<NewTripOptionsButton> createState() => _NewTripOptionsButtonState();
|
||||
}
|
||||
|
||||
class _NewTripOptionsButtonState extends State<NewTripOptionsButton> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: widget.trip,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
if (widget.trip.landmarks.isEmpty){
|
||||
return Container();
|
||||
}
|
||||
return FloatingActionButton.extended(
|
||||
onPressed: () async {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NewTripPreferencesPage(trip: widget.trip)
|
||||
)
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const AutoSizeText('Set preferences')
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
118
frontend/lib/old/modules/onbarding_agreement_card.dart
Normal file
118
frontend/lib/old/modules/onbarding_agreement_card.dart
Normal file
@@ -0,0 +1,118 @@
|
||||
import 'package:anyway/structs/agreement.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
|
||||
import 'package:anyway/modules/onboarding_card.dart';
|
||||
|
||||
|
||||
class OnboardingAgreementCard extends StatefulWidget {
|
||||
final String title;
|
||||
final String description;
|
||||
final String imagePath;
|
||||
final String agreementTextPath;
|
||||
final ValueChanged<bool> onAgreementChanged;
|
||||
|
||||
|
||||
const OnboardingAgreementCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.imagePath,
|
||||
required this.agreementTextPath,
|
||||
required this.onAgreementChanged
|
||||
});
|
||||
|
||||
@override
|
||||
State<OnboardingAgreementCard> createState() => _OnboardingAgreementCardState();
|
||||
}
|
||||
|
||||
class _OnboardingAgreementCardState extends State<OnboardingAgreementCard> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
OnboardingCard(title: widget.title, description: widget.description, imagePath: widget.imagePath),
|
||||
const Padding(padding: EdgeInsets.only(top: 20)),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// The checkbox of the agreement
|
||||
FutureBuilder(
|
||||
future: getAgreement(),
|
||||
builder: (context, snapshot) {
|
||||
bool agreed = false;
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.hasData) {
|
||||
Agreement agreement = snapshot.data!;
|
||||
agreed = agreement.agreed;
|
||||
} else {
|
||||
agreed = false;
|
||||
}
|
||||
} else {
|
||||
agreed = false;
|
||||
}
|
||||
return Checkbox(
|
||||
value: agreed,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
widget.onAgreementChanged(value!);
|
||||
});
|
||||
saveAgreement(value!);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// The text of the agreement
|
||||
Text(
|
||||
"I agree to the ",
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
|
||||
// The clickable text of the agreement that shows the agreement text
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
// show a dialog with the agreement text
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
scrollable: true,
|
||||
content: FutureBuilder(
|
||||
future: DefaultAssetBundle.of(context).loadString(widget.agreementTextPath),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return MarkdownBody(
|
||||
data: snapshot.data.toString(),
|
||||
);
|
||||
} else {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
},
|
||||
child: Text(
|
||||
"Terms of Service (click to view)",
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
45
frontend/lib/old/modules/onboarding_card.dart
Normal file
45
frontend/lib/old/modules/onboarding_card.dart
Normal file
@@ -0,0 +1,45 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class OnboardingCard extends StatelessWidget {
|
||||
final String title;
|
||||
final String description;
|
||||
final String imagePath;
|
||||
|
||||
const OnboardingCard({super.key,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.imagePath,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.headlineLarge!.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.only(top: 20)),
|
||||
SvgPicture.asset(
|
||||
imagePath,
|
||||
height: 200,
|
||||
),
|
||||
const Padding(padding: EdgeInsets.only(top: 20)),
|
||||
Text(
|
||||
description,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
70
frontend/lib/old/modules/step_between_landmarks.dart
Normal file
70
frontend/lib/old/modules/step_between_landmarks.dart
Normal file
@@ -0,0 +1,70 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:anyway/structs/landmark.dart';
|
||||
import 'package:anyway/modules/map_chooser.dart';
|
||||
|
||||
|
||||
class StepBetweenLandmarks extends StatefulWidget {
|
||||
final Landmark current;
|
||||
final Landmark next;
|
||||
|
||||
const StepBetweenLandmarks({
|
||||
super.key,
|
||||
required this.current,
|
||||
required this.next
|
||||
});
|
||||
|
||||
@override
|
||||
State<StepBetweenLandmarks> createState() => _StepBetweenLandmarksState();
|
||||
}
|
||||
|
||||
class _StepBetweenLandmarksState extends State<StepBetweenLandmarks> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
int? time = widget.current.tripTime?.inMinutes;
|
||||
|
||||
// since landmarks might have been marked as visited, the next landmark might not be the immediate next in the list
|
||||
// => the precomputed trip time is not valid anymore
|
||||
if (widget.current.next != widget.next) {
|
||||
time = null;
|
||||
}
|
||||
|
||||
// round 0 travel time to 1 minute
|
||||
if (time != null && time < 1) {
|
||||
time = 1;
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(10),
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
left: BorderSide(width: 3.0, color: Colors.black),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
const Icon(Icons.directions_walk),
|
||||
Text(
|
||||
time == null ? "" : "$time min",
|
||||
style: const TextStyle(fontSize: 10)
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
showMapChooser(context, widget.current, widget.next);
|
||||
},
|
||||
icon: const Icon(Icons.directions),
|
||||
label: const Text("Directions"),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
94
frontend/lib/old/modules/trips_saved_list.dart
Normal file
94
frontend/lib/old/modules/trips_saved_list.dart
Normal file
@@ -0,0 +1,94 @@
|
||||
import 'package:anyway/pages/current_trip.dart';
|
||||
import 'package:anyway/utils/load_trips.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
|
||||
|
||||
class TripsOverview extends StatefulWidget {
|
||||
final SavedTrips trips;
|
||||
const TripsOverview({
|
||||
super.key,
|
||||
required this.trips,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TripsOverview> createState() => _TripsOverviewState();
|
||||
}
|
||||
|
||||
class _TripsOverviewState extends State<TripsOverview> {
|
||||
Widget tripListItemBuilder(BuildContext context, int index) {
|
||||
Trip trip = widget.trips.trips[index];
|
||||
return ListTile(
|
||||
title: FutureBuilder(
|
||||
future: trip.cityName,
|
||||
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return Text("Trip to ${snapshot.data}");
|
||||
} else if (snapshot.hasError) {
|
||||
return Text("Error: ${snapshot.error}");
|
||||
} else {
|
||||
return const Text("Trip to ...");
|
||||
}
|
||||
},
|
||||
),
|
||||
// emoji of the country flag of the trip's country
|
||||
leading: const Icon(Icons.pin_drop),
|
||||
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => TripPage(trip: trip)
|
||||
)
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Widget listBuild (BuildContext context, SavedTrips trips) {
|
||||
// List<Widget> children;
|
||||
// List<Trip> items = trips.trips;
|
||||
// children = List<Widget>.generate(items.length, (index) {
|
||||
// Trip trip = items[index];
|
||||
// return ListTile(
|
||||
// title: FutureBuilder(
|
||||
// future: trip.cityName,
|
||||
// builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
|
||||
// if (snapshot.hasData) {
|
||||
// return Text("Trip to ${snapshot.data}");
|
||||
// } else if (snapshot.hasError) {
|
||||
// return Text("Error: ${snapshot.error}");
|
||||
// } else {
|
||||
// return const Text("Trip to ...");
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// leading: const Icon(Icons.pin_drop),
|
||||
// onTap: () {
|
||||
// Navigator.of(context).push(
|
||||
// MaterialPageRoute(
|
||||
// builder: (context) => TripPage(trip: trip)
|
||||
// )
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// });
|
||||
|
||||
// return ListView(
|
||||
// padding: const EdgeInsets.only(top: 0),
|
||||
// children: children,
|
||||
// );
|
||||
// }
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: widget.trips,
|
||||
builder: (BuildContext context, Widget? child) => ListView.builder(
|
||||
itemCount: widget.trips.trips.length,
|
||||
itemBuilder: tripListItemBuilder,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
71
frontend/lib/old/pages/current_trip.dart
Normal file
71
frontend/lib/old/pages/current_trip.dart
Normal file
@@ -0,0 +1,71 @@
|
||||
import 'package:anyway/constants.dart';
|
||||
import 'package:anyway/layouts/scaffold.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sliding_up_panel/sliding_up_panel.dart';
|
||||
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:anyway/modules/current_trip_overview.dart';
|
||||
import 'package:anyway/modules/current_trip_panel.dart';
|
||||
|
||||
final Shader textGradient = APP_GRADIENT.createShader(const Rect.fromLTWH(0.0, 0.0, 200.0, 70.0));
|
||||
TextStyle greeterStyle = TextStyle(
|
||||
foreground: Paint()..shader = textGradient,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 25
|
||||
);
|
||||
|
||||
|
||||
class TripPage extends StatefulWidget {
|
||||
final Trip trip;
|
||||
|
||||
const TripPage({super.key,
|
||||
required this.trip,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TripPage> createState() => _TripPageState();
|
||||
}
|
||||
|
||||
|
||||
|
||||
class _TripPageState extends State<TripPage> with ScaffoldLayout{
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return mainScaffold(
|
||||
context,
|
||||
child: 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
|
||||
// collapsed: Greeter(trip: widget.trip),
|
||||
body: CurrentTripOverview(trip: widget.trip),
|
||||
minHeight: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT,
|
||||
maxHeight: MediaQuery.of(context).size.height * TRIP_PANEL_MAX_HEIGHT,
|
||||
// padding in this context is annoying: it offsets the notion of vertical alignment.
|
||||
// children that want to be centered vertically need to have their size adjusted by 2x the padding
|
||||
// padding: const EdgeInsets.all(10.0),
|
||||
// Panel snapping should not be disabled because it significantly improves the user experience
|
||||
// panelSnapping: false
|
||||
borderRadius: const BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)),
|
||||
parallaxEnabled: true,
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
blurRadius: 20.0,
|
||||
color: Colors.black,
|
||||
)
|
||||
],
|
||||
),
|
||||
title: FutureBuilder(
|
||||
future: widget.trip.cityName,
|
||||
builder: (context, snapshot) => Text(
|
||||
'Trip to ${snapshot.hasData ? snapshot.data! : "..."}',
|
||||
)
|
||||
),
|
||||
helpTexts: [
|
||||
'Current trip',
|
||||
'You can see and edit your current trip here. Swipe up from the bottom to see a detailed view of the recommendations.'
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
47
frontend/lib/old/pages/new_trip_location.dart
Normal file
47
frontend/lib/old/pages/new_trip_location.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
import 'package:anyway/layouts/scaffold.dart';
|
||||
import 'package:anyway/modules/new_trip_options_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import "package:anyway/structs/trip.dart";
|
||||
import 'package:anyway/modules/new_trip_location_search.dart';
|
||||
import 'package:anyway/modules/new_trip_map.dart';
|
||||
|
||||
|
||||
class NewTripPage extends StatefulWidget {
|
||||
const NewTripPage({super.key});
|
||||
|
||||
@override
|
||||
_NewTripPageState createState() => _NewTripPageState();
|
||||
}
|
||||
|
||||
class _NewTripPageState extends State<NewTripPage> with ScaffoldLayout {
|
||||
final TextEditingController latController = TextEditingController();
|
||||
final TextEditingController lonController = TextEditingController();
|
||||
Trip trip = Trip();
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// floating search bar and map as a background
|
||||
return mainScaffold(
|
||||
context,
|
||||
child: Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
NewTripMap(trip),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(15),
|
||||
child: NewTripLocationSearch(trip),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: NewTripOptionsButton(trip: trip),
|
||||
),
|
||||
title: const 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."
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
124
frontend/lib/old/pages/new_trip_preferences.dart
Normal file
124
frontend/lib/old/pages/new_trip_preferences.dart
Normal file
@@ -0,0 +1,124 @@
|
||||
import 'package:anyway/layouts/scaffold.dart';
|
||||
import 'package:anyway/modules/new_trip_button.dart';
|
||||
import 'package:anyway/structs/landmark.dart';
|
||||
import 'package:anyway/structs/preferences.dart';
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
|
||||
class NewTripPreferencesPage extends StatefulWidget {
|
||||
final Trip trip;
|
||||
const NewTripPreferencesPage({super.key, required this.trip});
|
||||
|
||||
@override
|
||||
_NewTripPreferencesPageState createState() => _NewTripPreferencesPageState();
|
||||
}
|
||||
|
||||
class _NewTripPreferencesPageState extends State<NewTripPreferencesPage> with ScaffoldLayout {
|
||||
UserPreferences preferences = UserPreferences();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Ensure that the trip is "empty" save for the start landmark
|
||||
// This is necessary because users can swipe back to this page even after the trip has been created
|
||||
if (widget.trip.landmarks.length > 1) {
|
||||
Landmark start = widget.trip.landmarks.first;
|
||||
widget.trip.landmarks.clear();
|
||||
widget.trip.addLandmark(start);
|
||||
}
|
||||
|
||||
return mainScaffold(
|
||||
context,
|
||||
child: Scaffold(
|
||||
body: ListView(
|
||||
children: [
|
||||
const 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))
|
||||
),
|
||||
),
|
||||
|
||||
const Divider(indent: 25, endIndent: 25, height: 50),
|
||||
|
||||
durationPicker(preferences.maxTime),
|
||||
|
||||
preferenceSliders([preferences.sightseeing, preferences.shopping, preferences.nature]),
|
||||
|
||||
// Add a conditional padding to avoid the floating button covering the last slider
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom + 80),
|
||||
),
|
||||
]
|
||||
),
|
||||
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.'
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget durationPicker(SinglePreference maxTime) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0),
|
||||
shadowColor: Colors.grey,
|
||||
child: ListTile(
|
||||
leading: preferences.maxTime.icon,
|
||||
title: Text(preferences.maxTime.description),
|
||||
subtitle: CupertinoTimerPicker(
|
||||
mode: CupertinoTimerPickerMode.hm,
|
||||
initialTimerDuration: const Duration(minutes: 90),
|
||||
minuteInterval: 15,
|
||||
onTimerDurationChanged: (Duration newDuration) {
|
||||
setState(() {
|
||||
preferences.maxTime.value = newDuration.inMinutes;
|
||||
});
|
||||
},
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget preferenceSliders(List<SinglePreference> prefs) {
|
||||
List<Card> sliders = [];
|
||||
for (SinglePreference pref in prefs) {
|
||||
sliders.add(
|
||||
Card(
|
||||
child: ListTile(
|
||||
leading: pref.icon,
|
||||
title: Text(pref.name),
|
||||
subtitle: Slider(
|
||||
value: pref.value.toDouble(),
|
||||
min: pref.minVal.toDouble(),
|
||||
max: pref.maxVal.toDouble(),
|
||||
divisions: pref.maxVal - pref.minVal,
|
||||
label: pref.value.toString(),
|
||||
onChanged: (double newValue) {
|
||||
setState(() {
|
||||
pref.value = newValue.toInt();
|
||||
});
|
||||
},
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: sliders
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
53
frontend/lib/old/pages/no_trips_page.dart
Normal file
53
frontend/lib/old/pages/no_trips_page.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'package:anyway/pages/new_trip_location.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:anyway/layouts/scaffold.dart';
|
||||
class NoTripsPage extends StatefulWidget {
|
||||
const NoTripsPage({super.key});
|
||||
|
||||
@override
|
||||
State<NoTripsPage> createState() => _NoTripsPageState();
|
||||
}
|
||||
|
||||
class _NoTripsPageState extends State<NoTripsPage> with ScaffoldLayout {
|
||||
@override
|
||||
Widget build(BuildContext context) => mainScaffold(
|
||||
context,
|
||||
child: Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"No trips yet",
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Padding(padding: EdgeInsets.only(bottom: 10)),
|
||||
Text(
|
||||
"You can start a new trip by clicking the button below",
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const NewTripPage()
|
||||
)
|
||||
);
|
||||
},
|
||||
label: const Row(
|
||||
children: [
|
||||
Text("Start planning!"),
|
||||
Padding(padding: EdgeInsets.only(right: 8.0)),
|
||||
Icon(Icons.map_outlined)
|
||||
],
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
198
frontend/lib/old/pages/settings.dart
Normal file
198
frontend/lib/old/pages/settings.dart
Normal file
@@ -0,0 +1,198 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'package:anyway/main.dart';
|
||||
import 'package:anyway/constants.dart';
|
||||
import 'package:anyway/layouts/scaffold.dart';
|
||||
|
||||
|
||||
bool debugMode = false;
|
||||
|
||||
class SettingsPage extends StatefulWidget {
|
||||
const SettingsPage({super.key});
|
||||
|
||||
@override
|
||||
_SettingsPageState createState() => _SettingsPageState();
|
||||
}
|
||||
|
||||
class _SettingsPageState extends State<SettingsPage> with ScaffoldLayout {
|
||||
@override
|
||||
Widget build (BuildContext context) => mainScaffold(
|
||||
context,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(15),
|
||||
children: [
|
||||
// First a round, centered image
|
||||
const Center(
|
||||
child: CircleAvatar(
|
||||
radius: 75,
|
||||
child: Icon(Icons.settings, size: 100),
|
||||
)
|
||||
),
|
||||
const Center(
|
||||
child: Text('Global settings', style: TextStyle(fontSize: 24))
|
||||
),
|
||||
|
||||
const Divider(indent: 25, endIndent: 25, height: 50),
|
||||
|
||||
darkMode(),
|
||||
setLocationUsage(),
|
||||
setDebugMode(),
|
||||
|
||||
const Divider(indent: 25, endIndent: 25, height: 50),
|
||||
|
||||
privacyInfo(),
|
||||
]
|
||||
),
|
||||
title: const Text('Settings'),
|
||||
helpTexts: [
|
||||
'Settings',
|
||||
'Preferences set in this page are global and will affect the entire application.'
|
||||
],
|
||||
);
|
||||
|
||||
Widget setDebugMode() {
|
||||
return Row(
|
||||
children: [
|
||||
const Text('Debugging: use a custom API URL'),
|
||||
// white space
|
||||
const Spacer(),
|
||||
Switch(
|
||||
value: debugMode,
|
||||
onChanged: (bool? newValue) {
|
||||
setState(() {
|
||||
debugMode = newValue!;
|
||||
if (debugMode) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Debug mode - use a custom API endpoint'),
|
||||
content: TextField(
|
||||
controller: TextEditingController(text: API_URL_DEBUG),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
API_URL_BASE = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('OK'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget darkMode() {
|
||||
return Row(
|
||||
children: [
|
||||
const Text('Dark mode'),
|
||||
const Spacer(),
|
||||
Switch(
|
||||
value: Theme.of(context).brightness == Brightness.dark,
|
||||
onChanged: (bool? newValue) {
|
||||
setState(() {
|
||||
rootScaffoldMessengerKey.currentState!.showSnackBar(
|
||||
const SnackBar(content: Text('Dark mode is not implemented yet'))
|
||||
);
|
||||
// if (newValue!) {
|
||||
// // Dark mode
|
||||
// Theme.of(context).brightness = Brightness.dark;
|
||||
// } else {
|
||||
// // Light mode
|
||||
// Theme.of(context).brightness = Brightness.light;
|
||||
// }
|
||||
});
|
||||
}
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget setLocationUsage() {
|
||||
Future<SharedPreferences> preferences = SharedPreferences.getInstance();
|
||||
return Row(
|
||||
children: [
|
||||
const Text('Use location services'),
|
||||
// white space
|
||||
const Spacer(),
|
||||
FutureBuilder(
|
||||
future: preferences,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
bool useLocation = snapshot.data!.getBool('useLocation') ?? false;
|
||||
return Switch(
|
||||
value: useLocation,
|
||||
onChanged: setUseLocation,
|
||||
);
|
||||
} else {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
}
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void setUseLocation(bool newValue) async {
|
||||
await Permission.locationWhenInUse
|
||||
.onDeniedCallback(() {
|
||||
rootScaffoldMessengerKey.currentState!.showSnackBar(
|
||||
const SnackBar(content: Text('Location services are required for this feature'))
|
||||
);
|
||||
})
|
||||
.onGrantedCallback(() {
|
||||
rootScaffoldMessengerKey.currentState!.showSnackBar(
|
||||
const SnackBar(content: Text('Location services are now enabled'))
|
||||
);
|
||||
SharedPreferences.getInstance().then(
|
||||
(SharedPreferences prefs) {
|
||||
setState(() {
|
||||
prefs.setBool('useLocation', newValue);
|
||||
});
|
||||
}
|
||||
);
|
||||
})
|
||||
.onPermanentlyDeniedCallback(() {
|
||||
rootScaffoldMessengerKey.currentState!.showSnackBar(
|
||||
const SnackBar(content: Text('Location services are required for this feature'))
|
||||
);
|
||||
})
|
||||
.request();
|
||||
}
|
||||
|
||||
Widget privacyInfo() {
|
||||
return Center(
|
||||
child: Column(
|
||||
children: [
|
||||
const 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.', textAlign: TextAlign.center),
|
||||
const Padding(padding: EdgeInsets.only(top: 3)),
|
||||
const Text('Our full privacy policy is available under:', textAlign: TextAlign.center),
|
||||
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.info),
|
||||
label: Text(PRIVACY_URL),
|
||||
onPressed: () async{
|
||||
await launchUrl(Uri.parse(PRIVACY_URL));
|
||||
}
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
17
frontend/lib/old/structs/agreement.dart
Normal file
17
frontend/lib/old/structs/agreement.dart
Normal file
@@ -0,0 +1,17 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
final class Agreement{
|
||||
bool agreed;
|
||||
|
||||
Agreement({required this.agreed});
|
||||
}
|
||||
|
||||
void saveAgreement(bool agreed) async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
prefs.setBool('agreed_to_terms_and_conditions', agreed);
|
||||
}
|
||||
|
||||
Future<Agreement> getAgreement() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
return Agreement(agreed: prefs.getBool('agreed_to_terms_and_conditions') ?? false);
|
||||
}
|
||||
172
frontend/lib/old/structs/landmark.dart
Normal file
172
frontend/lib/old/structs/landmark.dart
Normal file
@@ -0,0 +1,172 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
LandmarkType typeSightseeing = LandmarkType(name: 'sightseeing');
|
||||
LandmarkType typeNature = LandmarkType(name: 'nature');
|
||||
LandmarkType typeShopping = LandmarkType(name: 'shopping');
|
||||
// LandmarkType museum = LandmarkType(name: 'Museum');
|
||||
// LandmarkType restaurant = LandmarkType(name: 'Restaurant');
|
||||
LandmarkType typeStart = LandmarkType(name: 'start');
|
||||
LandmarkType typeFinish = LandmarkType(name: 'finish');
|
||||
|
||||
|
||||
final class Landmark extends LinkedListEntry<Landmark>{
|
||||
// A linked node of a list of Landmarks
|
||||
final String uuid;
|
||||
final String name;
|
||||
final List<double> location;
|
||||
final LandmarkType type;
|
||||
final bool? isSecondary;
|
||||
|
||||
// description to be shown in the overview
|
||||
final String? nameEN;
|
||||
final String? websiteURL;
|
||||
String? imageURL; // not final because it can be patched
|
||||
final String? description;
|
||||
final Duration? duration;
|
||||
bool visited;
|
||||
|
||||
// Next node is implicitly available through the LinkedListEntry mixin
|
||||
// final Landmark? next;
|
||||
Duration? tripTime;
|
||||
// the trip time depends on the next landmark, so it is not final
|
||||
|
||||
|
||||
Landmark({
|
||||
required this.uuid,
|
||||
required this.name,
|
||||
required this.location,
|
||||
required this.type,
|
||||
this.isSecondary,
|
||||
|
||||
this.nameEN,
|
||||
this.websiteURL,
|
||||
this.imageURL,
|
||||
this.description,
|
||||
this.duration,
|
||||
this.visited = false,
|
||||
|
||||
// this.next,
|
||||
this.tripTime,
|
||||
});
|
||||
|
||||
|
||||
factory Landmark.fromJson(Map<String, dynamic> json) {
|
||||
if (json
|
||||
case { // automatically match all the non-optionals and cast them to the right type
|
||||
'uuid': String uuid,
|
||||
'name': String name,
|
||||
'location': List<dynamic> location,
|
||||
'type': String type,
|
||||
}) {
|
||||
// refine the parsing on a few fields
|
||||
List<double> locationFixed = List<double>.from(location);
|
||||
// parse the rest separately, they could be missing
|
||||
LandmarkType typeFixed = LandmarkType(name: type);
|
||||
final isSecondary = json['is_secondary'] as bool?;
|
||||
final nameEN = json['name_en'] as String?;
|
||||
final websiteURL = json['website_url'] as String?;
|
||||
final imageURL = json['image_url'] as String?;
|
||||
final description = json['description'] as String?;
|
||||
var duration = Duration(minutes: json['duration']);
|
||||
final visited = json['visited'] ?? false;
|
||||
var tripTime = Duration(minutes: json['time_to_reach_next'] ?? 0) as Duration?;
|
||||
|
||||
return Landmark(
|
||||
uuid: uuid,
|
||||
name: name,
|
||||
location: locationFixed,
|
||||
type: typeFixed,
|
||||
isSecondary: isSecondary,
|
||||
nameEN: nameEN,
|
||||
websiteURL: websiteURL,
|
||||
imageURL: imageURL,
|
||||
description: description,
|
||||
duration: duration,
|
||||
visited: visited,
|
||||
tripTime: tripTime
|
||||
);
|
||||
} else {
|
||||
throw FormatException('Invalid JSON: $json');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is Landmark && uuid == other.uuid;
|
||||
}
|
||||
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'uuid': uuid,
|
||||
'name': name,
|
||||
'location': location,
|
||||
'type': type.name,
|
||||
'is_secondary': isSecondary,
|
||||
'name_en': nameEN,
|
||||
'website_url': websiteURL,
|
||||
'image_url': imageURL,
|
||||
'description': description,
|
||||
'duration': duration?.inMinutes,
|
||||
'visited': visited,
|
||||
'trip_time': tripTime?.inMinutes,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
class LandmarkType {
|
||||
final String name;
|
||||
// final String description;
|
||||
Icon icon;
|
||||
|
||||
LandmarkType({required this.name, this.icon = const Icon(Icons.location_on)}) {
|
||||
switch (name) {
|
||||
case 'sightseeing':
|
||||
icon = const Icon(Icons.castle);
|
||||
break;
|
||||
case 'nature':
|
||||
icon = const Icon(Icons.eco);
|
||||
break;
|
||||
case 'shopping':
|
||||
icon = const Icon(Icons.shopping_cart);
|
||||
break;
|
||||
case 'start':
|
||||
icon = const Icon(Icons.play_arrow);
|
||||
break;
|
||||
case 'finish':
|
||||
icon = const Icon(Icons.flag);
|
||||
break;
|
||||
default:
|
||||
icon = const Icon(Icons.location_on);
|
||||
}
|
||||
}
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other is LandmarkType) {
|
||||
return name == other.name;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Helpers
|
||||
// Handling the landmarks requires a little bit of special care because the linked list is not directly representable in json
|
||||
(Landmark, String?) getLandmarkFromPrefs(SharedPreferences prefs, String uuid) {
|
||||
String? content = prefs.getString('landmark_$uuid');
|
||||
Map<String, dynamic> json = jsonDecode(content!);
|
||||
String? nextUUID = json['next_uuid'];
|
||||
return (Landmark.fromJson(json), nextUUID);
|
||||
}
|
||||
|
||||
|
||||
void landmarkToPrefs(SharedPreferences prefs, Landmark current, Landmark? next) {
|
||||
Map<String, dynamic> json = current.toJson();
|
||||
json['next_uuid'] = next?.uuid;
|
||||
prefs.setString('landmark_${current.uuid}', jsonEncode(json));
|
||||
}
|
||||
69
frontend/lib/old/structs/preferences.dart
Normal file
69
frontend/lib/old/structs/preferences.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
import 'package:anyway/structs/landmark.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class SinglePreference {
|
||||
String slug;
|
||||
String name;
|
||||
String description;
|
||||
int value;
|
||||
int minVal;
|
||||
int maxVal;
|
||||
Icon icon;
|
||||
|
||||
SinglePreference({
|
||||
required this.slug,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.value,
|
||||
required this.icon,
|
||||
this.minVal = 0,
|
||||
this.maxVal = 5,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
class UserPreferences {
|
||||
SinglePreference sightseeing = SinglePreference(
|
||||
name: "Sightseeing",
|
||||
slug: "sightseeing",
|
||||
description: "How much do you like sightseeing?",
|
||||
value: 0,
|
||||
icon: typeSightseeing.icon,
|
||||
);
|
||||
SinglePreference shopping = SinglePreference(
|
||||
name: "Shopping",
|
||||
slug: "shopping",
|
||||
description: "How much do you like shopping?",
|
||||
value: 0,
|
||||
icon: typeShopping.icon,
|
||||
);
|
||||
SinglePreference nature = SinglePreference(
|
||||
name: "Nature",
|
||||
slug: "nature",
|
||||
description: "How much do you like nature?",
|
||||
value: 0,
|
||||
icon: typeNature.icon,
|
||||
);
|
||||
|
||||
SinglePreference maxTime = SinglePreference(
|
||||
name: "Trip duration",
|
||||
slug: "duration",
|
||||
description: "How long should your trip be?",
|
||||
value: 30,
|
||||
minVal: 30,
|
||||
maxVal: 720,
|
||||
icon: const Icon(Icons.timer),
|
||||
);
|
||||
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
// This is "opinionated" JSON, corresponding to the backend's expectations
|
||||
return {
|
||||
"sightseeing": {"type": "sightseeing", "score": sightseeing.value},
|
||||
"shopping": {"type": "shopping", "score": shopping.value},
|
||||
"nature": {"type": "nature", "score": nature.value},
|
||||
"max_time_minute": maxTime.value
|
||||
};
|
||||
}
|
||||
}
|
||||
142
frontend/lib/old/structs/trip.dart
Normal file
142
frontend/lib/old/structs/trip.dart
Normal file
@@ -0,0 +1,142 @@
|
||||
// Represents a collection of landmarks that represent a journey
|
||||
// Different instances of a Trip can be saved and loaded by the user
|
||||
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:anyway/structs/landmark.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:geocoding/geocoding.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class Trip with ChangeNotifier {
|
||||
String uuid;
|
||||
Duration totalTime;
|
||||
LinkedList<Landmark> landmarks;
|
||||
// could be empty as well
|
||||
String? errorDescription;
|
||||
|
||||
Future<String> get cityName async {
|
||||
List<double>? location = landmarks.firstOrNull?.location;
|
||||
if (GeocodingPlatform.instance == null) {
|
||||
return '$location';
|
||||
} else if (location == null) {
|
||||
return 'Unknown';
|
||||
} else{
|
||||
List<Placemark> placemarks = await placemarkFromCoordinates(location[0], location[1]);
|
||||
return placemarks.first.locality ?? 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> landmarkPosition (Landmark landmark) async {
|
||||
int i = 0;
|
||||
for (Landmark l in landmarks) {
|
||||
if (l.uuid == landmark.uuid) {
|
||||
return i;
|
||||
} else if (l.type != typeStart && l.type != typeFinish) {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
Trip({
|
||||
this.uuid = 'pending',
|
||||
this.totalTime = Duration.zero,
|
||||
LinkedList<Landmark>? landmarks
|
||||
// a trip can be created with no landmarks, but the list should be initialized anyway
|
||||
}) : landmarks = landmarks ?? LinkedList<Landmark>();
|
||||
|
||||
|
||||
factory Trip.fromJson(Map<String, dynamic> json) {
|
||||
Trip trip = Trip(
|
||||
uuid: json['uuid'],
|
||||
totalTime: Duration(minutes: json['total_time']),
|
||||
);
|
||||
|
||||
return trip;
|
||||
}
|
||||
|
||||
void loadFromJson(Map<String, dynamic> json) {
|
||||
uuid = json['uuid'];
|
||||
totalTime = Duration(minutes: json['total_time']);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void addLandmark(Landmark landmark) {
|
||||
landmarks.add(landmark);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateUUID(String newUUID) {
|
||||
uuid = newUUID;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void removeLandmark (Landmark landmark) async {
|
||||
Landmark? previous = landmark.previous;
|
||||
Landmark? next = landmark.next;
|
||||
landmarks.remove(landmark);
|
||||
// removing the landmark means we need to recompute the time between the two adjoined landmarks
|
||||
if (previous != null && next != null) {
|
||||
// previous.next = next happens automatically since we are using a LinkedList
|
||||
totalTime -= previous.tripTime ?? Duration.zero;
|
||||
previous.tripTime = null;
|
||||
// TODO
|
||||
}
|
||||
totalTime -= landmark.tripTime ?? Duration.zero;
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateError(String error) {
|
||||
errorDescription = error;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void notifyUpdate(){
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
factory Trip.fromPrefs(SharedPreferences prefs, String uuid) {
|
||||
String? content = prefs.getString('trip_$uuid');
|
||||
Map<String, dynamic> json = jsonDecode(content!);
|
||||
Trip trip = Trip.fromJson(json);
|
||||
String? firstUUID = json['first_landmark_uuid'];
|
||||
log('Loading trip $uuid with first landmark $firstUUID');
|
||||
LinkedList<Landmark> landmarks = readLandmarks(prefs, firstUUID);
|
||||
trip.landmarks = landmarks;
|
||||
return trip;
|
||||
}
|
||||
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'uuid': uuid,
|
||||
'total_time': totalTime.inMinutes,
|
||||
'first_landmark_uuid': landmarks.first.uuid
|
||||
};
|
||||
|
||||
|
||||
void toPrefs(SharedPreferences prefs){
|
||||
Map<String, dynamic> json = toJson();
|
||||
log('Saving trip $uuid : $json');
|
||||
prefs.setString('trip_$uuid', jsonEncode(json));
|
||||
for (Landmark landmark in landmarks) {
|
||||
landmarkToPrefs(prefs, landmark, landmark.next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Helper
|
||||
LinkedList<Landmark> readLandmarks(SharedPreferences prefs, String? firstUUID) {
|
||||
LinkedList<Landmark> landmarks = LinkedList<Landmark>();
|
||||
while (firstUUID != null) {
|
||||
var (head, nextUUID) = getLandmarkFromPrefs(prefs, firstUUID);
|
||||
landmarks.add(head);
|
||||
firstUUID = nextUUID;
|
||||
}
|
||||
return landmarks;
|
||||
}
|
||||
167
frontend/lib/old/utils/fetch_trip.dart
Normal file
167
frontend/lib/old/utils/fetch_trip.dart
Normal file
@@ -0,0 +1,167 @@
|
||||
import "dart:async";
|
||||
import "dart:convert";
|
||||
import "dart:developer";
|
||||
import "dart:io";
|
||||
import "package:anyway/main.dart";
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import 'package:anyway/constants.dart';
|
||||
import "package:anyway/utils/load_landmark_image.dart";
|
||||
import "package:anyway/structs/landmark.dart";
|
||||
import "package:anyway/structs/trip.dart";
|
||||
import "package:anyway/structs/preferences.dart";
|
||||
|
||||
|
||||
Dio dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: API_URL_BASE,
|
||||
connectTimeout: const Duration(seconds: 5),
|
||||
receiveTimeout: const Duration(seconds: 120),
|
||||
// also accept 500 errors, since we cannot rule out that the server is at fault. We still want to gracefully handle these errors
|
||||
validateStatus: (status) => status! <= 500,
|
||||
receiveDataWhenStatusError: true,
|
||||
contentType: Headers.jsonContentType,
|
||||
responseType: ResponseType.json,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
fetchTrip(
|
||||
Trip trip,
|
||||
UserPreferences preferences,
|
||||
) async {
|
||||
Map<String, dynamic> data = {
|
||||
"preferences": preferences.toJson(),
|
||||
"start": trip.landmarks.first.location,
|
||||
};
|
||||
String dataString = jsonEncode(data);
|
||||
log(dataString);
|
||||
|
||||
late Response response;
|
||||
try {
|
||||
response = await dio.post(
|
||||
"/trip/new",
|
||||
data: data
|
||||
);
|
||||
} catch (e) {
|
||||
trip.updateUUID("error");
|
||||
|
||||
// Format the error message to be more user friendly
|
||||
String errorDescription;
|
||||
if (e is DioException) {
|
||||
errorDescription = e.message ?? "Unknown error";
|
||||
} else if (e is SocketException) {
|
||||
errorDescription = "No internet connection";
|
||||
} else if (e is TimeoutException) {
|
||||
errorDescription = "Request timed out";
|
||||
} else {
|
||||
errorDescription = "Unknown error";
|
||||
}
|
||||
|
||||
String errorMessage = """
|
||||
We're sorry, the following error was generated:
|
||||
|
||||
${errorDescription.trim()}
|
||||
""".trim();
|
||||
|
||||
trip.updateError(errorMessage);
|
||||
log(e.toString());
|
||||
log(errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// handle more specific errors
|
||||
if (response.statusCode != 200) {
|
||||
trip.updateUUID("error");
|
||||
String errorDescription;
|
||||
if (response.data.runtimeType == String) {
|
||||
errorDescription = response.data;
|
||||
} else if (response.data.runtimeType == Map<String, dynamic>) {
|
||||
errorDescription = response.data["detail"] ?? "Unknown error";
|
||||
} else {
|
||||
errorDescription = "Unknown error";
|
||||
}
|
||||
|
||||
String errorMessage = """
|
||||
We're sorry, our servers generated the following error:
|
||||
|
||||
${errorDescription.trim()}
|
||||
Please try again.
|
||||
""".trim();
|
||||
trip.updateError(errorMessage);
|
||||
log(errorMessage);
|
||||
// Actualy no need to throw an exception, we can just log the error and let the user retry
|
||||
// throw Exception(errorDetail);
|
||||
} else {
|
||||
|
||||
// if the response data is not json, throw an error
|
||||
if (response.data is! Map<String, dynamic>) {
|
||||
log("${response.data.runtimeType}");
|
||||
trip.updateUUID("error");
|
||||
String errorMessage = """
|
||||
We're sorry, our servers generated the following error:
|
||||
|
||||
${response.data.trim()}
|
||||
Please try again.
|
||||
""".trim();
|
||||
trip.updateError(errorMessage);
|
||||
log(errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, dynamic> json = response.data;
|
||||
|
||||
// only fill in the trip "meta" data for now
|
||||
trip.loadFromJson(json);
|
||||
|
||||
// now fill the trip with landmarks
|
||||
// we are going to recreate ALL the landmarks from the information given by the api
|
||||
trip.landmarks.remove(trip.landmarks.first);
|
||||
String? nextUUID = json["first_landmark_uuid"];
|
||||
while (nextUUID != null) {
|
||||
var (landmark, newUUID) = await fetchLandmark(nextUUID);
|
||||
trip.addLandmark(landmark);
|
||||
nextUUID = newUUID;
|
||||
}
|
||||
|
||||
log(response.data.toString());
|
||||
// Also save the trip for the user's convenience
|
||||
savedTrips.addTrip(trip);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 {
|
||||
final response = await dio.get(
|
||||
"/landmark/$uuid"
|
||||
);
|
||||
|
||||
// handle errors
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Failed to load landmark');
|
||||
}
|
||||
if (response.data["detail"] != null) {
|
||||
throw Exception(response.data["detail"]);
|
||||
}
|
||||
// log(response.data.toString());
|
||||
Map<String, dynamic> json = response.data;
|
||||
String? nextUUID = json["next_uuid"];
|
||||
Landmark landmark = Landmark.fromJson(json);
|
||||
patchLandmarkImage(landmark);
|
||||
return (landmark, nextUUID);
|
||||
}
|
||||
50
frontend/lib/old/utils/get_first_page.dart
Normal file
50
frontend/lib/old/utils/get_first_page.dart
Normal file
@@ -0,0 +1,50 @@
|
||||
import 'package:anyway/main.dart';
|
||||
import 'package:anyway/pages/no_trips_page.dart';
|
||||
import 'package:anyway/structs/agreement.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:anyway/pages/current_trip.dart';
|
||||
import 'package:anyway/pages/onboarding.dart';
|
||||
|
||||
|
||||
Widget getFirstPage() {
|
||||
// check if the user has already seen the onboarding and agreed to the terms of service
|
||||
return FutureBuilder(
|
||||
future: getAgreement(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.hasData) {
|
||||
Agreement agrement = snapshot.data!;
|
||||
if (agrement.agreed) {
|
||||
return FutureBuilder(
|
||||
future: savedTrips.loadTrips(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.hasData) {
|
||||
List<Trip> trips = snapshot.data!;
|
||||
if (trips.isNotEmpty) {
|
||||
return TripPage(trip: trips[0]);
|
||||
} else {
|
||||
return const NoTripsPage();
|
||||
}
|
||||
} else {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
} else {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return const OnboardingPage();
|
||||
}
|
||||
} else {
|
||||
return const OnboardingPage();
|
||||
}
|
||||
} else {
|
||||
return const OnboardingPage();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
71
frontend/lib/old/utils/load_landmark_image.dart
Normal file
71
frontend/lib/old/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);
|
||||
}
|
||||
40
frontend/lib/old/utils/load_trips.dart
Normal file
40
frontend/lib/old/utils/load_trips.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import 'package:anyway/structs/trip.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class SavedTrips extends ChangeNotifier {
|
||||
List<Trip> _trips = [];
|
||||
|
||||
List<Trip> get trips => _trips;
|
||||
|
||||
Future<List<Trip>> 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();
|
||||
return trips;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user