From eede94add407cc99bf2040a5b713d703bd34b9db Mon Sep 17 00:00:00 2001 From: Remy Moll Date: Sun, 23 Jun 2024 21:19:06 +0200 Subject: [PATCH] working save and load functionality with custom datastructures --- frontend/lib/layout.dart | 15 +-- frontend/lib/modules/greeter.dart | 99 ++++++++++------ frontend/lib/modules/landmark_card.dart | 2 +- frontend/lib/modules/landmarks_overview.dart | 15 ++- frontend/lib/modules/map.dart | 32 ++++-- frontend/lib/modules/trips_overview.dart | 2 +- frontend/lib/pages/overview.dart | 20 ++-- frontend/lib/structs/landmark.dart | 51 +++++--- frontend/lib/structs/trip.dart | 36 +++++- frontend/lib/utils/load_trips.dart | 115 +++++++++++++++---- 10 files changed, 279 insertions(+), 108 deletions(-) diff --git a/frontend/lib/layout.dart b/frontend/lib/layout.dart index 63f6c82..d1b80a8 100644 --- a/frontend/lib/layout.dart +++ b/frontend/lib/layout.dart @@ -12,7 +12,7 @@ import 'package:fast_network_navigation/pages/profile.dart'; // A side drawer is used to switch between pages class BasePage extends StatefulWidget { final String mainScreen; - final Trip? trip; + final Future? trip; const BasePage({ super.key, @@ -30,11 +30,10 @@ class _BasePageState extends State { Widget build(BuildContext context) { Widget currentView = const Text("loading..."); Future> trips = loadTrips(); - Future firstTrip = getFirstTrip(trips); - // Future trip = Future(trips[0]); + if (widget.mainScreen == "map") { - currentView = NavigationOverview(trip: firstTrip); + currentView = NavigationOverview(trip: widget.trip ?? getFirstTrip(trips)); } else if (widget.mainScreen == "tutorial") { currentView = TutorialPage(); } else if (widget.mainScreen == "profile") { @@ -88,12 +87,8 @@ class _BasePageState extends State { child: TripsOverview(trips: trips), ), ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const NewTripPage() - ) - ); + onPressed: () async { + removeAllTripsFromPrefs(); }, child: const Text('Clear trips'), ), diff --git a/frontend/lib/modules/greeter.dart b/frontend/lib/modules/greeter.dart index 767b43e..8ed404f 100644 --- a/frontend/lib/modules/greeter.dart +++ b/frontend/lib/modules/greeter.dart @@ -1,44 +1,73 @@ +import 'package:fast_network_navigation/structs/trip.dart'; + import 'package:flutter/material.dart'; -Widget Greeter(ThemeData theme, {bool full = false}) { - String greeterText = ""; - try { - String cityName = getCityName(); - greeterText = "Welcome to $cityName!"; - } catch (e) { - greeterText = "Welcome ..."; - } +class Greeter extends StatefulWidget { + final Future trip; + final bool standalone; - Widget topGreeter = Text( - greeterText, - style: TextStyle(color: theme.primaryColor, fontSize: 24.0, fontWeight: FontWeight.bold), - maxLines: 1, - ); + Greeter({ + required this.standalone, + required this.trip + }); - Widget bottomGreeter = Container(); - if (full) { - bottomGreeter = Text( - "Busy day ahead? Here is how to make the most of it!", - style: TextStyle(color: Colors.black, fontSize: 18), - textAlign: TextAlign.center, - ); - } - Widget greeter = Center( - child: Column( - children: [ - if (!full) Padding(padding: EdgeInsets.only(top: 24.0)), - topGreeter, - if (full) bottomGreeter, - Padding(padding: EdgeInsets.only(bottom: 24.0)), - ], - ), - ); - - return greeter; + @override + State createState() => _GreeterState(); } -String getCityName() { - return "Paris"; +class _GreeterState extends State { + Widget greeterBuild (BuildContext context, AsyncSnapshot snapshot) { + ThemeData theme = Theme.of(context); + String cityName = ""; + if (snapshot.hasData) { + cityName = snapshot.data?.cityName ?? '...'; + } else if (snapshot.hasError) { + cityName = "error"; + } else { // still awaiting the cityname + cityName = "..."; + } + + Widget topGreeter = Text( + 'Welcome to $cityName!', + style: TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24), + ); + + if (widget.standalone) { + return Center( + child: Padding( + padding: EdgeInsets.only(top: 24.0), + child: topGreeter, + ), + ); + } else { + return Center( + child: Column( + children: [ + Padding(padding: EdgeInsets.only(top: 24.0)), + topGreeter, + bottomGreeter, + Padding(padding: EdgeInsets.only(bottom: 24.0)), + ], + ) + ); + } + } + + Widget bottomGreeter = const Text( + "Busy day ahead? Here is how to make the most of it!", + style: TextStyle(color: Colors.black, fontSize: 18), + textAlign: TextAlign.center, + ); + + + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: widget.trip, + builder: greeterBuild, + ); + } } \ No newline at end of file diff --git a/frontend/lib/modules/landmark_card.dart b/frontend/lib/modules/landmark_card.dart index d0bfb83..070ff54 100644 --- a/frontend/lib/modules/landmark_card.dart +++ b/frontend/lib/modules/landmark_card.dart @@ -32,7 +32,7 @@ class _LandmarkCardState extends State { // force a fixed width width: 160, child: Image.network( - widget.landmark.imageURL!, + widget.landmark.imageURL ?? '', errorBuilder: (context, error, stackTrace) => Icon(Icons.question_mark_outlined), // TODO: make this a switch statement to load a placeholder if null // cover the whole container meaning the image will be cropped diff --git a/frontend/lib/modules/landmarks_overview.dart b/frontend/lib/modules/landmarks_overview.dart index 0d05c56..a29cd2a 100644 --- a/frontend/lib/modules/landmarks_overview.dart +++ b/frontend/lib/modules/landmarks_overview.dart @@ -5,6 +5,7 @@ import 'package:fast_network_navigation/structs/landmark.dart'; import 'package:fast_network_navigation/structs/trip.dart'; import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; @@ -31,7 +32,7 @@ class _LandmarksOverviewState extends State { builder: (BuildContext context, AsyncSnapshot> snapshot) { List children; if (snapshot.hasData) { - children = [landmarksWithSteps(snapshot.data!)]; + children = [landmarksWithSteps(snapshot.data!), saveButton()]; } else if (snapshot.hasError) { children = [ const Icon( @@ -41,7 +42,7 @@ class _LandmarksOverviewState extends State { ), Padding( padding: const EdgeInsets.only(top: 16), - child: Text('Error: ${snapshot.error}'), + child: Text('Error: ${snapshot.error}', style: TextStyle(fontSize: 12)), ), ]; } else { @@ -57,6 +58,15 @@ class _LandmarksOverviewState extends State { ), ); } + Widget saveButton() => ElevatedButton( + onPressed: () async { + Trip? trip = await widget.trip; + SharedPreferences prefs = await SharedPreferences.getInstance(); + trip?.toPrefs(prefs); + }, + child: const Text('Save'), + ); + } Widget landmarksWithSteps(LinkedList landmarks) { @@ -117,3 +127,4 @@ Future> getLandmarks (Future? trip) async { Trip tripf = await trip!; return tripf.landmarks; } + diff --git a/frontend/lib/modules/map.dart b/frontend/lib/modules/map.dart index 9c8fe3f..916d231 100644 --- a/frontend/lib/modules/map.dart +++ b/frontend/lib/modules/map.dart @@ -27,12 +27,18 @@ class _MapWidgetState extends State { Set markers = {}; - void _onMapCreated(GoogleMapController controller) { + void _onMapCreated(GoogleMapController controller) async { mapController = controller; + Trip? trip = await widget.trip; + List? newLocation = trip?.landmarks.first.location; + if (newLocation != null) { + CameraUpdate update = CameraUpdate.newLatLng(LatLng(newLocation[0], newLocation[1])); + controller.moveCamera(update); + } drawLandmarks(); } - + void _onCameraIdle() { // print(mapController.getLatLng(ScreenCoordinate(x: 0, y: 0))); } @@ -41,16 +47,18 @@ class _MapWidgetState extends State { void drawLandmarks() async { // (re)draws landmarks on the map Trip? trip = await widget.trip; - LinkedList landmarks = trip!.landmarks; - setState(() { - for (Landmark landmark in landmarks) { - markers.add(Marker( - markerId: MarkerId(landmark.name), - position: LatLng(landmark.location[0], landmark.location[1]), - infoWindow: InfoWindow(title: landmark.name, snippet: landmark.type.name), - )); - } - }); + LinkedList? landmarks = trip?.landmarks; + if (landmarks != null){ + setState(() { + for (Landmark landmark in landmarks) { + markers.add(Marker( + markerId: MarkerId(landmark.name), + position: LatLng(landmark.location[0], landmark.location[1]), + infoWindow: InfoWindow(title: landmark.name, snippet: landmark.type.name), + )); + } + }); + } } diff --git a/frontend/lib/modules/trips_overview.dart b/frontend/lib/modules/trips_overview.dart index bbc6a6e..bb803bb 100644 --- a/frontend/lib/modules/trips_overview.dart +++ b/frontend/lib/modules/trips_overview.dart @@ -30,7 +30,7 @@ class _TripsOverviewState extends State { onTap: () { Navigator.of(context).push( MaterialPageRoute( - builder: (context) => BasePage(mainScreen: "map", trip: trip) + builder: (context) => BasePage(mainScreen: "map", trip: Future.value(trip)) ) ); }, diff --git a/frontend/lib/pages/overview.dart b/frontend/lib/pages/overview.dart index 22565f5..5354641 100644 --- a/frontend/lib/pages/overview.dart +++ b/frontend/lib/pages/overview.dart @@ -1,10 +1,11 @@ -import 'package:fast_network_navigation/modules/greeter.dart'; -import 'package:fast_network_navigation/structs/trip.dart'; import 'package:flutter/material.dart'; import 'package:sliding_up_panel/sliding_up_panel.dart'; +import 'package:fast_network_navigation/structs/trip.dart'; + import 'package:fast_network_navigation/modules/landmarks_overview.dart'; import 'package:fast_network_navigation/modules/map.dart'; +import 'package:fast_network_navigation/modules/greeter.dart'; @@ -25,16 +26,16 @@ class _NavigationOverviewState extends State { @override Widget build(BuildContext context) { - final ThemeData theme = Theme.of(context); return SlidingUpPanel( renderPanelSheet: false, - panel: _floatingPanel(theme), - collapsed: _floatingCollapsed(theme), + panel: _floatingPanel(), + collapsed: _floatingCollapsed(), body: MapWidget(trip: widget.trip) ); } - Widget _floatingCollapsed(ThemeData theme){ + Widget _floatingCollapsed(){ + final ThemeData theme = Theme.of(context); return Container( decoration: BoxDecoration( color: theme.canvasColor, @@ -42,11 +43,12 @@ class _NavigationOverviewState extends State { boxShadow: [] ), - child: Greeter(theme) + child: Greeter(standalone: true, trip: widget.trip) ); } - Widget _floatingPanel(ThemeData theme){ + Widget _floatingPanel(){ + final ThemeData theme = Theme.of(context); return Container( decoration: BoxDecoration( color: Colors.white, @@ -64,7 +66,7 @@ class _NavigationOverviewState extends State { child: SingleChildScrollView( child: Column( children: [ - Greeter(theme, full: true), + Greeter(standalone: false, trip: widget.trip), LandmarksOverview(trip: widget.trip), ], ), diff --git a/frontend/lib/structs/landmark.dart b/frontend/lib/structs/landmark.dart index 3780cd4..9e5e1ef 100644 --- a/frontend/lib/structs/landmark.dart +++ b/frontend/lib/structs/landmark.dart @@ -21,6 +21,7 @@ final class Landmark extends LinkedListEntry{ // final Landmark? next; final Duration? tripTime; + Landmark({ required this.uuid, required this.name, @@ -37,23 +38,28 @@ final class Landmark extends LinkedListEntry{ this.tripTime, }); + factory Landmark.fromJson(Map 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 location, - 'type': LandmarkType type, + 'location': List location, + 'type': String type, }) { - // parse the rest separately, they could be missing - final isSecondary = json['is_secondary'] as bool?; - final imageURL = json['image_url'] as String?; - final description = json['description'] as String?; - final duration = json['duration'] as Duration?; - final visited = json['visited'] as bool?; - - return Landmark( - uuid: uuid, name: name, location: location, type: type, isSecondary: isSecondary, imageURL: imageURL, description: description, duration: duration, visited: visited); + // refine the parsing on a few + List locationFixed = List.from(location); + // parse the rest separately, they could be missing + LandmarkType typeFixed = LandmarkType(name: type); + final isSecondary = json['is_secondary'] as bool?; + final imageURL = json['image_url'] as String?; + final description = json['description'] as String?; + var duration = Duration(minutes: json['duration'] ?? 0) as Duration?; + if (duration == const Duration()) {duration = null;}; + final visited = json['visited'] as bool?; + + return Landmark( + uuid: uuid, name: name, location: locationFixed, type: typeFixed, isSecondary: isSecondary, imageURL: imageURL, description: description, duration: duration, visited: visited); } else { throw FormatException('Invalid JSON: $json'); } @@ -64,6 +70,19 @@ final class Landmark extends LinkedListEntry{ bool operator ==(Object other) { return other is Landmark && uuid == other.uuid; } + + + Map toJson() => { + 'uuid': uuid, + 'name': name, + 'location': location, + 'type': type.name, + 'is_secondary': isSecondary, + 'image_url': imageURL, + 'description': description, + 'duration': duration?.inMinutes, + 'visited': visited + }; } @@ -80,8 +99,8 @@ class LandmarkType { } - -// Helper +// 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 json = jsonDecode(content!); @@ -89,3 +108,9 @@ class LandmarkType { return (Landmark.fromJson(json), nextUUID); } + +void landmarkToPrefs(SharedPreferences prefs, Landmark current, Landmark? next) { + Map json = current.toJson(); + json['next_uuid'] = next?.uuid; + prefs.setString('landmark_${current.uuid}', jsonEncode(json)); +} diff --git a/frontend/lib/structs/trip.dart b/frontend/lib/structs/trip.dart index 739717f..7ac40e5 100644 --- a/frontend/lib/structs/trip.dart +++ b/frontend/lib/structs/trip.dart @@ -14,36 +14,62 @@ class Trip { final LinkedList landmarks; // could be empty as well + Trip({ required this.uuid, required this.cityName, required this.landmarks, - }); + }); + factory Trip.fromJson(Map json) { return Trip( uuid: json['uuid'], - cityName: json['cityName'], + cityName: json['city_name'], landmarks: LinkedList() ); } + factory Trip.fromPrefs(SharedPreferences prefs, String uuid) { String? content = prefs.getString('trip_$uuid'); Map json = jsonDecode(content!); Trip trip = Trip.fromJson(json); String? firstUUID = json['entry_uuid']; - appendLandmarks(trip.landmarks, prefs, firstUUID); + readLandmarks(trip.landmarks, prefs, firstUUID); return trip; } + + + Map toJson() => { + 'uuid': uuid, + 'city_name': cityName, + 'entry_uuid': landmarks.first?.uuid ?? '' + }; + + + void toPrefs(SharedPreferences prefs){ + Map json = toJson(); + prefs.setString('trip_$uuid', jsonEncode(json)); + for (Landmark landmark in landmarks) { + landmarkToPrefs(prefs, landmark, landmark.next); + } + } } -// Helper -appendLandmarks(LinkedList landmarks, SharedPreferences prefs, String? firstUUID) { +// Helper +readLandmarks(LinkedList landmarks, SharedPreferences prefs, String? firstUUID) { while (firstUUID != null) { var (head, nextUUID) = getLandmarkFromPrefs(prefs, firstUUID); landmarks.add(head); firstUUID = nextUUID; } } + + + +void removeAllTripsFromPrefs () async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.clear(); +} diff --git a/frontend/lib/utils/load_trips.dart b/frontend/lib/utils/load_trips.dart index 2facd3d..3c90c4a 100644 --- a/frontend/lib/utils/load_trips.dart +++ b/frontend/lib/utils/load_trips.dart @@ -1,6 +1,5 @@ import 'dart:collection'; -import 'package:fast_network_navigation/structs/linked_landmarks.dart'; import 'package:fast_network_navigation/structs/trip.dart'; import 'package:fast_network_navigation/structs/landmark.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -18,27 +17,103 @@ Future> loadTrips() async { } if (trips.isEmpty) { - String now = DateTime.now().toString(); - trips.add( - Trip(uuid: '1', cityName: 'Paris (generated $now)', landmarks: LinkedList()) + Trip t1 = Trip(uuid: '1', cityName: 'Paris', landmarks: LinkedList()); + t1.landmarks.add( + Landmark( + uuid: '1', + name: "Eiffel Tower", + location: [48.859, 2.295], + type: LandmarkType(name: "Tower"), + imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a8/Tour_Eiffel_Wikimedia_Commons.jpg/1037px-Tour_Eiffel_Wikimedia_Commons.jpg" + ), ); - // Trip(uuid: "1", cityName: "Paris", landmarks: [ - // Landmark(name: "Landmark 1", location: [48.85, 2.35], type: LandmarkType(name: "Type 1")), - // Landmark(name: "Landmark 2", location: [48.86, 2.36], type: LandmarkType(name: "Type 2")), - // Landmark(name: "Landmark 3", location: [48.75, 2.3], type: LandmarkType(name: "Type 3")), - // Landmark(name: "Landmark 4", location: [48.9, 2.4], type: LandmarkType(name: "Type 4")), - // Landmark(name: "Landmark 5", location: [48.91, 2.45], type: LandmarkType(name: "Type 5")), - // ])); - // trips.add(Trip(uuid: "2", cityName: "Vienna", landmarks: [])); - // trips.add(Trip(uuid: "3", cityName: "London", landmarks: [])); - // trips.add(Trip(uuid: "4", cityName: "Madrid", landmarks: [])); - // trips.add(Trip(uuid: "5", cityName: "Tokyo", landmarks: [])); - // trips.add(Trip(uuid: "6", cityName: "New York", landmarks: [])); - // trips.add(Trip(uuid: "7", cityName: "Los Angeles", landmarks: [])); - // trips.add(Trip(uuid: "8", cityName: "Zurich", landmarks: [])); - // trips.add(Trip(uuid: "9", cityName: "Orschwiller", landmarks: [])); + t1.landmarks.add( + Landmark( + uuid: "2", + name: "Notre Dame Cathedral", + location: [48.8530, 2.3498], + type: LandmarkType(name: "Monument"), + imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/Notre-Dame_de_Paris%2C_4_October_2017.jpg/440px-Notre-Dame_de_Paris%2C_4_October_2017.jpg" + ), + ); + t1.landmarks.add( + Landmark( + uuid: "3", + name: "Louvre palace", + location: [48.8606, 2.3376], + type: LandmarkType(name: "Museum"), + imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/66/Louvre_Museum_Wikimedia_Commons.jpg/540px-Louvre_Museum_Wikimedia_Commons.jpg" + ), + ); + t1.landmarks.add( + Landmark( + uuid: "4", + name: "Pont-des-arts", + location: [48.8585, 2.3376], + type: LandmarkType(name: "Bridge"), + imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d1/Pont_des_Arts%2C_6e_Arrondissement%2C_Paris_%28HDR%29_20140320_1.jpg/560px-Pont_des_Arts%2C_6e_Arrondissement%2C_Paris_%28HDR%29_20140320_1.jpg" + ), + ); + t1.landmarks.add( + Landmark( + uuid: "5", + name: "Panthéon", + location: [48.847, 2.347], + type: LandmarkType(name: "Monument"), + imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/8/80/Pantheon_of_Paris_007.JPG/1280px-Pantheon_of_Paris_007.JPG" + ), + ); + trips.add(t1); + + + Trip t2 = Trip(uuid: '2', cityName: 'Vienna', landmarks: LinkedList()); + + t2.landmarks.add( + Landmark( + uuid: '21', + name: "St. Charles's Church", + location: [48.1924563,16.3334399], + type: LandmarkType(name: "Monument"), + imageURL: "https://lh5.googleusercontent.com/p/AF1QipNNmA76Ps71NCL9rOOFoyheCEOyXWdHcUgQx9jd=w408-h305-k-no" + ), + ); + t2.landmarks.add( + Landmark( + uuid: "22", + name: "Vienna State Opera", + location: [48.1949124,16.3483292], + type: LandmarkType(name: "Culture"), + imageURL: "https://lh5.googleusercontent.com/p/AF1QipMOx398kcoeDXFruSHNsb4lmZtdT8vibtK0cLi-=w408-h306-k-no" + ), + ); + t2.landmarks.add( + Landmark( + uuid: "23", + name: "Belvedere-Schlossgarten", + location: [48.1956427,16.3711521], + type: LandmarkType(name: "Nature"), + imageURL: "https://lh5.googleusercontent.com/p/AF1QipNcI5LImH2Qdzx0GmF-5CY1wRKINFZ7HkahPEy1=w408-h306-k-no" + ), + ); + t2.landmarks.add( + Landmark( + uuid: "24", + name: "Kunsthistorisches Museum Wien", + location: [48.2047501,16.3581904], + type: LandmarkType(name: "Museum"), + imageURL: "https://lh5.googleusercontent.com/p/AF1QipPuDu-kCCowO4TcawjziE8AhDVAANagVtRYBjlv=w408-h450-k-no" + ), + ); + t2.landmarks.add( + Landmark( + uuid: "25", + name: "Salztorbrücke", + location: [48.2132382,16.369051], + type: LandmarkType(name: "Bridge"), + ), + ); + trips.add(t2); } - return trips; }