diff --git a/covas_mobile/android/app/src/main/AndroidManifest.xml b/covas_mobile/android/app/src/main/AndroidManifest.xml index 289462e..71290e3 100644 --- a/covas_mobile/android/app/src/main/AndroidManifest.xml +++ b/covas_mobile/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,7 @@ + + + diff --git a/covas_mobile/android/app/src/profile/AndroidManifest.xml b/covas_mobile/android/app/src/profile/AndroidManifest.xml index 6e10330..323e1c6 100644 --- a/covas_mobile/android/app/src/profile/AndroidManifest.xml +++ b/covas_mobile/android/app/src/profile/AndroidManifest.xml @@ -5,4 +5,16 @@ to allow setting breakpoints, to provide hot reload, etc. --> + + + + + + + + diff --git a/covas_mobile/ios/Runner/Info.plist b/covas_mobile/ios/Runner/Info.plist index cf192c9..436567c 100644 --- a/covas_mobile/ios/Runner/Info.plist +++ b/covas_mobile/ios/Runner/Info.plist @@ -45,5 +45,7 @@ CADisableMinimumFrameDurationOnPhone + NSLocationWhenInUseUsageDescription + Your location is needed for showing nearby events diff --git a/covas_mobile/lib/main.dart b/covas_mobile/lib/main.dart index 1db7c0d..be4b083 100644 --- a/covas_mobile/lib/main.dart +++ b/covas_mobile/lib/main.dart @@ -11,6 +11,7 @@ import 'pages/ListItemMenu.dart'; import 'classes/alert.dart'; import 'variable/globals.dart' as globals; +import 'package:permission_handler/permission_handler.dart'; void main() { runApp(MyApp()); @@ -153,10 +154,49 @@ class _LoginDemoState extends State with ShowErrorDialog { @override void initState() { + _checkLocationPermission(); start(); super.initState(); } + Future _checkLocationPermission() async { + PermissionStatus status = await Permission.location.status; + + if (status.isGranted) { + print("Location permission granted"); + } else if (status.isDenied) { + print("Location permission denied"); + _requestLocationPermission(); + } else if (status.isPermanentlyDenied) { + print("Location permission permanently denied"); + openAppSettings(); + } + } + + // Request location permission + Future _requestLocationPermission() async { + PermissionStatus status = await Permission.location.request(); + + if (status.isGranted) { + print("Location permission granted"); + } else if (status.isDenied) { + print("Location permission denied"); + } else if (status.isPermanentlyDenied) { + print("Location permission permanently denied"); + openAppSettings(); + } + } + + // Open app settings to allow user to grant permission manually + Future _openAppSettings() async { + bool opened = await openAppSettings(); + if (opened) { + print("App settings opened"); + } else { + print("Failed to open app settings"); + } + } + @override Widget build(BuildContext context) { return Scaffold( diff --git a/covas_mobile/lib/pages/DisplayPictureScreen.dart b/covas_mobile/lib/pages/DisplayPictureScreen.dart index 0c6d18a..32413a6 100644 --- a/covas_mobile/lib/pages/DisplayPictureScreen.dart +++ b/covas_mobile/lib/pages/DisplayPictureScreen.dart @@ -85,37 +85,43 @@ class DisplayPictureScreenState extends State } Future searchEvents(String json, String imagePath) async { - print(json); + print(json.replaceAll("'''json", '').replaceAll("'''", "")); SharedPreferences prefs = await SharedPreferences.getInstance(); + try { + Map jsonData = + jsonDecode(json.replaceAll("```json", '').replaceAll("```", "")); + print("json : ${jsonData}"); + var name = jsonData["name"]; + print("name : ${name}"); + var place = jsonData["place"]; + var accessToken = prefs.getString("access_token") ?? ""; - Map jsonData = jsonDecode(json); - var name = jsonData["name"]; - var place = jsonData["place"]; - var accessToken = prefs.getString("access_token") ?? ""; + if (accessToken.isNotEmpty) { + var urlGet = Uri.parse("${globals.api}/events?name=${name}"); - if (accessToken.isNotEmpty) { - var urlGet = Uri.parse("${globals.api}/events?name=${name}"); - - var responseGet = await http.get(urlGet, - headers: {HttpHeaders.cookieHeader: 'access_token=${accessToken}'}); - if (responseGet.statusCode == 200) { - var events = jsonDecode(utf8.decode(responseGet.bodyBytes)); - print("reponse http : ${events.length}"); - if (events.length == 0) { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => UpdateeventImage( - events: jsonData, imagePath: imagePath))); - } else { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => ItemMenu(title: events[0]["id"]))); + var responseGet = await http.get(urlGet, + headers: {HttpHeaders.cookieHeader: 'access_token=${accessToken}'}); + if (responseGet.statusCode == 200) { + var events = jsonDecode(utf8.decode(responseGet.bodyBytes)); + print("reponse http : ${events.length}"); + if (events.length == 0) { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => UpdateeventImage( + events: jsonData, imagePath: imagePath))); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ItemMenu(title: events[0]["id"]))); + } } + } else { + showErrorDialog(context, "Erreur de token"); } - } else { - showErrorDialog(context, "Erreur de token"); + } catch (e) { + showErrorDialog(context, "Erreur de format de donnée fourni par l'IA"); } //showDescImageAddDialog(context, message); @@ -132,7 +138,7 @@ class DisplayPictureScreenState extends State gemini .textAndImage( text: - "Peux-tu donner le nom, la date avec l'année actuelle ou d'une année future proche et le lieu de l'évènement sous format JSON avec les valeurs suivantes : name, address, city, zip_code, country, description, tags (tableau sans espace), organizers (tableau), start_date et end_date sous le format en YYYY-MM-DD HH:mm:ssZ, et sans la présence du mot json dans la chaîne de caractère", + "Peux-tu donner le nom, la date avec l'année actuelle ou d'une année future proche et le lieu de l'évènement sous format JSON (sans le caratère json au début de la chaine de caractère) avec les valeurs suivantes : name, place, description, tags (tableau sans espace), organizers (tableau), start_date et end_date (si le end_date est vide, alors donnez une valeur de six de plus par rapport à start_date) sous le format en YYYY-MM-DD HH:mm:ssZ", images: [file.readAsBytesSync()], modelName: "models/gemini-1.5-pro-latest") .then((value) => searchEvents( diff --git a/covas_mobile/lib/pages/ListItemMenu.dart b/covas_mobile/lib/pages/ListItemMenu.dart index 2f31a10..c280419 100644 --- a/covas_mobile/lib/pages/ListItemMenu.dart +++ b/covas_mobile/lib/pages/ListItemMenu.dart @@ -1,21 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:flutter_dotenv/flutter_dotenv.dart'; // Import dotenv import 'dart:convert'; import 'dart:io'; -import "ItemMenu.dart"; -import "Camera.dart"; - -import 'package:http/http.dart' as http; -import 'package:flutter/material.dart'; +import 'ItemMenu.dart'; +import 'SearchDelegate.dart'; import '../classes/events.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:intl/intl.dart'; import 'package:intl/date_symbol_data_local.dart'; +import 'dart:math'; +import 'package:geolocator/geolocator.dart'; +import '../variable/globals.dart' as globals; +import 'package:permission_handler/permission_handler.dart'; +import "Camera.dart"; import 'package:camera/camera.dart'; -import '../variable/globals.dart' as globals; - -// app starting point void main() { - initializeDateFormatting("fr_FR", null).then((_) => (const MyApp())); + initializeDateFormatting("fr_FR", null).then((_) => runApp(const MyApp())); } class MyApp extends StatelessWidget { @@ -30,7 +32,6 @@ class MyApp extends StatelessWidget { } } -// homepage class class ListItemMenu extends StatefulWidget { const ListItemMenu({super.key}); @@ -38,13 +39,62 @@ class ListItemMenu extends StatefulWidget { State createState() => _MyHomePageState(); } -// homepage state class _MyHomePageState extends State { - // variable to call and store future list of posts Future> postsFuture = getPosts(); + List filteredPosts = []; + String geographicalZone = ''; + String query = ''; + List> suggestions = []; + TextEditingController inputGeo = TextEditingController(); - // function to fetch data from api and return future list of posts + // Fetching events from API static Future> getPosts() async { + PermissionStatus status = await Permission.location.status; + var url = Uri.parse("${globals.api}/events"); + if (status.isGranted) { + print("Location permission granted"); + + // Get the current position with high accuracy + LocationSettings locationSettings = LocationSettings( + accuracy: LocationAccuracy.high, + distanceFilter: + 10, // Optional: Minimum distance (in meters) to trigger location update + ); + + Position position = await Geolocator.getCurrentPosition( + locationSettings: locationSettings, + ); + // Calculate the boundaries + double radiusInKm = 50; + double latDistance = radiusInKm / 111.0; + double lonDistance = + radiusInKm / (111.0 * cos(position.latitude * pi / 180)); + + double minLat = position.latitude - latDistance; + double maxLat = position.latitude + latDistance; + double minLon = position.longitude - lonDistance; + double maxLon = position.longitude + lonDistance; + + url = Uri.parse("${globals.api}/events/search" + "?min_lat=$minLat&max_lat=$maxLat" + "&min_lon=$minLon&max_lon=$maxLon"); + } + SharedPreferences prefs = await SharedPreferences.getInstance(); + var accessToken = prefs.getString("access_token") ?? ""; + final List body = []; + if (accessToken.isNotEmpty) { + final response = await http.get(url, headers: { + "Content-Type": "application/json", + HttpHeaders.cookieHeader: "access_token=${accessToken}" + }); + final List body = json.decode(utf8.decode(response.bodyBytes)); + return body.map((e) => Events.fromJson(e)).toList(); + } + return body; + } + + // Fetching events from API + Future> getAllPosts() async { SharedPreferences prefs = await SharedPreferences.getInstance(); var accessToken = prefs.getString("access_token") ?? ""; final List body = []; @@ -60,76 +110,344 @@ class _MyHomePageState extends State { return body; } + @override + void initState() { + super.initState(); + // Initialize data fetch when the page loads + _getCurrentLocation(); + } + + // Get the device's current location + Future _getCurrentLocation() async { + PermissionStatus status = await Permission.location.status; + + if (status.isGranted) { + print("Location permission granted"); + + // Get the current position with high accuracy + LocationSettings locationSettings = LocationSettings( + accuracy: LocationAccuracy.high, + distanceFilter: + 10, // Optional: Minimum distance (in meters) to trigger location update + ); + + Position position = await Geolocator.getCurrentPosition( + locationSettings: locationSettings, + ); + + // Reverse geocode: Get city and country from latitude and longitude using Mapbox Search API + final place = + await _getCityAndCountry(position.latitude, position.longitude); + } + } + + // Method to get city and country from latitude and longitude using Mapbox API + Future _getCityAndCountry(double latitude, double longitude) async { + await dotenv.load(fileName: ".env"); // Load .env file + + final mapboxAccessToken = dotenv.env['MAPBOX_ACCESS_TOKEN'] ?? ''; + final url = Uri.parse( + 'https://api.mapbox.com/geocoding/v5/mapbox.places/$longitude,$latitude.json?access_token=$mapboxAccessToken', + ); + + try { + // Send GET request to Mapbox API + final response = await http.get(url); + + // If the request is successful (HTTP status 200) + print("status mapbox : ${response.statusCode}"); + if (response.statusCode == 200) { + // Parse the response body + final data = json.decode(response.body); + + // Extract the city and country from the response + final features = data['features']; + + if (features.isNotEmpty) { + String city = _getCityFromFeatures(features); + String country = _getCountryFromFeatures(features); + print("city : ${city} ${country}"); + if (city.isNotEmpty && country.isNotEmpty) { + fetchPostsByLocation(latitude, longitude); + setState(() { + inputGeo.text = "${city}, ${country}"; + }); + } else { + _fetchInitialData(); + } + } else { + _fetchInitialData(); + } + } else { + _fetchInitialData(); + throw Exception('Failed to load location data'); + } + } catch (e) { + _fetchInitialData(); + print("Error getting city and country: $e"); + } + } + + // Helper function to extract the city from the Mapbox features array + String _getCityFromFeatures(List features) { + for (var feature in features) { + if (feature['place_type'] != null && + feature['place_type'].contains('place')) { + return feature['text'] ?? ''; + } + } + return ''; + } + + // Helper function to extract the country from the Mapbox features array + String _getCountryFromFeatures(List features) { + for (var feature in features) { + if (feature['place_type'] != null && + feature['place_type'].contains('country')) { + return feature['text'] ?? ''; + } + } + return ''; + } + + // Fetch initial data from API or any other necessary initialization + Future _fetchInitialData() async { + try { + // Optionally, you can fetch posts initially if needed. + List initialPosts = await getAllPosts(); + setState(() { + // Assign to the postsFuture and update the filtered posts if needed + filteredPosts = initialPosts; + }); + } catch (e) { + print('Error fetching initial data: $e'); + } + } + + Future searchSuggestions(String input) async { + await dotenv.load(fileName: ".env"); // Load .env file + + final mapboxAccessToken = dotenv.env['MAPBOX_ACCESS_TOKEN'] ?? ''; + final url = + 'https://api.mapbox.com/geocoding/v5/mapbox.places/${input}.json?access_token=${mapboxAccessToken}&proximity=ip'; + final response = await http.get(Uri.parse(url)); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + setState(() { + suggestions = (data['features'] as List) + .map((feature) => { + 'place_name': feature['place_name'], + 'geometry': feature[ + 'geometry'], // Include geometry for latitude/longitude + }) + .toList(); + }); + } else { + throw Exception('Failed to load suggestions'); + } + } + + Future fetchPostsByLocation(double latitude, double longitude) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + var accessToken = prefs.getString("access_token") ?? ""; + + if (accessToken.isNotEmpty) { + // Calculate the boundaries + double radiusInKm = 50; + double latDistance = radiusInKm / 111.0; + double lonDistance = radiusInKm / (111.0 * cos(latitude * pi / 180)); + + double minLat = latitude - latDistance; + double maxLat = latitude + latDistance; + double minLon = longitude - lonDistance; + double maxLon = longitude + lonDistance; + + var url = Uri.parse("${globals.api}/events/search" + "?min_lat=$minLat&max_lat=$maxLat" + "&min_lon=$minLon&max_lon=$maxLon"); + + final response = await http.get(url, headers: { + "Content-Type": "application/json", + HttpHeaders.cookieHeader: "access_token=$accessToken" + }); + print("status code : ${response.statusCode}"); + if (response.statusCode == 200) { + final List body = json.decode(utf8.decode(response.bodyBytes)); + print("results fetch : ${body}"); + // Update state after getting the response + setState(() { + if (body.isNotEmpty) { + // If we have results, map them to Events + filteredPosts = body + .map((e) => Events.fromJson(e as Map)) + .toList(); + } else { + // If no results, clear filteredPosts + filteredPosts.clear(); + } + }); + } else { + throw Exception('Failed to load posts'); + } + } + } + + Padding _buildGeographicalZoneSearchField() { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + TextField( + controller: inputGeo, + decoration: InputDecoration( + labelText: 'Search by geographical zone', + border: OutlineInputBorder(), + suffixIcon: IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + setState(() { + inputGeo.clear(); // Clear the text field + geographicalZone = ''; // Reset the geographical zone state + suggestions.clear(); // Optionally clear suggestions + _fetchInitialData(); // Clear the filtered posts + }); + }, + ), + ), + onChanged: (value) { + setState(() { + geographicalZone = value; + searchSuggestions(value); + }); + }, + ), + if (suggestions.isNotEmpty) + Container( + height: 200, + decoration: BoxDecoration( + border: Border.all(color: Colors.blue), + borderRadius: BorderRadius.circular(8), + ), + child: ListView.builder( + shrinkWrap: true, + itemCount: suggestions.length, + itemBuilder: (context, index) { + return ListTile( + title: Text(suggestions[index]['place_name']), + onTap: () async { + final latitude = + suggestions[index]['geometry']['coordinates'][1]; + final longitude = + suggestions[index]['geometry']['coordinates'][0]; + + setState(() { + geographicalZone = suggestions[index]['place_name']; + inputGeo.text = geographicalZone; + suggestions.clear(); + }); + + await fetchPostsByLocation(latitude, longitude); + }, + ); + }, + ), + ), + ], + ), + ); + } + Future popCamera() async { await availableCameras().then((value) => Navigator.push(context, MaterialPageRoute(builder: (_) => Camera(camera: value.first)))); } - // build function @override Widget build(BuildContext context) { - return Scaffold( - body: Center( - // FutureBuilder - child: FutureBuilder>( - future: postsFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - // until data is fetched, show loader - return const CircularProgressIndicator(); - } else if (snapshot.hasData) { - // once data is fetched, display it on screen (call buildPosts()) - final posts = snapshot.data!; - return buildPosts(posts); - } else { - // if no data, show simple Text - return const Text("No data available"); - } - }, - ), - ), - ); - } - - // function to display fetched data on screen - Widget buildPosts(List posts) { - // ListView Builder to show data in a list return Scaffold( appBar: AppBar( - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text("Item list menu"), + title: const Text("Item list menu"), backgroundColor: Colors.blue, foregroundColor: Colors.white, + actions: [ + IconButton( + icon: const Icon(Icons.search), + onPressed: () { + showSearch( + context: context, + delegate: SearchDelegateExample(geoQuery: inputGeo.text), + ); + }, + ), + ], ), - body: ListView.separated( - itemCount: posts.length, - itemBuilder: (context, index) { - final post = posts[index]; - final startDate = DateTime.parse(post.startDate!); - final date = DateFormat.yMd().format(startDate); - final time = DateFormat.Hm().format(startDate); - - return ListTile( - title: Text('${post.name!}'), - subtitle: Text('${post.place!}\n${date} ${time}'), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => ItemMenu(title: post.id!))); - }); - }, - separatorBuilder: (context, index) { - return Divider(); - }, + body: Column( + children: [ + _buildGeographicalZoneSearchField(), + Expanded( + child: FutureBuilder>( + future: postsFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasData) { + final posts = snapshot.data!; + final displayedPosts = + filteredPosts.isEmpty ? posts : filteredPosts; + return buildPosts(displayedPosts); + } else { + return const Text("No data available"); + } + }, + ), + ), + ], ), floatingActionButton: FloatingActionButton( onPressed: popCamera, backgroundColor: Colors.blue, tooltip: 'Recherche', - child: const Icon(Icons.search, color: Colors.white), + child: const Icon(Icons.photo_camera, color: Colors.white), ), ); } + + // Function to display fetched data on screen + Widget buildPosts(List posts) { + print("posts : ${posts}"); + print("filteredposts : ${filteredPosts}"); + final displayedPosts = filteredPosts; + print("results ${displayedPosts}"); + // If filteredPosts is empty, show a message saying no data is available + if (displayedPosts.isEmpty) { + return const Center( + child: Text('No events available for this location.', + style: TextStyle(fontSize: 18, color: Colors.grey)), + ); + } + + return ListView.separated( + itemCount: displayedPosts.length, + itemBuilder: (context, index) { + final post = displayedPosts[index]; + final startDate = DateTime.parse(post.startDate!); + final date = DateFormat.yMd().format(startDate); + final time = DateFormat.Hm().format(startDate); + + return ListTile( + title: Text('${post.name!}'), + subtitle: Text('${post.place!}\n${date} ${time}'), + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => ItemMenu(title: post.id!)), + ); + }, + ); + }, + separatorBuilder: (context, index) { + return Divider(); + }); + } } diff --git a/covas_mobile/lib/pages/SearchDelegate.dart b/covas_mobile/lib/pages/SearchDelegate.dart new file mode 100644 index 0000000..6c870ea --- /dev/null +++ b/covas_mobile/lib/pages/SearchDelegate.dart @@ -0,0 +1,129 @@ +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import 'dart:io'; +import 'ItemMenu.dart'; +import '../classes/events.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; // Import dotenv +import 'dart:math'; + +import '../variable/globals.dart' as globals; + +class SearchDelegateExample extends SearchDelegate { + final String geoQuery; + + SearchDelegateExample({ + required this.geoQuery, + }); + + @override + List buildActions(BuildContext context) { + return [ + IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + query = ''; + }, + ), + ]; + } + + @override + Widget buildLeading(BuildContext context) { + return IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + close(context, null); + }, + ); + } + + @override + Widget buildResults(BuildContext context) { + // Perform the search and return the results + return FutureBuilder>( + future: searchPosts(query, geoQuery), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasData) { + final posts = snapshot.data!; + return ListView.builder( + itemCount: posts.length, + itemBuilder: (context, index) { + final post = posts[index]; + return ListTile( + title: Text(post.name!), + subtitle: Text(post.place!), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ItemMenu(title: post.id!), + ), + ); + }, + ); + }, + ); + } else { + return const Center(child: Text("No results found")); + } + }, + ); + } + + @override + Widget buildSuggestions(BuildContext context) { + return Container(); // Implement suggestions if needed + } + + Future> searchPosts(String query, String geoQuery) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + var accessToken = prefs.getString("access_token") ?? ""; + final List body = []; + if (accessToken.isNotEmpty) { + var url = Uri.parse("${globals.api}/events/search?item=$query"); + if (geoQuery.isNotEmpty) { + await dotenv.load( + fileName: ".env"); // Load your .env for the Mapbox access token + final mapboxAccessToken = dotenv.env['MAPBOX_ACCESS_TOKEN'] ?? ''; + final geocodeUrl = Uri.parse( + 'https://api.mapbox.com/geocoding/v5/mapbox.places/$geoQuery.json?access_token=$mapboxAccessToken'); + final geocodeResponse = await http.get(geocodeUrl); + if (geocodeResponse.statusCode == 200) { + final geocodeData = json.decode(geocodeResponse.body); + if (geocodeData['features'].isNotEmpty) { + final coordinates = + geocodeData['features'][0]['geometry']['coordinates']; + final longitude = coordinates[0]; // Longitude + final latitude = coordinates[1]; // Latitude + + // Now use the latitude and longitude to get events within a 50km radius + double radiusInKm = 50; + double latDistance = radiusInKm / 111.0; + double lonDistance = + radiusInKm / (111.0 * cos(latitude * pi / 180)); + + double minLat = latitude - latDistance; + double maxLat = latitude + latDistance; + double minLon = longitude - lonDistance; + double maxLon = longitude + lonDistance; + + // Construct the search URL with the item query and latitude/longitude bounds + url = Uri.parse( + "${globals.api}/events/search?item=$query&min_lat=$minLat&max_lat=$maxLat&min_lon=$minLon&max_lon=$maxLon"); + } + } + } + final response = await http.get(url, headers: { + "Content-Type": "application/json", + HttpHeaders.cookieHeader: "access_token=${accessToken}" + }); + final List body = json.decode(utf8.decode(response.bodyBytes)); + return body.map((e) => Events.fromJson(e)).toList(); + } + return body; + } +} diff --git a/covas_mobile/lib/pages/UpdateEventImage.dart b/covas_mobile/lib/pages/UpdateEventImage.dart index 650aa4c..cd32624 100644 --- a/covas_mobile/lib/pages/UpdateEventImage.dart +++ b/covas_mobile/lib/pages/UpdateEventImage.dart @@ -44,20 +44,21 @@ class UpdateeventImage extends StatefulWidget { class _UpdateeventImageState extends State with ShowErrorDialog, ShowEventDialog { TextEditingController inputName = TextEditingController(); - TextEditingController inputAddress = TextEditingController(); - TextEditingController inputZipCode = TextEditingController(); - - TextEditingController inputCity = TextEditingController(); - TextEditingController inputCountry = TextEditingController(); TextEditingController inputDate = TextEditingController(); TextEditingController inputDesc = TextEditingController(); + TextEditingController inputGeo = TextEditingController(); + TextEditingController startDatepicker = TextEditingController(); TextEditingController startTimepicker = TextEditingController(); TextEditingController endDatepicker = TextEditingController(); TextEditingController endTimepicker = TextEditingController(); final _stringTagController = StringTagController(); + + List> suggestions = []; + String geographicalZone = ""; + List initialTags = []; final _stringOrgaController = StringTagController(); @@ -69,10 +70,16 @@ class _UpdateeventImageState extends State if (position == "end") { date = "end_date"; } + DateTime dateEvent; + if (widget.events[date].toString().isEmpty) { + dateEvent = DateTime.now(); + } else { + dateEvent = DateTime.parse(widget.events[date]); + } DateTime? pickedDate = await showDatePicker( context: context, - firstDate: DateTime.parse(widget.events[date]), - initialDate: DateTime.parse(widget.events[date]), + firstDate: dateEvent, + initialDate: dateEvent, lastDate: DateTime(2104)); if (pickedDate == null) return; if (position == "start") { @@ -89,10 +96,14 @@ class _UpdateeventImageState extends State if (position == "end") { date = "end_date"; } - TimeOfDay? pickedDate = await showTimePicker( - context: context, - initialTime: - TimeOfDay.fromDateTime(DateTime.parse(widget.events[date]))); + TimeOfDay timeEvent; + if (widget.events[date].toString().isEmpty) { + timeEvent = TimeOfDay.now(); + } else { + timeEvent = TimeOfDay.fromDateTime(DateTime.parse(widget.events[date])); + } + TimeOfDay? pickedDate = + await showTimePicker(context: context, initialTime: timeEvent); if (pickedDate == null) return; if (position == "start") { startTimepicker.text = pickedDate.format(context); @@ -128,10 +139,7 @@ class _UpdateeventImageState extends State Future _updateEvent(BuildContext context) async { var url = Uri.parse("${globals.api}/token"); var name = inputName.text; - var place = inputAddress.text; - var city = inputCity.text; - var country = inputCountry.text; - var zipCode = inputZipCode.text; + var place = inputGeo.text; var description = inputDesc.text; List tags = List.from(_stringTagController.getTags as List); List organizers = @@ -150,101 +158,118 @@ class _UpdateeventImageState extends State if (accessToken.isNotEmpty) { try { await dotenv.load(); - final params = { - 'expiration': '15552000', - 'key': dotenv.env["IMGBB_API_KEY"], - }; - print("Post Img"); - final urlPost = Uri.parse('https://api.imgbb.com/1/upload') - .replace(queryParameters: params); - File image = File(widget.imagePath); - Uint8List _bytes = await image.readAsBytes(); - String _base64String = base64.encode(_bytes); + final mapboxAccessToken = dotenv.env['MAPBOX_ACCESS_TOKEN'] ?? ''; + final url = + 'https://api.mapbox.com/geocoding/v5/mapbox.places/${place}.json?access_token=${mapboxAccessToken}&proximity=ip'; + final response = await http.get(Uri.parse(url)); - final req = http.MultipartRequest('POST', urlPost) - ..fields['image'] = _base64String; + if (response.statusCode == 200) { + final data = json.decode(response.body); - final stream = await req.send(); - final res = await http.Response.fromStream(stream); + if (data['features'].isNotEmpty) { + final coordinates = data['features'][0]['geometry']['coordinates']; + final longitude = coordinates[0]; // Longitude + final latitude = coordinates[1]; // Latitude - final status = res.statusCode; - print("code status imgbb ${status}"); - if (status == 200) { - var body = json.decode(utf8.decode(res.bodyBytes)); - String imgUrl = body["data"]["url"]; + final params = { + 'expiration': '15552000', + 'key': dotenv.env["IMGBB_API_KEY"], + }; + print("Post Img"); + final urlPost = Uri.parse('https://api.imgbb.com/1/upload') + .replace(queryParameters: params); + File image = File(widget.imagePath); + Uint8List _bytes = await image.readAsBytes(); + String _base64String = base64.encode(_bytes); - //String credentials = "${pseudo}:${password}"; - //Codec stringToBase64 = utf8.fuse(base64); - //String encoded = stringToBase64.encode(credentials); - var urlPut = Uri.parse("${globals.api}/events"); - var responsePut = await http.put(urlPut, - headers: { - HttpHeaders.cookieHeader: 'access_token=${accessToken}', - HttpHeaders.acceptHeader: 'application/json, text/plain, */*', - HttpHeaders.contentTypeHeader: 'application/json' - }, - body: jsonEncode({ - 'name': name, - 'place': place, - 'start_date': startDate, - 'end_date': endDate, - 'zip_code': zipCode, - 'country': country, - 'city': city, - 'organizers': organizers, - 'latitude': '0.0', - 'longitude': '0.0', - 'description': description, - "imgUrl": imgUrl, - "tags": tags - })); - print(responsePut.statusCode); - if ((responsePut.statusCode == 200) || - (responsePut.statusCode == 201)) { - showEventDialog(context, "Evenement ${name} ajoute"); - } else { - var text = ""; - switch (responsePut.statusCode) { - case 400: - { - text = "Requête mal construite"; + final req = http.MultipartRequest('POST', urlPost) + ..fields['image'] = _base64String; + + final stream = await req.send(); + final res = await http.Response.fromStream(stream); + + final status = res.statusCode; + print("code status imgbb ${status}"); + if (status == 200) { + var body = json.decode(utf8.decode(res.bodyBytes)); + String imgUrl = body["data"]["url"]; + + //String credentials = "${pseudo}:${password}"; + //Codec stringToBase64 = utf8.fuse(base64); + //String encoded = stringToBase64.encode(credentials); + var urlPut = Uri.parse("${globals.api}/events"); + var responsePut = await http.put(urlPut, + headers: { + HttpHeaders.cookieHeader: 'access_token=${accessToken}', + HttpHeaders.acceptHeader: + 'application/json, text/plain, */*', + HttpHeaders.contentTypeHeader: 'application/json' + }, + body: jsonEncode({ + 'name': name, + 'place': place, + 'start_date': startDate, + 'end_date': endDate, + 'organizers': organizers, + 'latitude': latitude, + 'longitude': longitude, + 'description': description, + "imgUrl": imgUrl, + "tags": tags + })); + print(responsePut.statusCode); + if ((responsePut.statusCode == 200) || + (responsePut.statusCode == 201)) { + showEventDialog(context, "Evenement ${name} ajoute"); + } else { + var text = ""; + switch (responsePut.statusCode) { + case 400: + { + text = "Requête mal construite"; + } + break; + case 406: + { + text = "Mot de passe incorrect"; + } + break; + case 404: + { + text = "Utilisateur inconnu"; + } + break; + case 403: + { + text = "Utilisateur desactive"; + } + break; + case 410: + { + text = "Token invalide"; + } + break; + case 500: + { + text = "Probleme interne du serveur"; + } + break; + default: + { + text = "Probleme d'authentification inconnu"; + } + break; } - break; - case 406: - { - text = "Mot de passe incorrect"; - } - break; - case 404: - { - text = "Utilisateur inconnu"; - } - break; - case 403: - { - text = "Utilisateur desactive"; - } - break; - case 410: - { - text = "Token invalide"; - } - break; - case 500: - { - text = "Probleme interne du serveur"; - } - break; - default: - { - text = "Probleme d'authentification inconnu"; - } - break; + showErrorDialog(context, text); + } + } else { + print("imgbb error : ${status}"); } - showErrorDialog(context, text); + } else { + showErrorDialog(context, "Aucune donnée geographique"); } } else { - print("imgbb error : ${status}"); + showErrorDialog(context, "Mapbox non accessible"); } } catch (e) { showErrorDialog(context, "${e}"); @@ -255,22 +280,22 @@ class _UpdateeventImageState extends State } void start() async { + print("events : ${widget.events}"); inputName.text = convertNulltoEmptyString(widget.events["name"]); - inputCity.text = convertNulltoEmptyString(widget.events["city"]); - inputAddress.text = convertNulltoEmptyString(widget.events["address"]); - inputZipCode.text = convertNulltoEmptyString(widget.events["zip_code"]); - inputCountry.text = convertNulltoEmptyString(widget.events["country"]); + inputGeo.text = convertNulltoEmptyString(widget.events["place"]); inputDesc.text = convertNulltoEmptyString(widget.events["description"]); - - DateTime pickedStartDate = - DateTime.parse(convertNulltoEmptyString(widget.events["start_date"])); - DateTime pickedEndDate = - DateTime.parse(convertNulltoEmptyString(widget.events["end_date"])); - - startDatepicker.text = DateFormat("dd-MM-yyyy").format(pickedStartDate); - endDatepicker.text = DateFormat("dd-MM-yyyy").format(pickedEndDate); - startTimepicker.text = DateFormat("HH-mm").format(pickedStartDate); - endTimepicker.text = DateFormat("HH-mm").format(pickedEndDate); + if (widget.events["start_date"].toString().isNotEmpty) { + DateTime pickedStartDate = + DateTime.parse(convertNulltoEmptyString(widget.events["start_date"])); + startDatepicker.text = DateFormat("dd-MM-yyyy").format(pickedStartDate); + startTimepicker.text = DateFormat("HH-mm").format(pickedStartDate); + } + if (widget.events["end_date"].toString().isNotEmpty) { + DateTime pickedEndDate = + DateTime.parse(convertNulltoEmptyString(widget.events["end_date"])); + endDatepicker.text = DateFormat("dd-MM-yyyy").format(pickedEndDate); + endTimepicker.text = DateFormat("HH-mm").format(pickedEndDate); + } initialTags = List.from(widget.events['tags'] as List); initialOrga = List.from(widget.events['organizers'] as List); } @@ -286,6 +311,93 @@ class _UpdateeventImageState extends State return value!.isEmpty ? 'Champ requis' : null; } + Future searchSuggestions(String input) async { + await dotenv.load(fileName: ".env"); // Load .env file + + final mapboxAccessToken = dotenv.env['MAPBOX_ACCESS_TOKEN'] ?? ''; + final url = + 'https://api.mapbox.com/geocoding/v5/mapbox.places/${input}.json?access_token=${mapboxAccessToken}&proximity=ip'; + final response = await http.get(Uri.parse(url)); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + setState(() { + suggestions = (data['features'] as List) + .map((feature) => { + 'place_name': feature['place_name'], + 'geometry': feature[ + 'geometry'], // Include geometry for latitude/longitude + }) + .toList(); + }); + } else { + throw Exception('Failed to load suggestions'); + } + } + + Padding _buildGeographicalZoneSearchField() { + return Padding( + padding: + const EdgeInsets.only(left: 15.0, right: 15.0, top: 15, bottom: 0), + child: Column( + children: [ + TextField( + controller: inputGeo, + decoration: InputDecoration( + labelText: 'Lieu', + border: OutlineInputBorder(), + suffixIcon: IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + setState(() { + inputGeo.clear(); // Clear the text field + geographicalZone = ''; // Reset the geographical zone state + suggestions.clear(); // Optionally clear suggestions + }); + }, + ), + ), + onChanged: (value) { + setState(() { + geographicalZone = value; + searchSuggestions(value); + }); + }, + ), + if (suggestions.isNotEmpty) + Container( + height: 200, + decoration: BoxDecoration( + border: Border.all(color: Colors.blue), + borderRadius: BorderRadius.circular(8), + ), + child: ListView.builder( + shrinkWrap: true, + itemCount: suggestions.length, + itemBuilder: (context, index) { + return ListTile( + title: Text(suggestions[index]['place_name']), + onTap: () async { + final latitude = + suggestions[index]['geometry']['coordinates'][1]; + final longitude = + suggestions[index]['geometry']['coordinates'][0]; + + setState(() { + geographicalZone = suggestions[index]['place_name']; + inputGeo.text = geographicalZone; + suggestions.clear(); + }); + }, + ); + }, + ), + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -323,58 +435,7 @@ class _UpdateeventImageState extends State hintText: 'Modifier le nom de l\'évènement'), ), ), - Padding( - padding: const EdgeInsets.only( - left: 15.0, right: 15.0, top: 15, bottom: 0), - //padding: EdgeInsets.symmetric(horizontal: 15), - child: TextFormField( - controller: inputAddress, - validator: (value) => _validateField(value), - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: 'Adresse', - hintText: 'Entrer une adresse'), - ), - ), - Padding( - padding: const EdgeInsets.only( - left: 15.0, right: 15.0, top: 15, bottom: 0), - //padding: EdgeInsets.symmetric(horizontal: 15), - child: TextFormField( - controller: inputZipCode, - validator: (value) => _validateField(value), - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: 'Code postal', - hintText: 'Entrer un code postal'), - ), - ), - Padding( - padding: const EdgeInsets.only( - left: 15.0, right: 15.0, top: 15, bottom: 0), - //padding: EdgeInsets.symmetric(horizontal: 15), - child: TextFormField( - controller: inputCity, - validator: (value) => _validateField(value), - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: 'Ville', - hintText: 'Entrer une ville'), - ), - ), - Padding( - padding: const EdgeInsets.only( - left: 15.0, right: 15.0, top: 15, bottom: 0), - //padding: EdgeInsets.symmetric(horizontal: 15), - child: TextFormField( - controller: inputCountry, - validator: (value) => _validateField(value), - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: 'Pays', - hintText: 'Entrer un pays'), - ), - ), + _buildGeographicalZoneSearchField(), Padding( padding: const EdgeInsets.only( left: 15.0, right: 15.0, top: 15, bottom: 0), diff --git a/covas_mobile/macos/Flutter/GeneratedPluginRegistrant.swift b/covas_mobile/macos/Flutter/GeneratedPluginRegistrant.swift index 4b4e1ac..53d8e6c 100644 --- a/covas_mobile/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/covas_mobile/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,11 +6,13 @@ import FlutterMacOS import Foundation import file_selector_macos +import geolocator_apple import path_provider_foundation import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/covas_mobile/pubspec.lock b/covas_mobile/pubspec.lock index 84f9170..9bad8b5 100644 --- a/covas_mobile/pubspec.lock +++ b/covas_mobile/pubspec.lock @@ -89,6 +89,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.4+2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" cupertino_icons: dependency: "direct main" description: @@ -177,6 +185,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.3+2" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -232,6 +248,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.4" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: "0ec58b731776bc43097fcf751f79681b6a8f6d3bc737c94779fe9f1ad73c1a81" + url: "https://pub.dev" + source: hosted + version: "13.0.1" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: "7aefc530db47d90d0580b552df3242440a10fe60814496a979aa67aa98b1fd47" + url: "https://pub.dev" + source: hosted + version: "4.6.1" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: bc2aca02423ad429cb0556121f56e60360a2b7d694c8570301d06ea0c00732fd + url: "https://pub.dev" + source: hosted + version: "2.3.7" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "386ce3d9cce47838355000070b1d0b13efb5bc430f8ecda7e9238c8409ace012" + url: "https://pub.dev" + source: hosted + version: "4.2.4" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: "2ed69328e05cd94e7eb48bb0535f5fc0c0c44d1c4fa1e9737267484d05c29b5e" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "53da08937d07c24b0d9952eb57a3b474e29aae2abf9dd717f7e1230995f13f0e" + url: "https://pub.dev" + source: hosted + version: "0.2.3" http: dependency: "direct main" description: @@ -448,6 +512,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" + url: "https://pub.dev" + source: hosted + version: "11.3.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1" + url: "https://pub.dev" + source: hosted + version: "12.0.13" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0 + url: "https://pub.dev" + source: hosted + version: "9.4.5" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851 + url: "https://pub.dev" + source: hosted + version: "0.1.3+2" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9 + url: "https://pub.dev" + source: hosted + version: "4.2.3" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" platform: dependency: transitive description: @@ -533,6 +645,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: @@ -597,6 +717,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" vector_math: dependency: transitive description: diff --git a/covas_mobile/pubspec.yaml b/covas_mobile/pubspec.yaml index 418204f..6bf13bd 100644 --- a/covas_mobile/pubspec.yaml +++ b/covas_mobile/pubspec.yaml @@ -46,6 +46,8 @@ dependencies: image_picker: ^1.1.2 date_format_field: ^0.1.0 textfield_tags: ^3.0.1 + geolocator: ^13.0.1 + permission_handler: ^11.3.1 dev_dependencies: flutter_test: diff --git a/covas_mobile/windows/flutter/generated_plugin_registrant.cc b/covas_mobile/windows/flutter/generated_plugin_registrant.cc index 77ab7a0..921279f 100644 --- a/covas_mobile/windows/flutter/generated_plugin_registrant.cc +++ b/covas_mobile/windows/flutter/generated_plugin_registrant.cc @@ -7,8 +7,14 @@ #include "generated_plugin_registrant.h" #include +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + GeolocatorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("GeolocatorWindows")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); } diff --git a/covas_mobile/windows/flutter/generated_plugins.cmake b/covas_mobile/windows/flutter/generated_plugins.cmake index a423a02..71dd257 100644 --- a/covas_mobile/windows/flutter/generated_plugins.cmake +++ b/covas_mobile/windows/flutter/generated_plugins.cmake @@ -4,6 +4,8 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows + geolocator_windows + permission_handler_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST