Merge pull request 'feature/searchbar' (#11) from feature/searchbar into main

Reviewed-on: #11
This commit is contained in:
v4l3n71n 2024-11-08 17:55:12 +01:00
commit c1e85c255e
13 changed files with 971 additions and 260 deletions

View File

@ -1,5 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.covas_mobile"> package="com.example.covas_mobile">
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<application <application
android:label="covas_mobile" android:label="covas_mobile"
android:name="${applicationName}" android:name="${applicationName}"
@ -24,6 +26,7 @@
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->

View File

@ -5,4 +5,16 @@
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.
--> -->
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<application
android:enableOnBackInvokedCallback="true"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:theme="@style/Theme.YourApp">
<!-- Other configurations -->
</application>
</manifest> </manifest>

View File

@ -45,5 +45,7 @@
<false/> <false/>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Your location is needed for showing nearby events</string>
</dict> </dict>
</plist> </plist>

View File

@ -11,6 +11,7 @@ import 'pages/ListItemMenu.dart';
import 'classes/alert.dart'; import 'classes/alert.dart';
import 'variable/globals.dart' as globals; import 'variable/globals.dart' as globals;
import 'package:permission_handler/permission_handler.dart';
void main() { void main() {
runApp(MyApp()); runApp(MyApp());
@ -153,10 +154,49 @@ class _LoginDemoState extends State<LoginDemo> with ShowErrorDialog {
@override @override
void initState() { void initState() {
_checkLocationPermission();
start(); start();
super.initState(); super.initState();
} }
Future<void> _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<void> _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<void> _openAppSettings() async {
bool opened = await openAppSettings();
if (opened) {
print("App settings opened");
} else {
print("Failed to open app settings");
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(

View File

@ -85,11 +85,14 @@ class DisplayPictureScreenState extends State<DisplayPictureScreen>
} }
Future<void> searchEvents(String json, String imagePath) async { Future<void> searchEvents(String json, String imagePath) async {
print(json); print(json.replaceAll("'''json", '').replaceAll("'''", ""));
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
try {
Map<String, dynamic> jsonData = jsonDecode(json); Map<String, dynamic> jsonData =
jsonDecode(json.replaceAll("```json", '').replaceAll("```", ""));
print("json : ${jsonData}");
var name = jsonData["name"]; var name = jsonData["name"];
print("name : ${name}");
var place = jsonData["place"]; var place = jsonData["place"];
var accessToken = prefs.getString("access_token") ?? ""; var accessToken = prefs.getString("access_token") ?? "";
@ -117,6 +120,9 @@ class DisplayPictureScreenState extends State<DisplayPictureScreen>
} else { } else {
showErrorDialog(context, "Erreur de token"); showErrorDialog(context, "Erreur de token");
} }
} catch (e) {
showErrorDialog(context, "Erreur de format de donnée fourni par l'IA");
}
//showDescImageAddDialog(context, message); //showDescImageAddDialog(context, message);
} }
@ -132,7 +138,7 @@ class DisplayPictureScreenState extends State<DisplayPictureScreen>
gemini gemini
.textAndImage( .textAndImage(
text: 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()], images: [file.readAsBytesSync()],
modelName: "models/gemini-1.5-pro-latest") modelName: "models/gemini-1.5-pro-latest")
.then((value) => searchEvents( .then((value) => searchEvents(

View File

@ -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:convert';
import 'dart:io'; import 'dart:io';
import "ItemMenu.dart"; import 'ItemMenu.dart';
import "Camera.dart"; import 'SearchDelegate.dart';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
import '../classes/events.dart'; import '../classes/events.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:intl/date_symbol_data_local.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 'package:camera/camera.dart';
import '../variable/globals.dart' as globals;
// app starting point
void main() { void main() {
initializeDateFormatting("fr_FR", null).then((_) => (const MyApp())); initializeDateFormatting("fr_FR", null).then((_) => runApp(const MyApp()));
} }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
@ -30,7 +32,6 @@ class MyApp extends StatelessWidget {
} }
} }
// homepage class
class ListItemMenu extends StatefulWidget { class ListItemMenu extends StatefulWidget {
const ListItemMenu({super.key}); const ListItemMenu({super.key});
@ -38,13 +39,62 @@ class ListItemMenu extends StatefulWidget {
State<ListItemMenu> createState() => _MyHomePageState(); State<ListItemMenu> createState() => _MyHomePageState();
} }
// homepage state
class _MyHomePageState extends State<ListItemMenu> { class _MyHomePageState extends State<ListItemMenu> {
// variable to call and store future list of posts
Future<List<Events>> postsFuture = getPosts(); Future<List<Events>> postsFuture = getPosts();
List<Events> filteredPosts = [];
String geographicalZone = '';
String query = '';
List<Map<String, dynamic>> suggestions = [];
TextEditingController inputGeo = TextEditingController();
// function to fetch data from api and return future list of posts // Fetching events from API
static Future<List<Events>> getPosts() async { static Future<List<Events>> 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<Events> 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<List<Events>> getAllPosts() async {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
var accessToken = prefs.getString("access_token") ?? ""; var accessToken = prefs.getString("access_token") ?? "";
final List<Events> body = []; final List<Events> body = [];
@ -60,52 +110,327 @@ class _MyHomePageState extends State<ListItemMenu> {
return body; return body;
} }
@override
void initState() {
super.initState();
// Initialize data fetch when the page loads
_getCurrentLocation();
}
// Get the device's current location
Future<void> _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<void> _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<dynamic> 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<dynamic> 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<void> _fetchInitialData() async {
try {
// Optionally, you can fetch posts initially if needed.
List<Events> 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<void> 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<void> 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<dynamic> 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<String, dynamic>))
.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<void> popCamera() async { Future<void> popCamera() async {
await availableCameras().then((value) => Navigator.push(context, await availableCameras().then((value) => Navigator.push(context,
MaterialPageRoute(builder: (_) => Camera(camera: value.first)))); MaterialPageRoute(builder: (_) => Camera(camera: value.first))));
} }
// build function
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: Center( appBar: AppBar(
// FutureBuilder 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: Column(
children: [
_buildGeographicalZoneSearchField(),
Expanded(
child: FutureBuilder<List<Events>>( child: FutureBuilder<List<Events>>(
future: postsFuture, future: postsFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) {
// until data is fetched, show loader return const Center(child: CircularProgressIndicator());
return const CircularProgressIndicator();
} else if (snapshot.hasData) { } else if (snapshot.hasData) {
// once data is fetched, display it on screen (call buildPosts())
final posts = snapshot.data!; final posts = snapshot.data!;
return buildPosts(posts); final displayedPosts =
filteredPosts.isEmpty ? posts : filteredPosts;
return buildPosts(displayedPosts);
} else { } else {
// if no data, show simple Text
return const Text("No data available"); return const Text("No data available");
} }
}, },
), ),
), ),
],
),
floatingActionButton: FloatingActionButton(
onPressed: popCamera,
backgroundColor: Colors.blue,
tooltip: 'Recherche',
child: const Icon(Icons.photo_camera, color: Colors.white),
),
); );
} }
// function to display fetched data on screen // Function to display fetched data on screen
Widget buildPosts(List<Events> posts) { Widget buildPosts(List<Events> posts) {
// ListView Builder to show data in a list print("posts : ${posts}");
return Scaffold( print("filteredposts : ${filteredPosts}");
appBar: AppBar( final displayedPosts = filteredPosts;
// Here we take the value from the MyHomePage object that was created by print("results ${displayedPosts}");
// the App.build method, and use it to set our appbar title. // If filteredPosts is empty, show a message saying no data is available
title: Text("Item list menu"), if (displayedPosts.isEmpty) {
backgroundColor: Colors.blue, return const Center(
foregroundColor: Colors.white, child: Text('No events available for this location.',
), style: TextStyle(fontSize: 18, color: Colors.grey)),
body: ListView.separated( );
itemCount: posts.length, }
return ListView.separated(
itemCount: displayedPosts.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final post = posts[index]; final post = displayedPosts[index];
final startDate = DateTime.parse(post.startDate!); final startDate = DateTime.parse(post.startDate!);
final date = DateFormat.yMd().format(startDate); final date = DateFormat.yMd().format(startDate);
final time = DateFormat.Hm().format(startDate); final time = DateFormat.Hm().format(startDate);
@ -116,20 +441,13 @@ class _MyHomePageState extends State<ListItemMenu> {
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(builder: (_) => ItemMenu(title: post.id!)),
builder: (_) => ItemMenu(title: post.id!))); );
}); },
);
}, },
separatorBuilder: (context, index) { separatorBuilder: (context, index) {
return Divider(); return Divider();
}, });
),
floatingActionButton: FloatingActionButton(
onPressed: popCamera,
backgroundColor: Colors.blue,
tooltip: 'Recherche',
child: const Icon(Icons.search, color: Colors.white),
),
);
} }
} }

View File

@ -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<Widget> 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<List<Events>>(
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<List<Events>> searchPosts(String query, String geoQuery) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
var accessToken = prefs.getString("access_token") ?? "";
final List<Events> 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;
}
}

View File

@ -44,20 +44,21 @@ class UpdateeventImage extends StatefulWidget {
class _UpdateeventImageState extends State<UpdateeventImage> class _UpdateeventImageState extends State<UpdateeventImage>
with ShowErrorDialog, ShowEventDialog { with ShowErrorDialog, ShowEventDialog {
TextEditingController inputName = TextEditingController(); TextEditingController inputName = TextEditingController();
TextEditingController inputAddress = TextEditingController();
TextEditingController inputZipCode = TextEditingController();
TextEditingController inputCity = TextEditingController();
TextEditingController inputCountry = TextEditingController();
TextEditingController inputDate = TextEditingController(); TextEditingController inputDate = TextEditingController();
TextEditingController inputDesc = TextEditingController(); TextEditingController inputDesc = TextEditingController();
TextEditingController inputGeo = TextEditingController();
TextEditingController startDatepicker = TextEditingController(); TextEditingController startDatepicker = TextEditingController();
TextEditingController startTimepicker = TextEditingController(); TextEditingController startTimepicker = TextEditingController();
TextEditingController endDatepicker = TextEditingController(); TextEditingController endDatepicker = TextEditingController();
TextEditingController endTimepicker = TextEditingController(); TextEditingController endTimepicker = TextEditingController();
final _stringTagController = StringTagController(); final _stringTagController = StringTagController();
List<Map<String, dynamic>> suggestions = [];
String geographicalZone = "";
List<String> initialTags = []; List<String> initialTags = [];
final _stringOrgaController = StringTagController(); final _stringOrgaController = StringTagController();
@ -69,10 +70,16 @@ class _UpdateeventImageState extends State<UpdateeventImage>
if (position == "end") { if (position == "end") {
date = "end_date"; 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( DateTime? pickedDate = await showDatePicker(
context: context, context: context,
firstDate: DateTime.parse(widget.events[date]), firstDate: dateEvent,
initialDate: DateTime.parse(widget.events[date]), initialDate: dateEvent,
lastDate: DateTime(2104)); lastDate: DateTime(2104));
if (pickedDate == null) return; if (pickedDate == null) return;
if (position == "start") { if (position == "start") {
@ -89,10 +96,14 @@ class _UpdateeventImageState extends State<UpdateeventImage>
if (position == "end") { if (position == "end") {
date = "end_date"; date = "end_date";
} }
TimeOfDay? pickedDate = await showTimePicker( TimeOfDay timeEvent;
context: context, if (widget.events[date].toString().isEmpty) {
initialTime: timeEvent = TimeOfDay.now();
TimeOfDay.fromDateTime(DateTime.parse(widget.events[date]))); } else {
timeEvent = TimeOfDay.fromDateTime(DateTime.parse(widget.events[date]));
}
TimeOfDay? pickedDate =
await showTimePicker(context: context, initialTime: timeEvent);
if (pickedDate == null) return; if (pickedDate == null) return;
if (position == "start") { if (position == "start") {
startTimepicker.text = pickedDate.format(context); startTimepicker.text = pickedDate.format(context);
@ -128,10 +139,7 @@ class _UpdateeventImageState extends State<UpdateeventImage>
Future<void> _updateEvent(BuildContext context) async { Future<void> _updateEvent(BuildContext context) async {
var url = Uri.parse("${globals.api}/token"); var url = Uri.parse("${globals.api}/token");
var name = inputName.text; var name = inputName.text;
var place = inputAddress.text; var place = inputGeo.text;
var city = inputCity.text;
var country = inputCountry.text;
var zipCode = inputZipCode.text;
var description = inputDesc.text; var description = inputDesc.text;
List<String> tags = List<String>.from(_stringTagController.getTags as List); List<String> tags = List<String>.from(_stringTagController.getTags as List);
List<String> organizers = List<String> organizers =
@ -150,6 +158,19 @@ class _UpdateeventImageState extends State<UpdateeventImage>
if (accessToken.isNotEmpty) { if (accessToken.isNotEmpty) {
try { try {
await dotenv.load(); await dotenv.load();
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));
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (data['features'].isNotEmpty) {
final coordinates = data['features'][0]['geometry']['coordinates'];
final longitude = coordinates[0]; // Longitude
final latitude = coordinates[1]; // Latitude
final params = { final params = {
'expiration': '15552000', 'expiration': '15552000',
'key': dotenv.env["IMGBB_API_KEY"], 'key': dotenv.env["IMGBB_API_KEY"],
@ -180,7 +201,8 @@ class _UpdateeventImageState extends State<UpdateeventImage>
var responsePut = await http.put(urlPut, var responsePut = await http.put(urlPut,
headers: { headers: {
HttpHeaders.cookieHeader: 'access_token=${accessToken}', HttpHeaders.cookieHeader: 'access_token=${accessToken}',
HttpHeaders.acceptHeader: 'application/json, text/plain, */*', HttpHeaders.acceptHeader:
'application/json, text/plain, */*',
HttpHeaders.contentTypeHeader: 'application/json' HttpHeaders.contentTypeHeader: 'application/json'
}, },
body: jsonEncode({ body: jsonEncode({
@ -188,12 +210,9 @@ class _UpdateeventImageState extends State<UpdateeventImage>
'place': place, 'place': place,
'start_date': startDate, 'start_date': startDate,
'end_date': endDate, 'end_date': endDate,
'zip_code': zipCode,
'country': country,
'city': city,
'organizers': organizers, 'organizers': organizers,
'latitude': '0.0', 'latitude': latitude,
'longitude': '0.0', 'longitude': longitude,
'description': description, 'description': description,
"imgUrl": imgUrl, "imgUrl": imgUrl,
"tags": tags "tags": tags
@ -246,6 +265,12 @@ class _UpdateeventImageState extends State<UpdateeventImage>
} else { } else {
print("imgbb error : ${status}"); print("imgbb error : ${status}");
} }
} else {
showErrorDialog(context, "Aucune donnée geographique");
}
} else {
showErrorDialog(context, "Mapbox non accessible");
}
} catch (e) { } catch (e) {
showErrorDialog(context, "${e}"); showErrorDialog(context, "${e}");
} }
@ -255,22 +280,22 @@ class _UpdateeventImageState extends State<UpdateeventImage>
} }
void start() async { void start() async {
print("events : ${widget.events}");
inputName.text = convertNulltoEmptyString(widget.events["name"]); inputName.text = convertNulltoEmptyString(widget.events["name"]);
inputCity.text = convertNulltoEmptyString(widget.events["city"]); inputGeo.text = convertNulltoEmptyString(widget.events["place"]);
inputAddress.text = convertNulltoEmptyString(widget.events["address"]);
inputZipCode.text = convertNulltoEmptyString(widget.events["zip_code"]);
inputCountry.text = convertNulltoEmptyString(widget.events["country"]);
inputDesc.text = convertNulltoEmptyString(widget.events["description"]); inputDesc.text = convertNulltoEmptyString(widget.events["description"]);
if (widget.events["start_date"].toString().isNotEmpty) {
DateTime pickedStartDate = DateTime pickedStartDate =
DateTime.parse(convertNulltoEmptyString(widget.events["start_date"])); 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 pickedEndDate =
DateTime.parse(convertNulltoEmptyString(widget.events["end_date"])); DateTime.parse(convertNulltoEmptyString(widget.events["end_date"]));
startDatepicker.text = DateFormat("dd-MM-yyyy").format(pickedStartDate);
endDatepicker.text = DateFormat("dd-MM-yyyy").format(pickedEndDate); endDatepicker.text = DateFormat("dd-MM-yyyy").format(pickedEndDate);
startTimepicker.text = DateFormat("HH-mm").format(pickedStartDate);
endTimepicker.text = DateFormat("HH-mm").format(pickedEndDate); endTimepicker.text = DateFormat("HH-mm").format(pickedEndDate);
}
initialTags = List<String>.from(widget.events['tags'] as List); initialTags = List<String>.from(widget.events['tags'] as List);
initialOrga = List<String>.from(widget.events['organizers'] as List); initialOrga = List<String>.from(widget.events['organizers'] as List);
} }
@ -286,6 +311,93 @@ class _UpdateeventImageState extends State<UpdateeventImage>
return value!.isEmpty ? 'Champ requis' : null; return value!.isEmpty ? 'Champ requis' : null;
} }
Future<void> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -323,58 +435,7 @@ class _UpdateeventImageState extends State<UpdateeventImage>
hintText: 'Modifier le nom de l\'évènement'), hintText: 'Modifier le nom de l\'évènement'),
), ),
), ),
Padding( _buildGeographicalZoneSearchField(),
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'),
),
),
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 15.0, right: 15.0, top: 15, bottom: 0), left: 15.0, right: 15.0, top: 15, bottom: 0),

View File

@ -6,11 +6,13 @@ import FlutterMacOS
import Foundation import Foundation
import file_selector_macos import file_selector_macos
import geolocator_apple
import path_provider_foundation import path_provider_foundation
import shared_preferences_foundation import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
} }

View File

@ -89,6 +89,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.4+2" 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: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@ -177,6 +185,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.9.3+2" version: "0.9.3+2"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -232,6 +248,54 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.4" 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: http:
dependency: "direct main" dependency: "direct main"
description: description:
@ -448,6 +512,54 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" 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: platform:
dependency: transitive dependency: transitive
description: description:
@ -533,6 +645,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.10.0" version: "1.10.0"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -597,6 +717,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.2" version: "1.3.2"
uuid:
dependency: transitive
description:
name: uuid
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
url: "https://pub.dev"
source: hosted
version: "4.5.1"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:

View File

@ -46,6 +46,8 @@ dependencies:
image_picker: ^1.1.2 image_picker: ^1.1.2
date_format_field: ^0.1.0 date_format_field: ^0.1.0
textfield_tags: ^3.0.1 textfield_tags: ^3.0.1
geolocator: ^13.0.1
permission_handler: ^11.3.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -7,8 +7,14 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <file_selector_windows/file_selector_windows.h> #include <file_selector_windows/file_selector_windows.h>
#include <geolocator_windows/geolocator_windows.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar( FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows")); registry->GetRegistrarForPlugin("FileSelectorWindows"));
GeolocatorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("GeolocatorWindows"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
} }

View File

@ -4,6 +4,8 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows file_selector_windows
geolocator_windows
permission_handler_windows
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST