diff --git a/Mobile/lib/api.dart b/Mobile/lib/api.dart index 496e157..25b6bdc 100644 --- a/Mobile/lib/api.dart +++ b/Mobile/lib/api.dart @@ -57,7 +57,7 @@ Future request(BuildContext context, ApiService service, String method, return null; } - return response.body; + return utf8.decode(response.bodyBytes); } Future isLoggedIn(BuildContext context) async { diff --git a/Mobile/lib/favorites.dart b/Mobile/lib/favorites.dart index 42a2890..8ac10bf 100644 --- a/Mobile/lib/favorites.dart +++ b/Mobile/lib/favorites.dart @@ -63,7 +63,7 @@ class _FavoritesPage extends State { Widget build(BuildContext context) { return SideMenu( selectedIndex: 1, - body: Container( + body: SingleChildScrollView(child: Container( decoration: const BoxDecoration(color: Color(0xFFF9F9F9)), width: MediaQuery.of(context).size.width, padding: const EdgeInsets.all(20.0), @@ -106,6 +106,6 @@ class _FavoritesPage extends State { )).toList(), ), ), - ); + )); } } diff --git a/Mobile/lib/main.dart b/Mobile/lib/main.dart index f98509e..d3f8bd9 100644 --- a/Mobile/lib/main.dart +++ b/Mobile/lib/main.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; @@ -9,6 +10,7 @@ import 'base/sidemenu.dart'; import 'profile.dart'; import 'api.dart' as api; import 'models.dart'; +import 'package:http/http.dart' as http; void main() { runApp(const MyApp()); @@ -47,23 +49,144 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { final GlobalKey _scaffoldKey = GlobalKey(); List _favorites = []; + LatLng? _selectedPoint; + double _zoom = 7.0; + + void _onTap(TapPosition _, LatLng point) async { + setState(() => _selectedPoint = point); + + final dynamic location; + try { + final response = await http.get( + Uri.parse('https://nominatim.openstreetmap.org/reverse.php?lat=${point.latitude}&lon=${point.longitude}&zoom=${max(12, _zoom.ceil())}&format=jsonv2'), + headers: {'User-Agent': 'SkanTravels/1.0'}, + ); + + if (mounted && response.statusCode != 200) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Unable to fetch information about this location (HTTP ${response.statusCode})'))); + debugPrint(response.body); + return; + } + + location = jsonDecode(response.body); + } catch (_) { + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Unable to fetch information about this location'))); + + setState(() => _selectedPoint = null); + + return; + } + + if (!mounted) return; + + if (location['name'] != null && location['display_name'] != null) { + await _showLocation(point, location['name'], location['display_name']); + } + + setState(() => _selectedPoint = null); + } + + Future _showLocation(LatLng point, String name, String description) async { + await showModalBottomSheet( + barrierColor: Colors.black.withOpacity(0.3), + context: context, + builder: (builder) { + return StatefulBuilder(builder: (BuildContext context, StateSetter setModalState) { + return Wrap(children: [ + Container( + color: Colors.white, + padding: const EdgeInsets.all(20), + width: MediaQuery.of(context).size.width, + child: Row(children: [ + Expanded(child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(name, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 24)), + const SizedBox(height: 10), + Text(description), + ], + )), + Column(children: [ + IconButton( + icon: const Icon(Icons.star), + iconSize: 32, + color: _favorites.where((fav) => fav.lat == point.latitude && fav.lng == point.longitude).isEmpty ? Colors.grey : Colors.yellow, + onPressed: () => _toggleFavorite(point, name, description, setModalState, context) + ), + const IconButton(icon: Icon(Icons.rate_review), iconSize: 32, onPressed: null), + ]), + ]), + ), + ]); + }); + }, + ); + } + + void _toggleFavorite(LatLng point, String name, String description, StateSetter setModalState, BuildContext context) async { + final messenger = ScaffoldMessenger.of(context); + final navigator = Navigator.of(context); + final favorite = _favorites.where((fav) => fav.lat == point.latitude && fav.lng == point.longitude).firstOrNull; + + if (!await api.isLoggedIn(context)) { + messenger.showSnackBar(const SnackBar(content: Text('You need to login to do that'), behavior: SnackBarBehavior.floating)); + navigator.pop(); + return; + } + + if (!context.mounted) return; + + if (favorite == null) { + final newFavorite = await api.request( + context, api.ApiService.app, 'POST', '/favorites', + {'lat': point.latitude, 'lng': point.longitude, 'name': name, 'description': description}, + ); + + if (newFavorite == null) { + navigator.pop(); + return; + } + + setState(() { + _favorites.add(Favorite.fromJson(jsonDecode(newFavorite))); + }); + setModalState(() {}); + + return; + } + + if (await api.request(context, api.ApiService.app, 'DELETE', '/favorites/${favorite.id}', null) == null) { + navigator.pop(); + return; + } + + setState(() { + _favorites = _favorites.where((fav) => fav.id != favorite.id).toList(); + }); + setModalState(() {}); + } + + Future _fetchFavorites() async { + final response = await api.request(context, api.ApiService.app, 'GET', '/favorites', null); + if (response == null) return; + + final List favorites = jsonDecode(response); + setState(() { + _favorites = favorites.map((favorite) => Favorite.fromJson(favorite)).toList(); + }); + } @override void didChangeDependencies() { super.didChangeDependencies(); - api.isLoggedIn(context).then((isLoggedIn) async { + api.isLoggedIn(context).then((isLoggedIn) { if (!isLoggedIn || !mounted) return; - final response = await api.request(context, api.ApiService.app, 'GET', '/favorites', null); - if (response == null) return; - - final List favorites = jsonDecode(response); - setState(() { - _favorites = favorites.map((favorite) => Favorite(favorite['id'], favorite['user_id'], favorite['lat'], favorite['lng'], favorite['name'], favorite['description'])).toList(); - }); + _fetchFavorites(); }); - } @override @@ -74,23 +197,52 @@ class _MyHomePageState extends State { key: _scaffoldKey, //drawer: navigationMenu, body: FlutterMap( - options: const MapOptions(initialCenter: LatLng(55.9397, 9.5156), initialZoom: 7.0), + options: MapOptions( + initialCenter: const LatLng(55.9397, 9.5156), + initialZoom: _zoom, + onTap: _onTap, + onPositionChanged: (pos, _) => _zoom = pos.zoom, + ), children: [ openStreetMapTileLayer, + if (_selectedPoint != null) + MarkerLayer(markers: [ + Marker( + point: _selectedPoint!, + width: 30, + height: 50, + alignment: Alignment.center, + child: const Stack( + children: [ + Icon(Icons.location_pin, size: 30, color: Colors.red), + Icon(Icons.location_on_outlined, size: 30, color: Colors.black), + ] + ), + ) + ]), ..._favorites.map((favorite) => MarkerLayer(markers: [ Marker( point: LatLng(favorite.lat, favorite.lng), - width: 60, - height: 100, + width: 30, + height: 50, alignment: Alignment.center, - child: const Icon( - Icons.location_pin, - size: 60, - color: Colors.yellow, - ) + child: Stack( + children: [ + IconButton( + padding: const EdgeInsets.only(bottom: 10), + icon: const Icon(Icons.location_pin, size: 30, color: Colors.yellow), + onPressed: () => _showLocation(LatLng(favorite.lat, favorite.lng), favorite.name, favorite.description), + ), + IconButton( + padding: const EdgeInsets.only(bottom: 10), + icon: const Icon(Icons.location_on_outlined, size: 30, color: Colors.black), + onPressed: () => _showLocation(LatLng(favorite.lat, favorite.lng), favorite.name, favorite.description), + ), + ] + ), ) - ]) + ]), ), ], ), @@ -99,7 +251,7 @@ class _MyHomePageState extends State { } TileLayer get openStreetMapTileLayer => TileLayer( - urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - userAgentPackageName: 'dev.fleaflet.flutter_map.example', - ); + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: 'dev.fleaflet.flutter_map.example', + ); } diff --git a/Mobile/lib/models.dart b/Mobile/lib/models.dart index 665387c..78fb075 100644 --- a/Mobile/lib/models.dart +++ b/Mobile/lib/models.dart @@ -7,6 +7,17 @@ class Favorite { String description; Favorite(this.id, this.userId, this.lat, this.lng, this.name, this.description); + + factory Favorite.fromJson(Map json) { + return Favorite( + json['id'], + json['user_id'], + json['lat'], + json['lng'], + json['name'], + json['description'], + ); + } } class Login { diff --git a/rust-backend/src/main.rs b/rust-backend/src/main.rs index 815df0d..f059c3a 100644 --- a/rust-backend/src/main.rs +++ b/rust-backend/src/main.rs @@ -56,39 +56,33 @@ fn get_favorites(db: MutexGuard<'_, rusqlite::Connection>, user_id: String) -> O struct CreateFavoriteRequest { lat: f64, lng: f64, -} - -#[derive(Deserialize, Debug)] -struct ReverseLookupResponse { name: String, - display_name: String, + description: String, } #[post("/favorites")] async fn create_favorite(auth: AuthorizedUser, data: web::Data, input: web::Json) -> impl Responder { let db = data.database.lock().unwrap(); - let Ok(response) = reqwest::Client::new() - .get(format!("https://nominatim.openstreetmap.org/reverse.php?lat={}&lon={}&zoom=18&format=jsonv2", input.lat, input.lng)) - .header("User-Agent", "SkanTravels/1.0") - .send().await - else { return HttpResponse::InternalServerError(); }; - - let Ok(response) = response.json::().await - else { return HttpResponse::InternalServerError(); }; - match db.execute( "INSERT INTO favorites (user_id, lat, lng, name, description) VALUES (:user_id, :lat, :lng, :name, :description)", &[ (":user_id", &auth.user_id), (":lat", &input.lat.to_string()), (":lng", &input.lng.to_string()), - (":name", &response.name), - (":description", &response.display_name), + (":name", &input.name), + (":description", &input.description), ], ) { - Ok(_) => HttpResponse::Created(), - Err(_) => HttpResponse::InternalServerError(), + Ok(_) => HttpResponse::Created().json(Favorite { + id: db.last_insert_rowid(), + user_id: auth.user_id, + lat: input.lat, + lng: input.lng, + name: input.name.clone(), + description: input.description.clone(), + }), + Err(_) => HttpResponse::InternalServerError().finish(), } } diff --git a/rust-backend/src/models.rs b/rust-backend/src/models.rs index a9fa37d..4892d0b 100644 --- a/rust-backend/src/models.rs +++ b/rust-backend/src/models.rs @@ -4,7 +4,7 @@ use rusqlite::{Row, Error}; #[derive(Serialize)] pub struct Favorite { - pub id: usize, + pub id: i64, pub user_id: String, pub lat: f64, pub lng: f64,