From 6c806b1a393e7360a195060dff65daf26799b0ea Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Wed, 23 Oct 2024 22:43:03 +0200 Subject: [PATCH 01/14] add searchbar --- covas_mobile/lib/pages/ListItemMenu.dart | 140 +++++++++++++++++++++-- 1 file changed, 132 insertions(+), 8 deletions(-) diff --git a/covas_mobile/lib/pages/ListItemMenu.dart b/covas_mobile/lib/pages/ListItemMenu.dart index 2f31a10..c46371c 100644 --- a/covas_mobile/lib/pages/ListItemMenu.dart +++ b/covas_mobile/lib/pages/ListItemMenu.dart @@ -42,6 +42,8 @@ class ListItemMenu extends StatefulWidget { class _MyHomePageState extends State { // variable to call and store future list of posts Future> postsFuture = getPosts(); + List filteredPosts = []; + late SearchBar searchBar; // function to fetch data from api and return future list of posts static Future> getPosts() async { @@ -60,15 +62,61 @@ class _MyHomePageState extends State { return body; } + Future> searchPosts(String query) 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"); + 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; + } + Future popCamera() async { await availableCameras().then((value) => Navigator.push(context, MaterialPageRoute(builder: (_) => Camera(camera: value.first)))); } + void _filterPosts(String query) async { + if (query.isNotEmpty) { + List results = await searchPosts(query); + setState(() { + filteredPosts = results; + }); + } else { + // Reset to full list or clear results + setState(() { + filteredPosts.clear(); + }); + } + } + // build function @override Widget build(BuildContext context) { return Scaffold( + appBar: AppBar( + 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(), + ); + }, + ), + ], + ), body: Center( // FutureBuilder child: FutureBuilder>( @@ -80,7 +128,9 @@ class _MyHomePageState extends State { } else if (snapshot.hasData) { // once data is fetched, display it on screen (call buildPosts()) final posts = snapshot.data!; - return buildPosts(posts); + final displayedPosts = + filteredPosts.isEmpty ? posts : filteredPosts; + return buildPosts(displayedPosts); } else { // if no data, show simple Text return const Text("No data available"); @@ -95,13 +145,6 @@ class _MyHomePageState extends State { 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"), - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - ), body: ListView.separated( itemCount: posts.length, itemBuilder: (context, index) { @@ -133,3 +176,84 @@ class _MyHomePageState extends State { ); } } + +class SearchDelegateExample extends SearchDelegate { + @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), + 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) { + // Show suggestions as the user types + return Container(); // Implement suggestions if needed + } + + Future> searchPosts(String query) 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"); + 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; + } +} From c43eb789b1f2cb0cd6eabe01582f21ae7446b436 Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Wed, 23 Oct 2024 23:56:04 +0200 Subject: [PATCH 02/14] fix icons camera --- covas_mobile/lib/pages/ListItemMenu.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covas_mobile/lib/pages/ListItemMenu.dart b/covas_mobile/lib/pages/ListItemMenu.dart index c46371c..6de0eec 100644 --- a/covas_mobile/lib/pages/ListItemMenu.dart +++ b/covas_mobile/lib/pages/ListItemMenu.dart @@ -171,7 +171,7 @@ class _MyHomePageState extends State { onPressed: popCamera, backgroundColor: Colors.blue, tooltip: 'Recherche', - child: const Icon(Icons.search, color: Colors.white), + child: const Icon(Icons.camera_alt, color: Colors.white), ), ); } From 4e0222d4bbbedd89d197b6fd26002644b8789ef5 Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Sat, 26 Oct 2024 17:51:31 +0200 Subject: [PATCH 03/14] add second searchbar to search by geographical zone --- covas_mobile/lib/pages/ListItemMenu.dart | 76 ++++++++++++++++-------- 1 file changed, 52 insertions(+), 24 deletions(-) diff --git a/covas_mobile/lib/pages/ListItemMenu.dart b/covas_mobile/lib/pages/ListItemMenu.dart index 6de0eec..92b37da 100644 --- a/covas_mobile/lib/pages/ListItemMenu.dart +++ b/covas_mobile/lib/pages/ListItemMenu.dart @@ -45,6 +45,9 @@ class _MyHomePageState extends State { List filteredPosts = []; late SearchBar searchBar; + String geographicalZone = ''; + String query = ''; + // function to fetch data from api and return future list of posts static Future> getPosts() async { SharedPreferences prefs = await SharedPreferences.getInstance(); @@ -83,20 +86,29 @@ class _MyHomePageState extends State { MaterialPageRoute(builder: (_) => Camera(camera: value.first)))); } - void _filterPosts(String query) async { - if (query.isNotEmpty) { + void _filterPosts() async { + if (query.isNotEmpty || geographicalZone.isNotEmpty) { List results = await searchPosts(query); setState(() { - filteredPosts = results; + filteredPosts = _applyFilters(results); }); } else { - // Reset to full list or clear results setState(() { filteredPosts.clear(); }); } } + List _applyFilters(List posts) { + return posts.where((post) { + final matchesQuery = + post.name!.toLowerCase().contains(query.toLowerCase()); + final matchesZone = geographicalZone.isEmpty || + post.place!.toLowerCase().contains(geographicalZone.toLowerCase()); + return matchesQuery && matchesZone; + }).toList(); + } + // build function @override Widget build(BuildContext context) { @@ -117,26 +129,42 @@ class _MyHomePageState extends State { ), ], ), - 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!; - final displayedPosts = - filteredPosts.isEmpty ? posts : filteredPosts; - return buildPosts(displayedPosts); - } else { - // if no data, show simple Text - return const Text("No data available"); - } - }, - ), + body: Column( + children: [ + // New Search Bar for Geographical Zone + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + decoration: InputDecoration( + labelText: 'Search by geographical zone', + border: OutlineInputBorder(), + ), + onChanged: (value) { + setState(() { + geographicalZone = value; + }); + _filterPosts(); // Call the filtering function + }, + ), + ), + Expanded( + child: FutureBuilder>( + future: postsFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const 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"); + } + }, + ), + ), + ], ), ); } From 576d045cd87ad1f6463627af5422487d737d3689 Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Mon, 28 Oct 2024 22:52:00 +0100 Subject: [PATCH 04/14] add searchbar by geographical zone --- covas_mobile/lib/pages/ListItemMenu.dart | 113 ++++++++--------------- 1 file changed, 38 insertions(+), 75 deletions(-) diff --git a/covas_mobile/lib/pages/ListItemMenu.dart b/covas_mobile/lib/pages/ListItemMenu.dart index 92b37da..4c18ef3 100644 --- a/covas_mobile/lib/pages/ListItemMenu.dart +++ b/covas_mobile/lib/pages/ListItemMenu.dart @@ -1,8 +1,7 @@ import 'dart:convert'; import 'dart:io'; -import "ItemMenu.dart"; -import "Camera.dart"; - +import 'ItemMenu.dart'; +import 'Camera.dart'; import 'package:http/http.dart' as http; import 'package:flutter/material.dart'; import '../classes/events.dart'; @@ -10,12 +9,11 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:intl/intl.dart'; import 'package:intl/date_symbol_data_local.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 { @@ -40,11 +38,8 @@ class ListItemMenu extends StatefulWidget { // homepage state class _MyHomePageState extends State { - // variable to call and store future list of posts Future> postsFuture = getPosts(); List filteredPosts = []; - late SearchBar searchBar; - String geographicalZone = ''; String query = ''; @@ -86,27 +81,21 @@ class _MyHomePageState extends State { MaterialPageRoute(builder: (_) => Camera(camera: value.first)))); } - void _filterPosts() async { - if (query.isNotEmpty || geographicalZone.isNotEmpty) { - List results = await searchPosts(query); - setState(() { - filteredPosts = _applyFilters(results); - }); - } else { - setState(() { - filteredPosts.clear(); - }); - } - } - - List _applyFilters(List posts) { - return posts.where((post) { - final matchesQuery = - post.name!.toLowerCase().contains(query.toLowerCase()); - final matchesZone = geographicalZone.isEmpty || - post.place!.toLowerCase().contains(geographicalZone.toLowerCase()); - return matchesQuery && matchesZone; - }).toList(); + Padding _buildGeographicalZoneSearchField() { + return Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + decoration: InputDecoration( + labelText: 'Search by geographical zone', + border: OutlineInputBorder(), + ), + onChanged: (value) { + setState(() { + geographicalZone = value; // Update geographical zone + }); + }, + ), + ); } // build function @@ -132,27 +121,13 @@ class _MyHomePageState extends State { body: Column( children: [ // New Search Bar for Geographical Zone - Padding( - padding: const EdgeInsets.all(8.0), - child: TextField( - decoration: InputDecoration( - labelText: 'Search by geographical zone', - border: OutlineInputBorder(), - ), - onChanged: (value) { - setState(() { - geographicalZone = value; - }); - _filterPosts(); // Call the filtering function - }, - ), - ), + _buildGeographicalZoneSearchField(), Expanded( child: FutureBuilder>( future: postsFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { - return const CircularProgressIndicator(); + return const Center(child: CircularProgressIndicator()); } else if (snapshot.hasData) { final posts = snapshot.data!; final displayedPosts = @@ -171,36 +146,25 @@ class _MyHomePageState extends State { // function to display fetched data on screen Widget buildPosts(List posts) { - // ListView Builder to show data in a list - return Scaffold( - 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 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(); - }, - ), - floatingActionButton: FloatingActionButton( - onPressed: popCamera, - backgroundColor: Colors.blue, - tooltip: 'Recherche', - child: const Icon(Icons.camera_alt, color: Colors.white), - ), + 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(); + }, ); } } @@ -265,7 +229,6 @@ class SearchDelegateExample extends SearchDelegate { @override Widget buildSuggestions(BuildContext context) { - // Show suggestions as the user types return Container(); // Implement suggestions if needed } From 3a350e33cc5af8e7d0e3e1370504fc7c3973cc1b Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Mon, 28 Oct 2024 23:50:20 +0100 Subject: [PATCH 05/14] add searchbar with mapbox and suggesstionbar --- covas_mobile/lib/pages/ListItemMenu.dart | 106 +++++++++++++++-------- covas_mobile/pubspec.lock | 80 +++++++++++++++++ covas_mobile/pubspec.yaml | 1 + 3 files changed, 150 insertions(+), 37 deletions(-) diff --git a/covas_mobile/lib/pages/ListItemMenu.dart b/covas_mobile/lib/pages/ListItemMenu.dart index 4c18ef3..46faa13 100644 --- a/covas_mobile/lib/pages/ListItemMenu.dart +++ b/covas_mobile/lib/pages/ListItemMenu.dart @@ -1,17 +1,18 @@ +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 '../classes/events.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:intl/intl.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:camera/camera.dart'; +import 'package:mapbox_gl/mapbox_gl.dart'; // Add this import import '../variable/globals.dart' as globals; -// app starting point void main() { initializeDateFormatting("fr_FR", null).then((_) => runApp(const MyApp())); } @@ -28,7 +29,6 @@ class MyApp extends StatelessWidget { } } -// homepage class class ListItemMenu extends StatefulWidget { const ListItemMenu({super.key}); @@ -36,14 +36,15 @@ class ListItemMenu extends StatefulWidget { State createState() => _MyHomePageState(); } -// homepage state class _MyHomePageState extends State { Future> postsFuture = getPosts(); List filteredPosts = []; String geographicalZone = ''; String query = ''; + List suggestions = []; // Store suggestions + TextEditingController inputGeo = TextEditingController(); - // function to fetch data from api and return future list of posts + // Fetching events from API static Future> getPosts() async { SharedPreferences prefs = await SharedPreferences.getInstance(); var accessToken = prefs.getString("access_token") ?? ""; @@ -60,45 +61,77 @@ class _MyHomePageState extends State { return body; } - Future> searchPosts(String query) 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"); - 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; - } + Future searchSuggestions(String input) async { + await dotenv.load(fileName: ".env"); // Load .env file - Future popCamera() async { - await availableCameras().then((value) => Navigator.push(context, - MaterialPageRoute(builder: (_) => Camera(camera: value.first)))); + final mapboxAccessToken = dotenv.env['MAPBOX_ACCESS_TOKEN'] ?? ''; + final url = + 'https://api.mapbox.com/geocoding/v5/mapbox.places/${input}.json?access_token=${mapboxAccessToken}&proximity=ip'; // Replace with your Mapbox token + 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) => + feature['place_name'] as String) // Cast to String explicitly + .toList(); + }); + } else { + throw Exception('Failed to load suggestions'); + } } Padding _buildGeographicalZoneSearchField() { return Padding( padding: const EdgeInsets.all(8.0), - child: TextField( - decoration: InputDecoration( - labelText: 'Search by geographical zone', - border: OutlineInputBorder(), - ), - onChanged: (value) { - setState(() { - geographicalZone = value; // Update geographical zone - }); - }, + child: Column( + children: [ + TextField( + controller: inputGeo, + decoration: InputDecoration( + labelText: 'Search by geographical zone', + border: OutlineInputBorder(), + ), + onChanged: (value) { + setState(() { + geographicalZone = value; + searchSuggestions(value); + }); + }, + ), + if (suggestions.isNotEmpty) + Container( + height: 200, + decoration: BoxDecoration( + border: Border.all(color: Colors.blue), // Add a border color + borderRadius: + BorderRadius.circular(8), // Optional: rounded corners + ), + child: ListView.builder( + shrinkWrap: + true, // Ensure the list takes only the required space + + itemCount: suggestions.length, + itemBuilder: (context, index) { + return ListTile( + title: Text(suggestions[index]), + onTap: () { + setState(() { + geographicalZone = suggestions[index]; + inputGeo.text = suggestions[index]; + suggestions.clear(); + }); + }, + ); + }, + ), + ), + ], ), ); } - // build function @override Widget build(BuildContext context) { return Scaffold( @@ -120,7 +153,6 @@ class _MyHomePageState extends State { ), body: Column( children: [ - // New Search Bar for Geographical Zone _buildGeographicalZoneSearchField(), Expanded( child: FutureBuilder>( @@ -144,7 +176,7 @@ class _MyHomePageState extends State { ); } - // function to display fetched data on screen + // Function to display fetched data on screen Widget buildPosts(List posts) { return ListView.separated( itemCount: posts.length, diff --git a/covas_mobile/pubspec.lock b/covas_mobile/pubspec.lock index 84f9170..cbc50cd 100644 --- a/covas_mobile/pubspec.lock +++ b/covas_mobile/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + archive: + dependency: transitive + description: + name: archive + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + url: "https://pub.dev" + source: hosted + version: "3.6.1" async: dependency: transitive description: @@ -89,6 +97,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: @@ -248,6 +264,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image: + dependency: transitive + description: + name: image + sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6" + url: "https://pub.dev" + source: hosted + version: "3.3.0" image_picker: dependency: "direct main" description: @@ -320,6 +344,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.19.0" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" json_annotation: dependency: transitive description: @@ -360,6 +392,38 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + mapbox_gl: + dependency: "direct main" + description: + name: mapbox_gl + sha256: d78907338ff232e3cf6c1d6dba45e6a8814069496fd352e49bb1967d498f09af + url: "https://pub.dev" + source: hosted + version: "0.16.0" + mapbox_gl_dart: + dependency: transitive + description: + name: mapbox_gl_dart + sha256: de6d03718e5eb05c9eb1ddaae7f0383b28acb5afa16405e1deed7ff04dd34f3d + url: "https://pub.dev" + source: hosted + version: "0.2.1" + mapbox_gl_platform_interface: + dependency: transitive + description: + name: mapbox_gl_platform_interface + sha256: b7c1490b022e650afd20412bdf8ae45a1897118b7ce6049ef6c42df09193d4b2 + url: "https://pub.dev" + source: hosted + version: "0.16.0" + mapbox_gl_web: + dependency: transitive + description: + name: mapbox_gl_web + sha256: e77113bf95a4f321ff44938232517e0f2725aae991f0b283af1afaa7e7a58aca + url: "https://pub.dev" + source: hosted + version: "0.16.0" matcher: dependency: transitive description: @@ -448,6 +512,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" platform: dependency: transitive description: @@ -629,6 +701,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" sdks: dart: ">=3.4.0 <4.0.0" flutter: ">=3.22.0" diff --git a/covas_mobile/pubspec.yaml b/covas_mobile/pubspec.yaml index 418204f..e289a3c 100644 --- a/covas_mobile/pubspec.yaml +++ b/covas_mobile/pubspec.yaml @@ -46,6 +46,7 @@ dependencies: image_picker: ^1.1.2 date_format_field: ^0.1.0 textfield_tags: ^3.0.1 + mapbox_gl: ^0.16.0 dev_dependencies: flutter_test: From 951127d7bc5568e092583c90b6d7c4fdbdb45382 Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Mon, 4 Nov 2024 23:53:24 +0100 Subject: [PATCH 06/14] add searchbar --- covas_mobile/lib/pages/ListItemMenu.dart | 78 ++++++++++++++++++++---- 1 file changed, 67 insertions(+), 11 deletions(-) diff --git a/covas_mobile/lib/pages/ListItemMenu.dart b/covas_mobile/lib/pages/ListItemMenu.dart index 46faa13..f6ffa13 100644 --- a/covas_mobile/lib/pages/ListItemMenu.dart +++ b/covas_mobile/lib/pages/ListItemMenu.dart @@ -11,6 +11,7 @@ import 'package:intl/intl.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:camera/camera.dart'; import 'package:mapbox_gl/mapbox_gl.dart'; // Add this import +import 'dart:math'; import '../variable/globals.dart' as globals; void main() { @@ -41,7 +42,7 @@ class _MyHomePageState extends State { List filteredPosts = []; String geographicalZone = ''; String query = ''; - List suggestions = []; // Store suggestions + List> suggestions = []; TextEditingController inputGeo = TextEditingController(); // Fetching events from API @@ -66,15 +67,18 @@ class _MyHomePageState extends State { final mapboxAccessToken = dotenv.env['MAPBOX_ACCESS_TOKEN'] ?? ''; final url = - 'https://api.mapbox.com/geocoding/v5/mapbox.places/${input}.json?access_token=${mapboxAccessToken}&proximity=ip'; // Replace with your Mapbox token + '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) => - feature['place_name'] as String) // Cast to String explicitly + .map((feature) => { + 'place_name': feature['place_name'], + 'geometry': feature[ + 'geometry'], // Include geometry for latitude/longitude + }) .toList(); }); } else { @@ -82,6 +86,43 @@ class _MyHomePageState extends State { } } + 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" + }); + + if (response.statusCode == 200) { + final List body = json.decode(utf8.decode(response.bodyBytes)); + setState(() { + filteredPosts = body + .map((e) => Events.fromJson(e as Map)) + .toList(); + }); + } else { + throw Exception('Failed to load posts'); + } + } + } + Padding _buildGeographicalZoneSearchField() { return Padding( padding: const EdgeInsets.all(8.0), @@ -92,6 +133,16 @@ class _MyHomePageState extends State { decoration: InputDecoration( labelText: 'Search by geographical zone', border: OutlineInputBorder(), + suffixIcon: IconButton( + icon: 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(() { @@ -109,19 +160,24 @@ class _MyHomePageState extends State { BorderRadius.circular(8), // Optional: rounded corners ), child: ListView.builder( - shrinkWrap: - true, // Ensure the list takes only the required space - + shrinkWrap: true, itemCount: suggestions.length, itemBuilder: (context, index) { return ListTile( - title: Text(suggestions[index]), - onTap: () { + 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]; - inputGeo.text = suggestions[index]; + geographicalZone = suggestions[index]['place_name']; + inputGeo.text = geographicalZone; suggestions.clear(); }); + + await fetchPostsByLocation(latitude, longitude); }, ); }, From 517652df9887a59629e13cdfaa3e7b6d2a9caac9 Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Tue, 5 Nov 2024 15:11:53 +0100 Subject: [PATCH 07/14] searchbar works but return empty doesn't refresh --- covas_mobile/lib/pages/ListItemMenu.dart | 46 +++++++++++++----------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/covas_mobile/lib/pages/ListItemMenu.dart b/covas_mobile/lib/pages/ListItemMenu.dart index f6ffa13..372f228 100644 --- a/covas_mobile/lib/pages/ListItemMenu.dart +++ b/covas_mobile/lib/pages/ListItemMenu.dart @@ -113,9 +113,11 @@ class _MyHomePageState extends State { if (response.statusCode == 200) { final List body = json.decode(utf8.decode(response.bodyBytes)); setState(() { - filteredPosts = body - .map((e) => Events.fromJson(e as Map)) - .toList(); + filteredPosts = body.isNotEmpty + ? body + .map((e) => Events.fromJson(e as Map)) + .toList() + : []; }); } else { throw Exception('Failed to load posts'); @@ -140,6 +142,7 @@ class _MyHomePageState extends State { inputGeo.clear(); // Clear the text field geographicalZone = ''; // Reset the geographical zone state suggestions.clear(); // Optionally clear suggestions + filteredPosts.clear(); }); }, ), @@ -155,9 +158,8 @@ class _MyHomePageState extends State { Container( height: 200, decoration: BoxDecoration( - border: Border.all(color: Colors.blue), // Add a border color - borderRadius: - BorderRadius.circular(8), // Optional: rounded corners + border: Border.all(color: Colors.blue), + borderRadius: BorderRadius.circular(8), ), child: ListView.builder( shrinkWrap: true, @@ -243,16 +245,17 @@ class _MyHomePageState extends State { 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(); + title: Text('${post.name!}'), + subtitle: Text('${post.place!}\n${date} ${time}'), + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => ItemMenu(title: post.id!)), + ); + }, + ); }, + separatorBuilder: (context, index) => Divider(), ); } } @@ -264,7 +267,8 @@ class SearchDelegateExample extends SearchDelegate { IconButton( icon: const Icon(Icons.clear), onPressed: () { - query = ''; + query = ''; // Clear the query text + showSuggestions(context); // Show suggestions }, ), ]; @@ -273,18 +277,18 @@ class SearchDelegateExample extends SearchDelegate { @override Widget buildLeading(BuildContext context) { return IconButton( - icon: const Icon(Icons.arrow_back), + icon: const Icon(Icons.arrow_back), // Default back icon onPressed: () { - close(context, null); + close(context, null); // Close the search delegate }, ); } @override Widget buildResults(BuildContext context) { - // Perform the search and return the results + // Here, you can return the actual results based on the query return FutureBuilder>( - future: searchPosts(query), + future: searchPosts(query), // Implement your own search logic builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); @@ -317,7 +321,7 @@ class SearchDelegateExample extends SearchDelegate { @override Widget buildSuggestions(BuildContext context) { - return Container(); // Implement suggestions if needed + return Container(); // This is for showing search suggestions if needed } Future> searchPosts(String query) async { From b796d0206f551ef611871606482e64490be8327b Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Tue, 5 Nov 2024 15:59:31 +0100 Subject: [PATCH 08/14] geographical search bars does works --- covas_mobile/lib/pages/ListItemMenu.dart | 71 ++++++++++++++++++------ 1 file changed, 54 insertions(+), 17 deletions(-) diff --git a/covas_mobile/lib/pages/ListItemMenu.dart b/covas_mobile/lib/pages/ListItemMenu.dart index 372f228..87a974e 100644 --- a/covas_mobile/lib/pages/ListItemMenu.dart +++ b/covas_mobile/lib/pages/ListItemMenu.dart @@ -62,6 +62,27 @@ class _MyHomePageState extends State { return body; } + @override + void initState() { + super.initState(); + // Initialize data fetch when the page loads + _fetchInitialData(); + } + + // 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 getPosts(); + 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 @@ -109,15 +130,21 @@ class _MyHomePageState extends State { "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(() { - filteredPosts = body.isNotEmpty - ? body - .map((e) => Events.fromJson(e as Map)) - .toList() - : []; + 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'); @@ -136,13 +163,13 @@ class _MyHomePageState extends State { labelText: 'Search by geographical zone', border: OutlineInputBorder(), suffixIcon: IconButton( - icon: Icon(Icons.clear), + icon: const Icon(Icons.clear), onPressed: () { setState(() { inputGeo.clear(); // Clear the text field geographicalZone = ''; // Reset the geographical zone state suggestions.clear(); // Optionally clear suggestions - filteredPosts.clear(); + _fetchInitialData(); // Clear the filtered posts }); }, ), @@ -236,10 +263,21 @@ class _MyHomePageState extends State { // Function to display fetched data on screen Widget buildPosts(List 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: posts.length, + itemCount: displayedPosts.length, itemBuilder: (context, index) { - final post = posts[index]; + final post = displayedPosts[index]; final startDate = DateTime.parse(post.startDate!); final date = DateFormat.yMd().format(startDate); final time = DateFormat.Hm().format(startDate); @@ -267,8 +305,7 @@ class SearchDelegateExample extends SearchDelegate { IconButton( icon: const Icon(Icons.clear), onPressed: () { - query = ''; // Clear the query text - showSuggestions(context); // Show suggestions + query = ''; }, ), ]; @@ -277,18 +314,18 @@ class SearchDelegateExample extends SearchDelegate { @override Widget buildLeading(BuildContext context) { return IconButton( - icon: const Icon(Icons.arrow_back), // Default back icon + icon: const Icon(Icons.arrow_back), onPressed: () { - close(context, null); // Close the search delegate + close(context, null); }, ); } @override Widget buildResults(BuildContext context) { - // Here, you can return the actual results based on the query + // Perform the search and return the results return FutureBuilder>( - future: searchPosts(query), // Implement your own search logic + future: searchPosts(query), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); @@ -321,7 +358,7 @@ class SearchDelegateExample extends SearchDelegate { @override Widget buildSuggestions(BuildContext context) { - return Container(); // This is for showing search suggestions if needed + return Container(); // Implement suggestions if needed } Future> searchPosts(String query) async { From 3a0f24cc4c23b1b12feee7a65bc79e00c970bd85 Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Tue, 5 Nov 2024 23:29:55 +0100 Subject: [PATCH 09/14] search item with geographical search --- covas_mobile/lib/pages/ListItemMenu.dart | 86 +------------- covas_mobile/lib/pages/SearchDelegate.dart | 129 +++++++++++++++++++++ 2 files changed, 131 insertions(+), 84 deletions(-) create mode 100644 covas_mobile/lib/pages/SearchDelegate.dart diff --git a/covas_mobile/lib/pages/ListItemMenu.dart b/covas_mobile/lib/pages/ListItemMenu.dart index 87a974e..145255d 100644 --- a/covas_mobile/lib/pages/ListItemMenu.dart +++ b/covas_mobile/lib/pages/ListItemMenu.dart @@ -4,13 +4,11 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; // Import dotenv import 'dart:convert'; import 'dart:io'; import 'ItemMenu.dart'; -import 'Camera.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 'package:camera/camera.dart'; -import 'package:mapbox_gl/mapbox_gl.dart'; // Add this import import 'dart:math'; import '../variable/globals.dart' as globals; @@ -230,7 +228,7 @@ class _MyHomePageState extends State { onPressed: () { showSearch( context: context, - delegate: SearchDelegateExample(), + delegate: SearchDelegateExample(geoQuery: inputGeo.text), ); }, ), @@ -297,83 +295,3 @@ class _MyHomePageState extends State { ); } } - -class SearchDelegateExample extends SearchDelegate { - @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), - 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) 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"); - 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/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; + } +} From 5c0f4d345d65e691bc448893cd71ac32718918e2 Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Wed, 6 Nov 2024 13:27:31 +0100 Subject: [PATCH 10/14] get location from starting app --- .../android/app/src/main/AndroidManifest.xml | 3 + .../app/src/profile/AndroidManifest.xml | 12 ++ covas_mobile/ios/Runner/Info.plist | 2 + covas_mobile/lib/pages/ListItemMenu.dart | 133 ++++++++++++- .../Flutter/GeneratedPluginRegistrant.swift | 2 + covas_mobile/pubspec.lock | 186 +++++++++++------- covas_mobile/pubspec.yaml | 3 +- .../flutter/generated_plugin_registrant.cc | 6 + .../windows/flutter/generated_plugins.cmake | 2 + 9 files changed, 278 insertions(+), 71 deletions(-) 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/pages/ListItemMenu.dart b/covas_mobile/lib/pages/ListItemMenu.dart index 145255d..1a83313 100644 --- a/covas_mobile/lib/pages/ListItemMenu.dart +++ b/covas_mobile/lib/pages/ListItemMenu.dart @@ -10,7 +10,9 @@ 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'; void main() { initializeDateFormatting("fr_FR", null).then((_) => runApp(const MyApp())); @@ -63,8 +65,137 @@ class _MyHomePageState extends State { @override void initState() { super.initState(); + _checkLocationPermission(); // Initialize data fetch when the page loads - _fetchInitialData(); + _getCurrentLocation(); + } + + // Check if location permission is granted + 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"); + _fetchInitialData(); + } 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"); + _fetchInitialData(); + } + } + + // Get the device's current location + Future _getCurrentLocation() async { + // 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) { + setState(() { + inputGeo.text = "${city}, ${country}"; + }); + fetchPostsByLocation(latitude, longitude); + } else { + _fetchInitialData(); + } + } else { + _fetchInitialData(); + } + } else { + _fetchInitialData(); + throw Exception('Failed to load location data'); + } + } catch (e) { + print("Error getting city and country: $e"); + _fetchInitialData(); + } + } + + // 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 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 cbc50cd..9bad8b5 100644 --- a/covas_mobile/pubspec.lock +++ b/covas_mobile/pubspec.lock @@ -1,14 +1,6 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: - archive: - dependency: transitive - description: - name: archive - sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d - url: "https://pub.dev" - source: hosted - version: "3.6.1" async: dependency: transitive description: @@ -193,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 @@ -248,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: @@ -264,14 +312,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" - image: - dependency: transitive - description: - name: image - sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6" - url: "https://pub.dev" - source: hosted - version: "3.3.0" image_picker: dependency: "direct main" description: @@ -344,14 +384,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.19.0" - js: - dependency: transitive - description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.dev" - source: hosted - version: "0.6.7" json_annotation: dependency: transitive description: @@ -392,38 +424,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" - mapbox_gl: - dependency: "direct main" - description: - name: mapbox_gl - sha256: d78907338ff232e3cf6c1d6dba45e6a8814069496fd352e49bb1967d498f09af - url: "https://pub.dev" - source: hosted - version: "0.16.0" - mapbox_gl_dart: - dependency: transitive - description: - name: mapbox_gl_dart - sha256: de6d03718e5eb05c9eb1ddaae7f0383b28acb5afa16405e1deed7ff04dd34f3d - url: "https://pub.dev" - source: hosted - version: "0.2.1" - mapbox_gl_platform_interface: - dependency: transitive - description: - name: mapbox_gl_platform_interface - sha256: b7c1490b022e650afd20412bdf8ae45a1897118b7ce6049ef6c42df09193d4b2 - url: "https://pub.dev" - source: hosted - version: "0.16.0" - mapbox_gl_web: - dependency: transitive - description: - name: mapbox_gl_web - sha256: e77113bf95a4f321ff44938232517e0f2725aae991f0b283af1afaa7e7a58aca - url: "https://pub.dev" - source: hosted - version: "0.16.0" matcher: dependency: transitive description: @@ -512,14 +512,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" - petitparser: - dependency: transitive + permission_handler: + dependency: "direct main" description: - name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + name: permission_handler + sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" url: "https://pub.dev" source: hosted - version: "6.0.2" + 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: @@ -605,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: @@ -669,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: @@ -701,14 +757,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" - xml: - dependency: transitive - description: - name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 - url: "https://pub.dev" - source: hosted - version: "6.5.0" sdks: dart: ">=3.4.0 <4.0.0" flutter: ">=3.22.0" diff --git a/covas_mobile/pubspec.yaml b/covas_mobile/pubspec.yaml index e289a3c..6bf13bd 100644 --- a/covas_mobile/pubspec.yaml +++ b/covas_mobile/pubspec.yaml @@ -46,7 +46,8 @@ dependencies: image_picker: ^1.1.2 date_format_field: ^0.1.0 textfield_tags: ^3.0.1 - mapbox_gl: ^0.16.0 + 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 From b22458021b709cda1fb2c376ff64a4cda51375ed Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Wed, 6 Nov 2024 15:55:15 +0100 Subject: [PATCH 11/14] get events from current position --- covas_mobile/lib/main.dart | 40 ++++++++ covas_mobile/lib/pages/ListItemMenu.dart | 123 ++++++++++++----------- 2 files changed, 107 insertions(+), 56 deletions(-) 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/ListItemMenu.dart b/covas_mobile/lib/pages/ListItemMenu.dart index 1a83313..bde1c0f 100644 --- a/covas_mobile/lib/pages/ListItemMenu.dart +++ b/covas_mobile/lib/pages/ListItemMenu.dart @@ -47,6 +47,52 @@ class _MyHomePageState extends State { // 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 = []; @@ -65,70 +111,34 @@ class _MyHomePageState extends State { @override void initState() { super.initState(); - _checkLocationPermission(); // Initialize data fetch when the page loads _getCurrentLocation(); } - // Check if location permission is granted - Future _checkLocationPermission() async { + // Get the device's current location + Future _getCurrentLocation() 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(); + + // 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); } } - // 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"); - _fetchInitialData(); - } 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"); - _fetchInitialData(); - } - } - - // Get the device's current location - Future _getCurrentLocation() async { - // 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 @@ -156,10 +166,10 @@ class _MyHomePageState extends State { String country = _getCountryFromFeatures(features); print("city : ${city} ${country}"); if (city.isNotEmpty && country.isNotEmpty) { + fetchPostsByLocation(latitude, longitude); setState(() { inputGeo.text = "${city}, ${country}"; }); - fetchPostsByLocation(latitude, longitude); } else { _fetchInitialData(); } @@ -171,8 +181,8 @@ class _MyHomePageState extends State { throw Exception('Failed to load location data'); } } catch (e) { - print("Error getting city and country: $e"); _fetchInitialData(); + print("Error getting city and country: $e"); } } @@ -202,7 +212,7 @@ class _MyHomePageState extends State { Future _fetchInitialData() async { try { // Optionally, you can fetch posts initially if needed. - List initialPosts = await getPosts(); + List initialPosts = await getAllPosts(); setState(() { // Assign to the postsFuture and update the filtered posts if needed filteredPosts = initialPosts; @@ -392,6 +402,7 @@ class _MyHomePageState extends State { // Function to display fetched data on screen Widget buildPosts(List posts) { + print("posts : ${posts}"); print("filteredposts : ${filteredPosts}"); final displayedPosts = filteredPosts; print("results ${displayedPosts}"); From 44a0691e312e11a7aa393d9b37b8ef15d2d73430 Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Thu, 7 Nov 2024 23:10:03 +0100 Subject: [PATCH 12/14] add latitude and longitude --- .../lib/pages/DisplayPictureScreen.dart | 9 +- covas_mobile/lib/pages/ListItemMenu.dart | 52 ++- covas_mobile/lib/pages/UpdateEventImage.dart | 353 ++++++++++-------- 3 files changed, 240 insertions(+), 174 deletions(-) diff --git a/covas_mobile/lib/pages/DisplayPictureScreen.dart b/covas_mobile/lib/pages/DisplayPictureScreen.dart index 0c6d18a..76a5fc3 100644 --- a/covas_mobile/lib/pages/DisplayPictureScreen.dart +++ b/covas_mobile/lib/pages/DisplayPictureScreen.dart @@ -85,11 +85,14 @@ class DisplayPictureScreenState extends State } Future searchEvents(String json, String imagePath) async { - print(json); + print(json.replaceAll("'''json", '').replaceAll("'''", "")); SharedPreferences prefs = await SharedPreferences.getInstance(); - Map jsonData = jsonDecode(json); + 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") ?? ""; @@ -132,7 +135,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 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 bde1c0f..c280419 100644 --- a/covas_mobile/lib/pages/ListItemMenu.dart +++ b/covas_mobile/lib/pages/ListItemMenu.dart @@ -13,6 +13,8 @@ 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'; void main() { initializeDateFormatting("fr_FR", null).then((_) => runApp(const MyApp())); @@ -356,6 +358,11 @@ class _MyHomePageState extends State { ); } + Future popCamera() async { + await availableCameras().then((value) => Navigator.push(context, + MaterialPageRoute(builder: (_) => Camera(camera: value.first)))); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -397,6 +404,12 @@ class _MyHomePageState extends State { ), ], ), + floatingActionButton: FloatingActionButton( + onPressed: popCamera, + backgroundColor: Colors.blue, + tooltip: 'Recherche', + child: const Icon(Icons.photo_camera, color: Colors.white), + ), ); } @@ -415,25 +428,26 @@ class _MyHomePageState extends State { } 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); + 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) => Divider(), - ); + 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/UpdateEventImage.dart b/covas_mobile/lib/pages/UpdateEventImage.dart index 650aa4c..c3e0bfa 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(); @@ -128,10 +129,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 +148,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,11 +270,9 @@ 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 = @@ -286,6 +299,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 +423,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), From 00a9125eb26d0f1cc587f18f76217640b4dacfe2 Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Fri, 8 Nov 2024 16:57:07 +0100 Subject: [PATCH 13/14] fix datepicker --- .../lib/pages/DisplayPictureScreen.dart | 59 ++++++++++--------- covas_mobile/lib/pages/UpdateEventImage.dart | 44 +++++++++----- 2 files changed, 59 insertions(+), 44 deletions(-) diff --git a/covas_mobile/lib/pages/DisplayPictureScreen.dart b/covas_mobile/lib/pages/DisplayPictureScreen.dart index 76a5fc3..b09ca67 100644 --- a/covas_mobile/lib/pages/DisplayPictureScreen.dart +++ b/covas_mobile/lib/pages/DisplayPictureScreen.dart @@ -87,38 +87,41 @@ class DisplayPictureScreenState extends State Future searchEvents(String json, String imagePath) async { 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.replaceAll("```json", '').replaceAll("```", "")); - print("json : ${jsonData}"); - var name = jsonData["name"]; - print("name : ${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); diff --git a/covas_mobile/lib/pages/UpdateEventImage.dart b/covas_mobile/lib/pages/UpdateEventImage.dart index c3e0bfa..cd32624 100644 --- a/covas_mobile/lib/pages/UpdateEventImage.dart +++ b/covas_mobile/lib/pages/UpdateEventImage.dart @@ -70,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") { @@ -90,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); @@ -274,16 +284,18 @@ class _UpdateeventImageState extends State inputName.text = convertNulltoEmptyString(widget.events["name"]); 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); } From 1580d7a3cc4af1457e37201283f2a449fca4ab6d Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Fri, 8 Nov 2024 17:07:40 +0100 Subject: [PATCH 14/14] ajout d'une date de fin si jamais, c'est vide --- covas_mobile/lib/pages/DisplayPictureScreen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/covas_mobile/lib/pages/DisplayPictureScreen.dart b/covas_mobile/lib/pages/DisplayPictureScreen.dart index b09ca67..32413a6 100644 --- a/covas_mobile/lib/pages/DisplayPictureScreen.dart +++ b/covas_mobile/lib/pages/DisplayPictureScreen.dart @@ -138,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 (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 sous le format en YYYY-MM-DD HH:mm:ssZ", + "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(