diff --git a/assets/images/no_connection.png b/assets/images/no_connection.png new file mode 100644 index 0000000..8777448 Binary files /dev/null and b/assets/images/no_connection.png differ diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5d23c25..d2a0085 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,6 +1,9 @@ PODS: - camera_avfoundation (0.0.1): - Flutter + - connectivity_plus (0.0.1): + - Flutter + - ReachabilitySwift - device_info_plus (0.0.1): - Flutter - Flutter (1.0.0) @@ -23,6 +26,7 @@ PODS: - FlutterMacOS - permission_handler_apple (9.0.4): - Flutter + - ReachabilitySwift (5.0.0) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS @@ -37,6 +41,7 @@ PODS: DEPENDENCIES: - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) + - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - Flutter (from `Flutter`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) @@ -54,11 +59,14 @@ SPEC REPOS: trunk: - FMDB - ObjectBox + - ReachabilitySwift - Toast EXTERNAL SOURCES: camera_avfoundation: :path: ".symlinks/plugins/camera_avfoundation/ios" + connectivity_plus: + :path: ".symlinks/plugins/connectivity_plus/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" Flutter: @@ -86,6 +94,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: camera_avfoundation: 3125e8cd1a4387f6f31c6c63abb8a55892a9eeeb + connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0 @@ -96,6 +105,7 @@ SPEC CHECKSUMS: package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9 permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce + ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 shared_preferences_foundation: 986fc17f3d3251412d18b0265f9c64113a8c2472 sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 diff --git a/lib/db/box_visit_photo.dart b/lib/db/box_visit_photo.dart index 1db3e66..b633f22 100644 --- a/lib/db/box_visit_photo.dart +++ b/lib/db/box_visit_photo.dart @@ -20,6 +20,7 @@ class VisitPhoto { int photo_principale; String tags; int id_concurrence_lien; + bool depuis_galerie; VisitPhoto( {this.id = 0, @@ -31,7 +32,8 @@ class VisitPhoto { this.photo_principale = 0, this.tags = '', DateTime? date_photo, - this.id_concurrence_lien = 0}) + this.id_concurrence_lien = 0, + this.depuis_galerie = false}) : date_photo = date_photo ?? DateTime.now(); static String? _photosDir = SharedPrefs().photosDir; @@ -40,7 +42,10 @@ class VisitPhoto { if (_photosDir == null) { throw Exception('Photos directory not initialized'); } - return '$_photosDir/$image_name'; + if (depuis_galerie == false) { + return '$_photosDir/$image_name'; + } else + return image_name; } String get dateFormat => DateFormat('dd.MM.yyyy hh:mm:ss').format(date_photo); @@ -57,6 +62,7 @@ class VisitPhoto { int? photo_principale, String? tags, int? id_concurrence_lien, + bool? depuis_galerie, }) { return VisitPhoto( id: id ?? this.id, @@ -69,6 +75,7 @@ class VisitPhoto { photo_principale: photo_principale ?? this.photo_principale, tags: tags ?? this.tags, id_concurrence_lien: id_concurrence_lien ?? this.id_concurrence_lien, + depuis_galerie: depuis_galerie ?? this.depuis_galerie, ); } /* diff --git a/lib/main.dart b/lib/main.dart index 5988045..340183c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -43,9 +43,6 @@ Future main() async { objectbox = await ObjectBox.create(); - int objectboxAddress = identityHashCode(objectbox); - print(objectboxAddress); - /// Log objectbox.addLog('LOG', 'MOBDR', 'Ouverture application ', 0); diff --git a/lib/network/api_provider.dart b/lib/network/api_provider.dart index 604cfc6..2dcf0ea 100644 --- a/lib/network/api_provider.dart +++ b/lib/network/api_provider.dart @@ -183,6 +183,7 @@ class ApiProvider { String base64Photo = base64.encode(ReponseImg.data); /// create box user + objectbox.userBox.removeAll(); objectbox.addUSer( response.data['id_utilisateur'], response.data['login'], diff --git a/lib/objectbox-model.json b/lib/objectbox-model.json index 4086c7b..b7700fe 100644 --- a/lib/objectbox-model.json +++ b/lib/objectbox-model.json @@ -224,7 +224,7 @@ }, { "id": "13:6298506278273268036", - "lastPropertyId": "11:7594245284938827569", + "lastPropertyId": "13:3286310216758176958", "name": "VisitPhoto", "properties": [ { @@ -277,6 +277,11 @@ "id": "11:7594245284938827569", "name": "id_concurrence_lien", "type": 6 + }, + { + "id": "13:3286310216758176958", + "name": "depuis_galerie", + "type": 1 } ], "relations": [] @@ -463,7 +468,8 @@ 102253757473665009, 1526411175344533047, 1603887098520719919, - 427077651567855068 + 427077651567855068, + 7039119413270734559 ], "retiredRelationUids": [], "version": 1 diff --git a/lib/objectbox.dart b/lib/objectbox.dart index dc79fcb..34aef1b 100644 --- a/lib/objectbox.dart +++ b/lib/objectbox.dart @@ -54,27 +54,6 @@ class ObjectBox { PhotoTypologyBox = Box(store); visitPhotoBox = Box(store); logBox = Box(store); - - // Add some demo data if the box is empty. - if (noteBox.isEmpty()) { - _putDemoData(); - } - - userBox.removeAll(); - - /* - etabBox.removeAll(); - etabCompetitorBox.removeAll(); - visitBox.removeAll(); - visitTagBox.removeAll(); - visitPhotoBox.removeAll(); - PhotoTypologyBox.removeAll(); - */ - - // Add some demo data if the box is empty. - if (userBox.isEmpty()) { - _putUserAdminData(); - } } /// Create an instance of ObjectBox to use throughout the app. @@ -84,19 +63,9 @@ class ObjectBox { return ObjectBox._create(store); } - void _putDemoData() { - final demoNotes = [ - Note('Quickly add a note by writing text and pressing Enter'), - Note('Delete notes by tapping on one'), - Note('Write a demo app for ObjectBox') - ]; - store.runInTransactionAsync(TxMode.write, _putNotesInTx, demoNotes); - } - - void _putUserAdminData() { - //addUSer(0, 'root', 'admim', 'admin', ''); - } - + /** + * TODO: A SUPPRIMER + Box note + */ Stream> getNotes() { // Query for all notes, sorted by their date. // https://docs.objectbox.io/queries @@ -134,20 +103,14 @@ class ObjectBox { /// USER --------------------------------------------------------------------- /// - Future addUSer(int _id_utilisateur, String _login, String _nom, - String _prenom, String _photo) => - store.runInTransactionAsync( - TxMode.write, - _addUserInTx, - User( - id_utilisateur: _id_utilisateur, - login: _login, - nom: _nom, - prenom: _prenom, - photo: _photo)); - - static void _addUserInTx(Store store, _User) { - store.box().put(_User); + void addUSer(int _id_utilisateur, String _login, String _nom, String _prenom, + String _photo) { + userBox.put(User( + id_utilisateur: _id_utilisateur, + login: _login, + nom: _nom, + prenom: _prenom, + photo: _photo)); } String getUserAvatar(int id_utilisateur) { @@ -241,9 +204,23 @@ class ObjectBox { .query(EtabCompetitor_.id_etab.equals(_id_etab)) .order(EtabCompetitor_.nom) .build(); - final photoCompetitors = query.find(); + final etabCompetitors = query.find(); - return photoCompetitors.toList(); + return etabCompetitors.toList(); + } + + /// Retrieves a etab competitor object from the ObjectBox database with the specified ID. + /// + /// Parameters: + /// _id_concurrence_lien : The _id_concurrence_lien of the competitor to retrieve. + /// + /// Returns: + /// A Etab Competitor object, or null if no object is found. + EtabCompetitor? getEtabCompetitorByLink(int _id_concurrence_lien) { + final query = etabCompetitorBox + .query(EtabCompetitor_.id_concurrence_lien.equals(_id_concurrence_lien)) + .build(); + return query.findFirst(); } int getEtabCompetitorsCount() { @@ -598,42 +575,42 @@ class ObjectBox { } } - Future putPhotoTypologie(int photoId, int typologieId) async { + void putPhotoTypologie(int photoId, int typologieId) { final photo = visitPhotoBox.get(photoId); if (photo != null) { final updatedPhoto = photo.copyWith(id_photo_typologie: typologieId); - await visitPhotoBox.putAsync(updatedPhoto); + visitPhotoBox.put(updatedPhoto); } } - Future putPhotoTags(int photoId, List tags) async { + void putPhotoTags(int photoId, List tags) { final photo = visitPhotoBox.get(photoId); if (photo != null) { final updatedPhoto = photo.copyWith(tags: tags.join(",")); - await visitPhotoBox.putAsync(updatedPhoto); + visitPhotoBox.put(updatedPhoto); } } - Future putPhotoVisibilities( - int photoId, List visibilities) async { + void putPhotoVisibilities(int photoId, List visibilities) { final photo = visitPhotoBox.get(photoId); if (photo != null) { final updatedPhoto = photo.copyWith( photo_principale: visibilities.contains('principal') ? 1 : 0, photo_privee: visibilities.contains('private') ? 1 : 0); - await visitPhotoBox.putAsync(updatedPhoto); + + visitPhotoBox.put(updatedPhoto); } } - Future putPhotoCompetitor(int photoId, int competitorId) async { + void putPhotoCompetitor(int photoId, int competitorId) { final photo = visitPhotoBox.get(photoId); if (photo != null) { final updatedPhoto = photo.copyWith(id_concurrence_lien: competitorId); - await visitPhotoBox.putAsync(updatedPhoto); + visitPhotoBox.put(updatedPhoto); } } diff --git a/lib/objectbox.g.dart b/lib/objectbox.g.dart index 19c1604..2234bd5 100644 --- a/lib/objectbox.g.dart +++ b/lib/objectbox.g.dart @@ -249,7 +249,7 @@ final _entities = [ ModelEntity( id: const IdUid(13, 6298506278273268036), name: 'VisitPhoto', - lastPropertyId: const IdUid(11, 7594245284938827569), + lastPropertyId: const IdUid(13, 3286310216758176958), flags: 0, properties: [ ModelProperty( @@ -301,6 +301,11 @@ final _entities = [ id: const IdUid(11, 7594245284938827569), name: 'id_concurrence_lien', type: 6, + flags: 0), + ModelProperty( + id: const IdUid(13, 3286310216758176958), + name: 'depuis_galerie', + type: 1, flags: 0) ], relations: [], @@ -504,7 +509,8 @@ ModelDefinition getObjectBoxModel() { 102253757473665009, 1526411175344533047, 1603887098520719919, - 427077651567855068 + 427077651567855068, + 7039119413270734559 ], retiredRelationUids: const [], modelVersion: 5, @@ -761,7 +767,7 @@ ModelDefinition getObjectBoxModel() { objectToFB: (VisitPhoto object, fb.Builder fbb) { final image_nameOffset = fbb.writeString(object.image_name); final tagsOffset = fbb.writeString(object.tags); - fbb.startTable(12); + fbb.startTable(14); fbb.addInt64(0, object.id); fbb.addInt64(1, object.id_visite); fbb.addInt64(2, object.id_photo_typologie); @@ -772,6 +778,7 @@ ModelDefinition getObjectBoxModel() { fbb.addInt64(7, object.photo_principale); fbb.addOffset(8, tagsOffset); fbb.addInt64(10, object.id_concurrence_lien); + fbb.addBool(12, object.depuis_galerie); fbb.finish(fbb.endTable()); return object.id; }, @@ -798,7 +805,9 @@ ModelDefinition getObjectBoxModel() { date_photo: DateTime.fromMillisecondsSinceEpoch( const fb.Int64Reader().vTableGet(buffer, rootOffset, 12, 0)), id_concurrence_lien: - const fb.Int64Reader().vTableGet(buffer, rootOffset, 24, 0)); + const fb.Int64Reader().vTableGet(buffer, rootOffset, 24, 0), + depuis_galerie: const fb.BoolReader() + .vTableGet(buffer, rootOffset, 28, false)); return object; }), @@ -1081,6 +1090,10 @@ class VisitPhoto_ { /// see [VisitPhoto.id_concurrence_lien] static final id_concurrence_lien = QueryIntegerProperty(_entities[6].properties[9]); + + /// see [VisitPhoto.depuis_galerie] + static final depuis_galerie = + QueryBooleanProperty(_entities[6].properties[10]); } /// [Visit] entity fields to define ObjectBox queries. diff --git a/lib/service/shared_prefs.dart b/lib/service/shared_prefs.dart index ba5e86a..6befde2 100644 --- a/lib/service/shared_prefs.dart +++ b/lib/service/shared_prefs.dart @@ -89,18 +89,11 @@ class SharedPrefs { _sharedPrefs.setString('key_photo', value); } - /// get/set id_distrib - int get id_distrib => _sharedPrefs.getInt('key_id_distrib') ?? 0; + /// get/set last id_visite + int get last_id_visite => _sharedPrefs.getInt('key_last_id_visite') ?? 0; - set id_distrib(int value) { - _sharedPrefs.setInt('key_id_distrib', value); - } - - /// get/set id_visite - int get id_visite => _sharedPrefs.getInt('key_id_visite') ?? 0; - - set id_visite(int value) { - _sharedPrefs.setInt('key_id_visite', value); + set last_id_visite(int value) { + _sharedPrefs.setInt('key_last_id_visite', value); } /// get/set isSimulator diff --git a/lib/ui/home.dart b/lib/ui/home.dart index 7d1704f..b7a46cd 100644 --- a/lib/ui/home.dart +++ b/lib/ui/home.dart @@ -50,7 +50,6 @@ class _HomePageState extends State } void _handleTabSelection() { - // TODO a voir si on laisse setState ?? /* setState(() { }); @@ -99,7 +98,7 @@ class _HomePageState extends State icon: Icon(Icons.home, color: _currentIndex == 0 ? PRIMARY_COLOR : CHARCOAL)), BottomNavigationBarItem( - label: 'Visits', + label: 'Synchro', icon: Icon(Icons.sync, color: _currentIndex == 1 ? ASSENT_COLOR : CHARCOAL)), BottomNavigationBarItem( diff --git a/lib/ui/home/tab_home.dart b/lib/ui/home/tab_home.dart index ac3356a..183a35e 100644 --- a/lib/ui/home/tab_home.dart +++ b/lib/ui/home/tab_home.dart @@ -6,6 +6,7 @@ we used AutomaticKeepAliveClientMixin to keep the state when moving from 1 navba import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:badges/badges.dart' as badges; @@ -43,13 +44,13 @@ class _TabHomePageState extends State String defaultLang = 'en'; - late LanguageCubit _languageCubit; + DateTime _todayDate = DateTime.now(); - bool _isLoading = true; - String _errorMessage = ''; + late LanguageCubit _languageCubit; late List todayVisitData = []; late List previousVisitData = []; + VisitModel? lastVisitData = null; @override void initState() { @@ -65,21 +66,22 @@ class _TabHomePageState extends State loadData().then((_) { setState(() { - _isLoading = false; - // Listen particular event subVisitPhotoCountEvent = eventBus.on().listen((e) { + SharedPrefs().last_id_visite = e.id_visite; setState(() { for (int i = 0; i < todayVisitData.length; i++) { if (todayVisitData[i].id_visite == e.id_visite) { todayVisitData[i].photoCount = e.photoCount; + lastVisitData = todayVisitData[i]; break; } } for (int i = 0; i < previousVisitData.length; i++) { if (previousVisitData[i].id_visite == e.id_visite) { previousVisitData[i].photoCount = e.photoCount; + lastVisitData = previousVisitData[i]; break; } } @@ -114,9 +116,9 @@ class _TabHomePageState extends State // if we used AutomaticKeepAliveClientMixin, we must call super.build(context); super.build(context); final double boxImageSize = (MediaQuery.of(context).size.width / 3); - if (_isLoading) { - return Center(child: CircularProgressIndicator()); - } + final formattedDate = + DateFormat('EEEE d MMMM y', 'fr_FR').format(_todayDate); + return Scaffold( appBar: AppBar( automaticallyImplyLeading: false, @@ -149,22 +151,93 @@ class _TabHomePageState extends State body: ListView( physics: AlwaysScrollableScrollPhysics(), children: [ + Padding( + padding: EdgeInsets.all(16.0), + child: Row( + children: [ + Icon(Icons.calendar_today), + SizedBox(width: 8.0), + Text( + formattedDate, + style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold), + ), + ], + ), + ), + _buildLastVisit(boxImageSize), _buildTodayVisits(boxImageSize), _builPreviousVisits(boxImageSize), + ElevatedButton( + onPressed: () async { + final result = await Navigator.push( + context, + MaterialPageRoute(builder: (context) => SyncCalendarPage()), + ); + + // Refresh the widget if the synchronization was successful. + if (result == true) { + setState(() { + loadData(); + }); + } + }, + child: Text('Synchronisation'), + ) ], ), ); } + Widget _buildLastVisit(boxImageSize) { + if (lastVisitData == null) { + return SizedBox.shrink(); // Rien ne sera affiché + } else { + return Column( + children: [ + Container( + padding: EdgeInsets.fromLTRB(16, 16, 16, 0), + child: Row( + children: [ + Text('Last visit access', style: GlobalStyle.horizontalTitle), + ], + ), + ), + Container( + margin: EdgeInsets.only(top: 8), + height: boxImageSize * GlobalStyle.cardHeightMultiplication, + alignment: Alignment.centerLeft, + padding: EdgeInsets.symmetric(horizontal: 12), + child: buildHorizontalVisitListCard(context, lastVisitData)), + ], + ); + } + } + Widget _buildTodayVisits(boxImageSize) { return Column( children: [ Container( padding: EdgeInsets.fromLTRB(16, 16, 16, 0), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Today Visits', style: GlobalStyle.horizontalTitle), + Container( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 2.0, horizontal: 8.0), + child: badges.Badge( + badgeStyle: badges.BadgeStyle( + badgeColor: Colors.black, + padding: + EdgeInsets.all(todayVisitData.length >= 10 ? 2 : 6), + ), + badgeContent: Text( + todayVisitData.length.toString(), + style: TextStyle(color: Colors.white), + ), + ), + ), + ), ], ), ), @@ -175,11 +248,18 @@ class _TabHomePageState extends State SizedBox(height: 16), // Ajout de l'espace ici Text('Aucune visite ce jour'), ElevatedButton( - onPressed: () { - Navigator.push( + onPressed: () async { + final result = await Navigator.push( context, MaterialPageRoute(builder: (context) => SyncCalendarPage()), ); + + // Refresh the widget if the synchronization was successful. + if (result == true) { + setState(() { + loadData(); + }); + } }, child: Text('Synchroniser'), ), @@ -329,13 +409,26 @@ class _TabHomePageState extends State /// Initializes data when the page loads. Future loadData() async { - try { - // visite model initialisation - todayVisitData = await VisitModel.getTodayVisit(); - previousVisitData = await VisitModel.getPreviousVisit(); - } catch (e) { - // set errorMessage for debug - _errorMessage = 'Error loading visites : $e'; + // visite model initialisation + todayVisitData = await VisitModel.getTodayVisit(); + previousVisitData = await VisitModel.getPreviousVisit(); + + // Search for the visit matching last_id_visit + int lastIdVisite = SharedPrefs().last_id_visite; + + lastVisitData = null; + + if (lastIdVisite > 0) { + for (var visit in todayVisitData) { + if (visit.id_visite == lastIdVisite) { + lastVisitData = visit; + break; + } + } + + if (lastVisitData == null) { + SharedPrefs().last_id_visite = 0; + } } } } diff --git a/lib/ui/reusable/reusable_widget.dart b/lib/ui/reusable/reusable_widget.dart index 37695ec..48a3c8c 100644 --- a/lib/ui/reusable/reusable_widget.dart +++ b/lib/ui/reusable/reusable_widget.dart @@ -1,12 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:mobdr/main.dart'; -import 'package:mobdr/events.dart'; -import 'package:mobdr/config/global_style.dart'; import 'package:mobdr/config/constant.dart'; -import 'package:mobdr/service/shared_prefs.dart'; -import 'package:mobdr/ui/reusable/cache_image_network.dart'; //TODO Rechercher toutes les utilisations diff --git a/lib/ui/splash_screen.dart b/lib/ui/splash_screen.dart index 5044274..32932a2 100644 --- a/lib/ui/splash_screen.dart +++ b/lib/ui/splash_screen.dart @@ -6,7 +6,7 @@ import 'package:flutter/services.dart'; import 'package:mobdr/service/shared_prefs.dart'; import 'package:mobdr/config/constant.dart'; import 'package:mobdr/ui/onboarding.dart'; -import 'package:mobdr/ui/authentication/signin.dart'; +import 'package:mobdr/ui/home.dart'; class SplashScreenPage extends StatefulWidget { @override @@ -28,14 +28,11 @@ class _SplashScreenPageState extends State { if (SharedPrefs().onboarding == 0) { SharedPrefs().onboarding = 1; - - // for this example we will use pushReplacement because we want to go back to the list Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => OnBoardingPage())); } else { - // for this example we will use pushReplacement because we want to go back to the list Navigator.pushReplacement( - context, MaterialPageRoute(builder: (context) => SigninPage())); + context, MaterialPageRoute(builder: (context) => HomePage())); } // if you use this splash screen on the very first time when you open the page, use below code //Navigator.of(context).pushAndRemoveUntil(MaterialPageRoute(builder: (context) => OnBoardingPage()), (Route route) => false); diff --git a/lib/ui/sync/check_connection.dart b/lib/ui/sync/check_connection.dart new file mode 100644 index 0000000..a5b059d --- /dev/null +++ b/lib/ui/sync/check_connection.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; + +class CheckConnectionPage extends StatefulWidget { + final Widget redirectPage; + + const CheckConnectionPage({Key? key, required this.redirectPage}) + : super(key: key); + + @override + _CheckConnectionPageState createState() => _CheckConnectionPageState(); +} + +class _CheckConnectionPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Mayday ..."), + backgroundColor: + Colors.red, // Définir la couleur de fond de l'appbar en rouge + ), + body: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/images/no_connection.png', + width: 250, + ), + SizedBox(height: 32), + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "No internet connection", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + SizedBox(height: 10), + Center( + child: Text( + "You are not connected. Check your connection.", + style: TextStyle(fontSize: 14), + textAlign: TextAlign.center, + ), + ), + ], + ), + SizedBox(height: 32), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + ), + child: const Text("Check Connection"), + onPressed: () async { + final connectivityResult = + await Connectivity().checkConnectivity(); + if (connectivityResult == ConnectivityResult.none) { + await showDialog( + barrierDismissible: false, + context: context, + builder: (_) => NetworkErrorDialog( + onRetryPressed: () async { + final connectivityResult = + await Connectivity().checkConnectivity(); + if (connectivityResult == ConnectivityResult.none) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Please turn on your wifi or mobile data'))); + } else { + Navigator.pop(context); + // redirect to UploadPhotosPage here + } + }, + ), + ); + } else { + // redirect to UploadPhotosPage here + } + }, + ), + ], + ), + ), + ); + } +} + +class NetworkErrorDialog extends StatelessWidget { + const NetworkErrorDialog({Key? key, this.onRetryPressed}) : super(key: key); + + final VoidCallback? onRetryPressed; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('No Internet Connection'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.error, color: Colors.red, size: 48), + const SizedBox(height: 16), + const Text( + 'Please check your internet connection and try again.', + textAlign: TextAlign.center, + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Go Back'), + ), + TextButton( + onPressed: onRetryPressed, + child: const Text('Retry'), + ), + ], + ); + } +} diff --git a/lib/ui/sync/sync_calendar.dart b/lib/ui/sync/sync_calendar.dart index a354f6c..3caf655 100644 --- a/lib/ui/sync/sync_calendar.dart +++ b/lib/ui/sync/sync_calendar.dart @@ -41,7 +41,7 @@ class _SyncCalendarPageState extends State { void _popScreen() { if (mounted) { - Navigator.of(context).pop(); + Navigator.of(context).pop(_syncSuccessful); } } diff --git a/lib/ui/sync/upload_photos.dart b/lib/ui/sync/upload_photos.dart index b84ef8d..c6a5002 100644 --- a/lib/ui/sync/upload_photos.dart +++ b/lib/ui/sync/upload_photos.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; diff --git a/lib/ui/visit/tab_visit.dart b/lib/ui/visit/tab_visit.dart index b96c029..aeb7031 100644 --- a/lib/ui/visit/tab_visit.dart +++ b/lib/ui/visit/tab_visit.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:mobdr/main.dart'; import 'package:mobdr/config/constant.dart'; @@ -9,6 +10,7 @@ import 'package:mobdr/model/visit_model.dart'; import 'package:mobdr/ui/visit/visit_photo_typology.dart'; import 'package:mobdr/ui/reusable/cache_image_network.dart'; import 'package:mobdr/ui/sync/upload_photos.dart'; +import 'package:mobdr/ui/sync/check_connection.dart'; class TabVisitListPage extends StatefulWidget { @override @@ -17,9 +19,6 @@ class TabVisitListPage extends StatefulWidget { class _TabVisitListPageState extends State with AutomaticKeepAliveClientMixin { - // _listKey is used for AnimatedList - final GlobalKey _listKey = GlobalKey(); - // keep the state to do not refresh when switch navbar @override bool get wantKeepAlive => true; @@ -31,6 +30,9 @@ class _TabVisitListPageState extends State late StreamSubscription subVisitPhotoCountEvent; + // _listKey is used for AnimatedList + var _listKey = GlobalKey(); + @override void initState() { super.initState(); @@ -73,28 +75,57 @@ class _TabVisitListPageState extends State ); } return Scaffold( - appBar: AppBar( - iconTheme: IconThemeData( - color: GlobalStyle.appBarIconThemeColor, - ), - elevation: GlobalStyle.appBarElevation, - title: Text( - 'Visit List', - style: GlobalStyle.appBarTitle, - ), - backgroundColor: GlobalStyle.appBarBackgroundColor, - systemOverlayStyle: GlobalStyle.appBarSystemOverlayStyle), - body: Column(children: [ + appBar: AppBar( + iconTheme: IconThemeData( + color: GlobalStyle.appBarIconThemeColor, + ), + elevation: GlobalStyle.appBarElevation, + title: Text( + 'Synchronization', + style: GlobalStyle.appBarTitle, + ), + backgroundColor: GlobalStyle.appBarBackgroundColor, + systemOverlayStyle: GlobalStyle.appBarSystemOverlayStyle), + body: Column( + children: [ Flexible( - child: AnimatedList( - key: _listKey, - initialItemCount: modelData.length, - physics: AlwaysScrollableScrollPhysics(), - itemBuilder: (context, index, animation) { - return _buildVisitelistCard( - modelData[index], boxImageSize, animation, index); - }, - )), + child: AnimatedList( + key: _listKey, + initialItemCount: modelData.length, + physics: AlwaysScrollableScrollPhysics(), + itemBuilder: (context, index, animation) { + return Dismissible( + key: UniqueKey(), + direction: DismissDirection.endToStart, + onDismissed: (direction) { + // the photo must be removed + setState(() { + modelData.removeAt(index); + _listKey = GlobalKey(); + }); + }, + background: Container( + color: Colors.red, + child: Stack( + children: [ + Positioned.fill( + child: Align( + alignment: Alignment.center, + child: Icon( + Icons.delete, + color: Colors.white, + ), + ), + ), + ], + ), + ), + child: _buildVisitelistCard( + modelData[index], boxImageSize, animation, index), + ); + }, + ), + ), Container( padding: EdgeInsets.all(12), decoration: BoxDecoration( @@ -114,11 +145,11 @@ class _TabVisitListPageState extends State onTap: () { // TODO functionality to be implemented /*` - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ChatUsPage())); - */ + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChatUsPage())); + */ }, child: ClipOval( child: Container( @@ -135,10 +166,7 @@ class _TabVisitListPageState extends State Expanded( child: GestureDetector( onTap: () { - Route route = MaterialPageRoute( - builder: (context) => UploadPhotosPage(pp_id_visite: 0), - ); - Navigator.push(context, route); + navigateToPage(context, 0); }, child: Container( alignment: Alignment.center, @@ -146,20 +174,24 @@ class _TabVisitListPageState extends State margin: EdgeInsets.only(right: 8), decoration: BoxDecoration( color: Colors.white, - border: Border.all(width: 1, color: SOFT_BLUE), - borderRadius: BorderRadius.all(Radius.circular( - 10) // <--- border radius here - )), - child: Text('Synchronize ALL', - style: TextStyle( - color: SOFT_BLUE, fontWeight: FontWeight.bold)), + border: Border.all(width: 1, color: Colors.red), + borderRadius: BorderRadius.all( + Radius.circular(10), + )), + child: Text( + 'Synchronize ALL visits', + style: TextStyle( + color: Colors.red, fontWeight: FontWeight.bold), + ), ), ), ), ], ), - ) - ])); + ), + ], + ), + ); } Widget _buildVisitelistCard(VisitModel data, boxImageSize, animation, index) { @@ -283,11 +315,7 @@ class _TabVisitListPageState extends State )) : OutlinedButton( onPressed: () { - Route route = MaterialPageRoute( - builder: (context) => UploadPhotosPage( - pp_id_visite: data.id_visite), - ); - Navigator.push(context, route); + navigateToPage(context, data.id_visite); }, style: ButtonStyle( minimumSize: MaterialStateProperty.all( @@ -324,6 +352,26 @@ class _TabVisitListPageState extends State ); } + Future navigateToPage(BuildContext context, int id_visite) async { + var connectivityResult = await (Connectivity().checkConnectivity()); + + if (connectivityResult == ConnectivityResult.none) { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => CheckConnectionPage( + redirectPage: UploadPhotosPage(pp_id_visite: id_visite), + ), + ), + ); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => UploadPhotosPage(pp_id_visite: id_visite))); + } + } + /// Initializes data when the page loads. Future loadData() async { try { diff --git a/lib/ui/visit/visit_photo_tag.dart b/lib/ui/visit/visit_photo_tag.dart index f29c20a..b4c4810 100644 --- a/lib/ui/visit/visit_photo_tag.dart +++ b/lib/ui/visit/visit_photo_tag.dart @@ -28,8 +28,6 @@ class PhotoTagPage extends StatefulWidget { } class _PhotoTagPageState extends State { - final GlobalKey _formKey = GlobalKey(); - late List allTagsList = []; late List _selectedTags = []; bool isLoading = true; diff --git a/lib/ui/visit/visit_photo_typology_detail.dart b/lib/ui/visit/visit_photo_typology_detail.dart index 0cb563e..5bd0382 100644 --- a/lib/ui/visit/visit_photo_typology_detail.dart +++ b/lib/ui/visit/visit_photo_typology_detail.dart @@ -40,17 +40,17 @@ class _VisitPhotoTypologyDetailPageState String _errorMessage = ''; // Typology list - late List _typologiesList = []; - int _typologyIndex = 0; + late List _photoTypologiesList = []; + int _photoTypologyIndex = 0; List _visibilities = []; late List tagList = []; - late VisitPhoto _photo; + late VisitPhoto _visitPhoto; // competitors - List _competitorsList = []; - String _competitor = ''; + List _etabCompetitorsList = []; + String _etabCompetitor = ''; @override void initState() { @@ -104,8 +104,8 @@ class _VisitPhotoTypologyDetailPageState onWillPop: () { // fred Map result = { - 'change_typologie': _typologyIndex != - _typologiesList.indexWhere((typology) => + 'change_typologie': _photoTypologyIndex != + _photoTypologiesList.indexWhere((typology) => typology.id_photo_typologie == widget.pp_id_typologie), 'tags': tagList.join(","), 'photo_principale': _visibilities.contains('principal') ? 1 : 0, @@ -213,8 +213,8 @@ class _VisitPhotoTypologyDetailPageState height: 16, ), Wrap( - children: List.generate(_typologiesList.length, (index) { - return radioSize(_typologiesList[index].libelle, index); + children: List.generate(_photoTypologiesList.length, (index) { + return radioSize(_photoTypologiesList[index].libelle, index); }), ), ], @@ -225,27 +225,29 @@ class _VisitPhotoTypologyDetailPageState return GestureDetector( onTap: () async { setState(() { - _typologyIndex = index; + _photoTypologyIndex = index; }); // save photo typology in the database objectbox.putPhotoTypologie( - widget.pp_imageId, _typologiesList[index].id_photo_typologie); + widget.pp_imageId, _photoTypologiesList[index].id_photo_typologie); }, child: Container( padding: EdgeInsets.fromLTRB(12, 8, 12, 8), margin: EdgeInsets.only(right: 8, top: 8), decoration: BoxDecoration( - color: _typologyIndex == index ? SOFT_BLUE : Colors.white, + color: _photoTypologyIndex == index ? SOFT_BLUE : Colors.white, border: Border.all( width: 1, - color: _typologyIndex == index ? SOFT_BLUE : Colors.grey[300]!), + color: _photoTypologyIndex == index + ? SOFT_BLUE + : Colors.grey[300]!), borderRadius: BorderRadius.all( Radius.circular(10) // <--- border radius here )), child: Text(txt, style: TextStyle( - color: _typologyIndex == index ? Colors.white : CHARCOAL)), + color: _photoTypologyIndex == index ? Colors.white : CHARCOAL)), ), ); } @@ -338,7 +340,7 @@ class _VisitPhotoTypologyDetailPageState builder: (context) => PhotoTagPage( pp_langage: widget.pp_visitModel.langage, pp_id_distrib: widget.pp_visitModel.id_distrib, - pp_photoId: this._photo.id, + pp_photoId: this._visitPhoto.id, pp_currentTags: tagList), ), ); @@ -412,7 +414,7 @@ class _VisitPhotoTypologyDetailPageState borderRadius: BorderRadius.all( Radius.circular(10) // <--- border radius here )), - child: _competitor == '' + child: _etabCompetitor == '' ? Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -435,7 +437,7 @@ class _VisitPhotoTypologyDetailPageState Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(_competitor, + Text(_etabCompetitor, style: TextStyle( color: CHARCOAL, fontWeight: FontWeight.bold)), @@ -473,19 +475,19 @@ class _VisitPhotoTypologyDetailPageState Flexible( child: ListView.builder( padding: EdgeInsets.all(16), - itemCount: _competitorsList.length, + itemCount: _etabCompetitorsList.length, itemBuilder: (BuildContext context, int index) { - EtabCompetitor competitor = _competitorsList[index]; + EtabCompetitor competitor = _etabCompetitorsList[index]; return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { setState(() { - _competitor = competitor.nom; + _etabCompetitor = competitor.nom; }); // save photo competitor in the database - objectbox.putPhotoCompetitor(widget.pp_visitModel.id, - competitor.id_concurrence_lien); + objectbox.putPhotoCompetitor( + widget.pp_imageId, competitor.id_concurrence_lien); Navigator.pop(context); }, @@ -513,26 +515,29 @@ class _VisitPhotoTypologyDetailPageState try { // photo typologies initialization - _typologiesList = objectbox.getPhotoTypologiesList(); + _photoTypologiesList = objectbox.getPhotoTypologiesList(); - _typologyIndex = _typologiesList.indexWhere( + _photoTypologyIndex = _photoTypologiesList.indexWhere( (typology) => typology.id_photo_typologie == widget.pp_id_typologie); // get photo object - _photo = objectbox.getPhotoById(photoId)!; + _visitPhoto = objectbox.getPhotoById(photoId)!; // visibilities initialization - if (_photo.photo_privee == 1) _visibilities.add('private'); - if (_photo.photo_principale == 1) _visibilities.add('principal'); + if (_visitPhoto.photo_privee == 1) _visibilities.add('private'); + if (_visitPhoto.photo_principale == 1) _visibilities.add('principal'); // photo tag initialization - tags = _photo.tags; - tagList = tags.isEmpty ? [] : _photo.tags.split(","); + tags = _visitPhoto.tags; + tagList = tags.isEmpty ? [] : _visitPhoto.tags.split(","); // competitor initialization - _competitorsList = + _etabCompetitorsList = objectbox.getEtabCompetitorList(widget.pp_visitModel.id_etab); - _competitor = ""; + + final etabCompetitor = + objectbox.getEtabCompetitorByLink(_visitPhoto.id_concurrence_lien); + _etabCompetitor = etabCompetitor != null ? etabCompetitor.nom : ""; } catch (e) { // set errorMessage for debug _errorMessage = 'Error loading photo: $e'; diff --git a/lib/ui/visit/visit_photo_typology_list.dart b/lib/ui/visit/visit_photo_typology_list.dart index 00ad398..583010d 100644 --- a/lib/ui/visit/visit_photo_typology_list.dart +++ b/lib/ui/visit/visit_photo_typology_list.dart @@ -8,6 +8,7 @@ import 'package:flutter/services.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:image/image.dart' as img; +import 'package:image_picker/image_picker.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; @@ -138,20 +139,19 @@ class _VisitPhotoTypologyListPageState Container( child: GestureDetector( onTap: () { - // TODO functionality to be implemented - /*` - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ChatUsPage())); - */ + ImportImageFromGallery(); + /*Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PhotoPickPage())); + */ }, child: ClipOval( child: Container( color: SOFT_BLUE, padding: EdgeInsets.all(9), - child: - Icon(Icons.chat, color: Colors.white, size: 16)), + child: Icon(Icons.photo_library, + color: Colors.white, size: 16)), ), ), ), @@ -443,7 +443,6 @@ class _VisitPhotoTypologyListPageState ); } - // TODO ƒuture void ? void loadData() { _visitPhotoData = objectbox.getAllVisitTypologyPhotos( widget.pp_visitModel.id_visite, widget.pp_id_typologie); @@ -474,6 +473,7 @@ class _VisitPhotoTypologyListPageState _listPhotos.add(VisitPhoto( id_visite: widget.pp_visitModel.id_visite, id_photo_typologie: widget.pp_id_typologie, + depuis_galerie: false, image_name: myPhoto.path.split('/').last)); } @@ -542,8 +542,11 @@ class _VisitPhotoTypologyListPageState // delete file on database objectbox.delPhotoByName(removedItem.image_name); - // delete file on local storage - deleteFile(new File(removedItem.getImage())); + // if the photo is not from the gallery + if (removedItem.depuis_galerie == false) { + // delete file on local storage + deleteFile(new File(removedItem.getImage())); + } // This builder is just so that the animation has something // to work with before it disappears from view since the original @@ -650,4 +653,47 @@ class _VisitPhotoTypologyListPageState return tempFile; } + + Future ImportImageFromGallery() async { + try { + // get images from gallery + List? images = await ImagePicker().pickMultiImage(); + + // if images have been selected + if (images.length > 0) { + final List _listPhotos = []; + + for (var image in images) { + // to insert into database + _listPhotos.add(VisitPhoto( + id_visite: widget.pp_visitModel.id_visite, + id_photo_typologie: widget.pp_id_typologie, + depuis_galerie: true, + image_name: image.path)); + } + + // insert photo(s) in database (async) + final addedPhotos = await objectbox.addPhotos(_listPhotos); + + // insert photo(s) in widget at the beginning (0) + _visitPhotoData.insertAll(0, addedPhotos); + + // if new photos are taken + if (addedPhotos.length > 0) { + visitPhotoCount += addedPhotos.length; + // a global refresh event is sent + eventBus.fire(VisitPhotoCountEvent( + widget.pp_visitModel.id_visite, visitPhotoCount)); + } + + // refresh widget + setState(() { + _listKey = GlobalKey(); + }); + } + } catch (e) { + // Gestion de l'erreur + print('Erreur lors de la sélection d\'images : $e'); + } + } } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 83c4826..4464393 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import connectivity_plus import device_info_plus import objectbox_flutter_libs import package_info_plus @@ -14,6 +15,7 @@ import sqflite import wakelock_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) ObjectboxFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "ObjectboxFlutterLibsPlugin")) FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 6f91b2a..a4c58d5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -241,6 +241,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: b74247fad72c171381dbe700ca17da24deac637ab6d43c343b42867acb95c991 + url: "https://pub.dev" + source: hosted + version: "3.0.6" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a + url: "https://pub.dev" + source: hosted + version: "1.2.4" convert: dependency: transitive description: @@ -297,6 +313,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.4" + dbus: + dependency: transitive + description: + name: dbus + sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" + url: "https://pub.dev" + source: hosted + version: "0.7.8" debounce_throttle: dependency: transitive description: @@ -529,10 +553,10 @@ packages: dependency: "direct main" description: name: image_picker - sha256: "64b21d9f0e065f9ab0e4dde458076226c97382cc0c6949144cb874c62bf8e9f8" + sha256: "3da954c3b8906d82ecb50fd5e2b5401758f06d5678904eed6cbc06172283a263" url: "https://pub.dev" source: hosted - version: "0.8.7" + version: "0.8.7+4" image_picker_android: dependency: transitive description: @@ -653,6 +677,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" numerus: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7d0c82f..a019ecd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,7 +51,8 @@ dependencies: wakelock: ^0.6.2 shimmer: 2.0.0 - image_picker: ^0.8.7 + # https://pub.dev/packages/image_picker + image_picker: ^0.8.7+4 # https://pub.dev/packages/camera camera: ^0.10.4 @@ -89,6 +90,9 @@ dependencies: # https://pub.dev/packages/badges badges: ^3.1.1 + + # https://pub.dev/packages/connectivity_plus + connectivity_plus: ^3.0.6 dev_dependencies: flutter_test: @@ -165,6 +169,7 @@ flutter: - assets/images/process_timeline/status4.png - assets/images/process_timeline/status5.png - assets/images/simulator.jpeg + - assets/images/no_connection.png - assets/lang/fr.json - assets/lang/en.json diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 2ef7154..be59d5c 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,10 +6,13 @@ #include "generated_plugin_registrant.h" +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + ConnectivityPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); ObjectboxFlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ObjectboxFlutterLibsPlugin")); PermissionHandlerWindowsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index c46e00f..7121101 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + connectivity_plus objectbox_flutter_libs permission_handler_windows )