Compare commits

...

4 Commits

Author SHA1 Message Date
dd48fda99f WIP: loading indicator
All checks were successful
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 4m9s
Build and release debug APK / Build APK (pull_request) Successful in 10m28s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 56s
2024-12-18 13:09:53 +01:00
d992b62533 tentatively shrink trip overview, nicer onboarding 2024-12-17 11:17:59 +01:00
e78bee4597 some more images 2024-12-17 10:28:33 +01:00
d186a51a87 WIP: ladnmark card adjustments 2024-12-15 16:30:17 +01:00
15 changed files with 487 additions and 236 deletions

111
backend/test.py Normal file
View File

@ -0,0 +1,111 @@
import numpy as np
def euclidean_distance(p1, p2):
print(p1, p2)
return np.sqrt((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2)
def maximize_score(places, max_distance, fixed_entry, top_k=3):
"""
Maximizes the total score of visited places while staying below the maximum distance.
Parameters:
places (list of tuples): Each tuple contains (score, (x, y), location).
max_distance (float): The maximum distance that can be traveled.
fixed_entry (tuple): The place that needs to be visited independently of its score.
top_k (int): Number of top candidates to consider in each iteration.
Returns:
list of tuples: The visited places.
float: The total score of the visited places.
"""
# Initialize total distance and score
total_distance = 0
total_score = 0
visited_places = []
# Add the fixed entry to the visited list
score, (x, y), _ = fixed_entry
visited_places.append(fixed_entry)
total_score += score
# Remove the fixed entry from the list of places
remaining_places = [place for place in places if place != fixed_entry]
# Sort remaining places by score-to-distance ratio
remaining_places.sort(key=lambda p: p[0] / euclidean_distance((x, y), (p[1][0], p[1][1])), reverse=True)
# Add places to the visited list if they don't exceed the maximum distance
current_location = (x, y)
while remaining_places and total_distance < max_distance:
# Consider top_k candidates
candidates = remaining_places[:top_k]
best_candidate = None
best_score_increase = -np.inf
for candidate in candidates:
score, (cx, cy), location = candidate
distance = euclidean_distance(current_location, (cx, cy))
if total_distance + distance <= max_distance:
score_increase = score / distance
if score_increase > best_score_increase:
best_score_increase = score_increase
best_candidate = candidate
if best_candidate:
visited_places.append(best_candidate)
total_distance += euclidean_distance(current_location, best_candidate[1])
total_score += best_candidate[0]
current_location = best_candidate[1]
remaining_places.remove(best_candidate)
else:
break
return visited_places, total_score
# Example usage
places = [
(10, (0, 0), 'A'),
(8, (4, 2), 'B'),
(15, (6, 4), 'C'),
(7, (5, 6), 'D'),
(12, (1, 8), 'E'),
(14, (34, 10), 'F'),
(15, (65, 12), 'G'),
(12, (3, 14), 'H'),
(12, (15, 1), 'I'),
(7, (17, 4), 'J'),
(12, (3, 3), 'K'),
(4, (21, 22), 'L'),
(12, (23, 24), 'M'),
(4, (25, 26), 'N'),
(2, (27, 28), 'O'),
]
fixed_entry = (10, (0, 0), 'A')
max_distance = 50
visited_places, total_score = maximize_score(places, max_distance, fixed_entry)
print("Visited Places:", visited_places)
print("Total Score:", total_score)
import matplotlib.pyplot as plt
# Plot the route
def plot_route(visited_places):
x_coords = [place[1][0] for place in visited_places]
y_coords = [place[1][1] for place in visited_places]
labels = [place[2] for place in visited_places]
plt.figure(figsize=(10, 6))
plt.plot(x_coords, y_coords, marker='o', linestyle='-', color='b')
for i, label in enumerate(labels):
plt.text(x_coords[i], y_coords[i], label, fontsize=12, ha='right')
plt.title('Route of Visited Places')
plt.xlabel('X Coordinate')
plt.ylabel('Y Coordinate')
plt.grid(True)
plt.savefig('route.png')
plot_route(visited_places)

View File

@ -1,10 +1,12 @@
import 'package:anyway/utils/get_first_page.dart';
import 'package:anyway/utils/load_trips.dart';
import 'package:flutter/material.dart';
import 'package:anyway/constants.dart';
void main() => runApp(const App());
final GlobalKey<ScaffoldMessengerState> rootScaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
final SavedTrips savedTrips = SavedTrips();
class App extends StatelessWidget {
const App({super.key});

View File

@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
import 'package:anyway/modules/landmark_card.dart';
import 'package:anyway/structs/landmark.dart';
import 'package:anyway/structs/trip.dart';
import 'package:anyway/main.dart';
@ -25,30 +24,7 @@ List<Widget> landmarksList(Trip trip) {
for (Landmark landmark in trip.landmarks) {
children.add(
Dismissible(
key: ValueKey<int>(landmark.hashCode),
child: LandmarkCard(landmark),
dismissThresholds: {DismissDirection.endToStart: 0.95, DismissDirection.startToEnd: 0.95},
onDismissed: (direction) {
log('Removing ${landmark.name}');
trip.removeLandmark(landmark);
rootScaffoldMessengerKey.currentState!.showSnackBar(
SnackBar(content: Text("We won't show ${landmark.name} again"))
);
},
background: Container(color: Colors.red),
secondaryBackground: Container(
color: Colors.red,
child: Icon(
Icons.delete,
color: Colors.white,
),
padding: EdgeInsets.all(15),
alignment: Alignment.centerRight,
),
)
LandmarkCard(landmark, trip),
);
if (landmark.next != null) {

View File

@ -34,29 +34,85 @@ Widget loadingText(Trip trip) => FutureBuilder(
Widget greeter;
if (snapshot.hasData) {
greeter = AutoSizeText(
maxLines: 1,
'Generating your trip to ${snapshot.data}...',
greeter = AnimatedGradientText(
text: 'Generating your trip to ${snapshot.data}...',
style: greeterStyle,
);
} else if (snapshot.hasError) {
// the exact error is shown in the central part of the trip overview. No need to show it here
greeter = AutoSizeText(
maxLines: 1,
'Error while loading trip.',
greeter = AnimatedGradientText(
text: 'Error while loading trip.',
style: greeterStyle,
);
);
} else {
greeter = AutoSizeText(
maxLines: 1,
'Generating your trip...',
greeter = AnimatedGradientText(
text: 'Generating your trip...',
style: greeterStyle,
);
);
}
return greeter;
}
);
class AnimatedGradientText extends StatefulWidget {
final String text;
final TextStyle style;
const AnimatedGradientText({
Key? key,
required this.text,
required this.style,
}) : super(key: key);
@override
_AnimatedGradientTextState createState() => _AnimatedGradientTextState();
}
class _AnimatedGradientTextState extends State<AnimatedGradientText> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return ShaderMask(
shaderCallback: (bounds) {
return LinearGradient(
colors: [Colors.blue, Colors.red, Colors.blue],
stops: [
_controller.value - 1.0,
_controller.value,
_controller.value + 1.0,
],
tileMode: TileMode.mirror,
).createShader(bounds);
},
child: Text(
widget.text,
style: widget.style,
),
);
},
);
}
}

View File

@ -3,7 +3,6 @@ import 'package:anyway/main.dart';
import 'package:anyway/structs/trip.dart';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class saveButton extends StatefulWidget {
@ -19,8 +18,9 @@ class _saveButtonState extends State<saveButton> {
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () async {
SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() => widget.trip.toPrefs(prefs));
savedTrips.addTrip(widget.trip);
// SharedPreferences prefs = await SharedPreferences.getInstance();
// setState(() => widget.trip.toPrefs(prefs));
rootScaffoldMessengerKey.currentState!.showSnackBar(
SnackBar(
content: Text('Trip saved'),

View File

@ -1,3 +1,5 @@
import 'package:anyway/main.dart';
import 'package:anyway/structs/trip.dart';
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:url_launcher/url_launcher.dart';
@ -6,8 +8,12 @@ import 'package:anyway/structs/landmark.dart';
class LandmarkCard extends StatefulWidget {
final Landmark landmark;
final Trip parentTrip;
LandmarkCard(this.landmark);
LandmarkCard(
this.landmark,
this.parentTrip,
);
@override
_LandmarkCardState createState() => _LandmarkCardState();
@ -17,107 +23,149 @@ class LandmarkCard extends StatefulWidget {
class _LandmarkCardState extends State<LandmarkCard> {
@override
Widget build(BuildContext context) {
if (widget.landmark.type == typeStart || widget.landmark.type == typeFinish) {
return TextButton.icon(
onPressed: () {},
icon: widget.landmark.type.icon,
label: Text(widget.landmark.name),
);
}
// else:
return Container(
height: 160,
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0),
),
elevation: 5,
clipBehavior: Clip.antiAliasWithSaveLayer,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container( // the image on the left
// inherit the height of the parent container
height: double.infinity,
// force a fixed width
width: 160,
child: CachedNetworkImage(
imageUrl: widget.landmark.imageURL ?? '',
placeholder: (context, url) => Center(child: CircularProgressIndicator()),
errorWidget: (context, error, stackTrace) => Icon(Icons.question_mark_outlined),
fit: BoxFit.cover,
),
),
Flexible(
child: Padding(
padding: EdgeInsets.all(10),
child: Column(
children: [
Row(
children: [
Flexible(
child: Text(
widget.landmark.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
maxLines: 2,
),
)
],
),
if (widget.landmark.nameEN != null)
Row(
children: [
Flexible(
child: Text(
widget.landmark.nameEN!,
style: const TextStyle(
fontSize: 16,
),
maxLines: 1,
),
)
],
),
SingleChildScrollView(
// allows the buttons to be scrolled
scrollDirection: Axis.horizontal,
child: Wrap(
spacing: 10,
// show the type, the website, and the wikipedia link as buttons/labels in a row
children: [
TextButton.icon(
onPressed: () {},
icon: widget.landmark.type.icon,
label: Text(widget.landmark.type.name),
),
if (widget.landmark.duration != null && widget.landmark.duration!.inMinutes > 0)
TextButton.icon(
onPressed: () {},
icon: Icon(Icons.hourglass_bottom),
label: Text('${widget.landmark.duration!.inMinutes} minutes'),
),
if (widget.landmark.websiteURL != null)
TextButton.icon(
onPressed: () async {
// open a browser with the website link
await launchUrl(Uri.parse(widget.landmark.websiteURL!));
},
icon: Icon(Icons.link),
label: Text('Website'),
),
// if (widget.landmark.wikipediaURL != null)
// TextButton.icon(
// onPressed: () async {
// // open a browser with the wikipedia link
// await launchUrl(Uri.parse(widget.landmark.wikipediaURL!));
// },
// icon: Icon(Icons.book),
// label: Text('Wikipedia'),
// ),
],
),
),
],
),
),
),
],
// if the image is available, display it on the left side of the card, otherwise only display the text
child: widget.landmark.imageURL != null ? splitLayout() : textLayout(),
),
);
}
Widget splitLayout() {
// If an image is available, display it on the left side of the card
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
// the image on the left
width: 160,
height: 160,
child: CachedNetworkImage(
imageUrl: widget.landmark.imageURL ?? '',
placeholder: (context, url) => Center(child: CircularProgressIndicator()),
errorWidget: (context, error, stackTrace) => Icon(Icons.question_mark_outlined),
fit: BoxFit.cover,
),
),
Flexible(
child: textLayout(),
),
],
);
}
Widget textLayout() {
return Padding(
padding: EdgeInsets.all(10),
child: Column(
children: [
Row(
children: [
Flexible(
child: Text(
widget.landmark.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
maxLines: 2,
),
)
],
),
if (widget.landmark.nameEN != null)
Row(
children: [
Flexible(
child: Text(
widget.landmark.nameEN!,
style: const TextStyle(
fontSize: 16,
),
maxLines: 1,
),
)
],
),
Padding(padding: EdgeInsets.only(top: 10)),
Align(
alignment: Alignment.centerLeft,
child: SingleChildScrollView(
// allows the buttons to be scrolled
scrollDirection: Axis.horizontal,
child: Wrap(
spacing: 10,
// show the type, the website, and the wikipedia link as buttons/labels in a row
children: [
TextButton.icon(
onPressed: () {},
icon: widget.landmark.type.icon,
label: Text(widget.landmark.type.name),
),
if (widget.landmark.duration != null && widget.landmark.duration!.inMinutes > 0)
TextButton.icon(
onPressed: () {},
icon: Icon(Icons.hourglass_bottom),
label: Text('${widget.landmark.duration!.inMinutes} minutes'),
),
if (widget.landmark.websiteURL != null)
TextButton.icon(
onPressed: () async {
// open a browser with the website link
await launchUrl(Uri.parse(widget.landmark.websiteURL!));
},
icon: Icon(Icons.link),
label: Text('Website'),
),
PopupMenuButton(
icon: Icon(Icons.settings),
style: TextButtonTheme.of(context).style,
itemBuilder: (context) => [
PopupMenuItem(
child: ListTile(
leading: Icon(Icons.delete),
title: Text('Delete'),
onTap: () async {
widget.parentTrip.removeLandmark(widget.landmark);
rootScaffoldMessengerKey.currentState!.showSnackBar(
SnackBar(content: Text("We won't show ${widget.landmark.name} again"))
);
},
),
),
PopupMenuItem(
child: ListTile(
leading: Icon(Icons.star),
title: Text('Favorite'),
onTap: () async {
// delete the landmark
// await deleteLandmark(widget.landmark);
},
),
),
],
)
],
),
),
),
],
),
);
}

View File

@ -1,11 +1,12 @@
import 'package:anyway/pages/current_trip.dart';
import 'package:anyway/utils/load_trips.dart';
import 'package:flutter/material.dart';
import 'package:anyway/structs/trip.dart';
class TripsOverview extends StatefulWidget {
final Future<List<Trip>> trips;
final SavedTrips trips;
const TripsOverview({
super.key,
required this.trips,
@ -16,49 +17,34 @@ class TripsOverview extends StatefulWidget {
}
class _TripsOverviewState extends State<TripsOverview> {
Widget listBuild (BuildContext context, AsyncSnapshot<List<Trip>> snapshot) {
Widget listBuild (BuildContext context, SavedTrips trips) {
List<Widget> children;
if (snapshot.hasData) {
children = List<Widget>.generate(snapshot.data!.length, (index) {
Trip trip = snapshot.data![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: Icon(Icons.pin_drop),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => TripPage(trip: trip)
)
);
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 ...");
}
},
);
});
} else if (snapshot.hasError) {
children = [
const Icon(
Icons.error_outline,
color: Colors.red,
size: 60,
),
Padding(
padding: const EdgeInsets.only(top: 16),
child: Text('Error: ${snapshot.error}'),
),
];
} else {
children = [Center(child: CircularProgressIndicator())];
}
leading: Icon(Icons.pin_drop),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => TripPage(trip: trip)
)
);
},
);
});
return ListView(
children: children,
@ -68,9 +54,11 @@ class _TripsOverviewState extends State<TripsOverview> {
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: widget.trips,
builder: listBuild,
return ListenableBuilder(
listenable: widget.trips,
builder: (BuildContext context, Widget? child) {
return listBuild(context, widget.trips);
}
);
}
}

View File

@ -1,3 +1,4 @@
import 'package:anyway/main.dart';
import 'package:anyway/modules/help_dialog.dart';
import 'package:anyway/pages/current_trip.dart';
import 'package:anyway/pages/settings.dart';
@ -38,7 +39,8 @@ class _BasePageState extends State<BasePage> {
@override
Widget build(BuildContext context) {
Future<List<Trip>> trips = loadTrips();
savedTrips.loadTrips();
return Scaffold(
appBar: AppBar(
@ -98,11 +100,11 @@ class _BasePageState extends State<BasePage> {
// through the options in the drawer if there isn't enough vertical
// space to fit everything.
Expanded(
child: TripsOverview(trips: trips),
child: TripsOverview(trips: savedTrips),
),
ElevatedButton(
onPressed: () async {
removeAllTripsFromPrefs();
savedTrips.clearTrips();
},
child: const Text('Clear trips'),
),

View File

@ -1,3 +1,6 @@
import 'dart:ui';
import 'package:anyway/constants.dart';
import 'package:anyway/modules/onboarding_card.dart';
import 'package:anyway/pages/new_trip_location.dart';
import 'package:flutter/material.dart';
@ -33,32 +36,51 @@ class OnboardingPage extends StatefulWidget {
}
class _OnboardingPageState extends State<OnboardingPage> {
final PageController _controller = PageController();
@override
Widget build(BuildContext context) {
final PageController _controller = PageController();
return Scaffold(
body: Stack(
children: [
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.red, Colors.blue],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: PageView(
controller: _controller,
children: List.generate(
onboardingCards.length,
(index) {
return Container(
alignment: Alignment.center,
child: onboardingCards[index],
);
}
),
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Stack(
children: [
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: APP_GRADIENT.colors,
stops: [
(_controller.hasClients ? _controller.page ?? _controller.initialPage : _controller.initialPage) / onboardingCards.length,
(_controller.hasClients ? _controller.page ?? _controller.initialPage + 1 : _controller.initialPage + 1) / onboardingCards.length,
],
),
),
),
BackdropFilter(
filter: ImageFilter.blur(sigmaX: 100, sigmaY: 100),
child: Container(
color: Colors.black.withOpacity(0),
),
),
],
);
},
),
PageView(
controller: _controller,
children: List.generate(
onboardingCards.length,
(index) {
return Container(
alignment: Alignment.center,
child: onboardingCards[index],
);
}
),
),
],
@ -75,10 +97,10 @@ class _OnboardingPageState extends State<OnboardingPage> {
_controller.nextPage(duration: Duration(milliseconds: 500), curve: Curves.ease);
}
},
label: ListenableBuilder(
listenable: _controller,
label: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
if (_controller.page == onboardingCards.length - 1) {
if ((_controller.page ?? _controller.initialPage) == onboardingCards.length - 1) {
return Row(
children: [
const Text("Start planning!"),

View File

@ -125,7 +125,7 @@ class LandmarkType {
LandmarkType({required this.name, this.icon = const Icon(Icons.location_on)}) {
switch (name) {
case 'sightseeing':
icon = const Icon(Icons.church);
icon = const Icon(Icons.castle);
break;
case 'nature':
icon = const Icon(Icons.eco);

View File

@ -113,10 +113,3 @@ LinkedList<Landmark> readLandmarks(SharedPreferences prefs, String? firstUUID) {
}
return landmarks;
}
void removeAllTripsFromPrefs () async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.clear();
}

View File

@ -93,6 +93,11 @@ patchLandmarkImage(Landmark landmark) async {
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;
}
}

View File

@ -5,23 +5,37 @@ import 'package:anyway/utils/load_trips.dart';
import 'package:flutter/material.dart';
Widget getFirstPage() {
Future<List<Trip>> trips = loadTrips();
// test if there are any active trips
// if there are, return the trip list
// if there are not, return the onboarding page
return FutureBuilder(
future: trips,
builder: (context, snapshot) {
if (snapshot.hasData) {
List<Trip> availableTrips = snapshot.data!;
if (availableTrips.isNotEmpty) {
return TripPage(trip: availableTrips[0]);
} else {
return OnboardingPage();
}
SavedTrips trips = SavedTrips();
trips.loadTrips();
return ListenableBuilder(
listenable: trips,
builder: (BuildContext context, Widget? child) {
List<Trip> items = trips.trips;
if (items.isNotEmpty) {
return TripPage(trip: items[0]);
} else {
return CircularProgressIndicator();
return OnboardingPage();
}
}
);
// Future<List<Trip>> trips = loadTrips();
// // test if there are any active trips
// // if there are, return the trip list
// // if there are not, return the onboarding page
// return FutureBuilder(
// future: trips,
// builder: (context, snapshot) {
// if (snapshot.hasData) {
// List<Trip> availableTrips = snapshot.data!;
// if (availableTrips.isNotEmpty) {
// return TripPage(trip: availableTrips[0]);
// } else {
// return OnboardingPage();
// }
// } else {
// return CircularProgressIndicator();
// }
// }
// );
}

View File

@ -58,3 +58,14 @@ Future<String?> getImageUrlFromName(String title) async {
}
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);
}

View File

@ -1,16 +1,39 @@
import 'package:anyway/structs/trip.dart';
import 'package:shared_preferences/shared_preferences.dart';
Future<List<Trip>> loadTrips() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
import 'package:flutter/foundation.dart';
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));
class SavedTrips extends ChangeNotifier {
List<Trip> _trips = [];
List<Trip> get trips => _trips;
void loadTrips() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
List<Trip> trips = [];
Set<String> keys = prefs.getKeys();
for (String key in keys) {
if (key.startsWith('trip_')) {
String uuid = key.replaceFirst('trip_', '');
trips.add(Trip.fromPrefs(prefs, uuid));
}
}
_trips = trips;
notifyListeners();
}
void addTrip(Trip trip) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
trip.toPrefs(prefs);
_trips.add(trip);
notifyListeners();
}
void clearTrips () async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.clear();
_trips = [];
notifyListeners();
}
return trips;
}