import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'dart:io'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:mobdr/config/global_style.dart'; import 'package:mobdr/main.dart'; import 'package:mobdr/events.dart'; import 'package:mobdr/ui/reusable/reusable_widget.dart'; import 'package:mobdr/ui/appstore/updatecheck.dart'; import 'package:mobdr/ui/authentication/signin.dart'; import 'package:mobdr/service/shared_prefs.dart'; import 'package:mobdr/network/api_provider.dart'; import 'package:mobdr/db/box_visit_photo.dart'; import 'package:mobdr/service/logger_util.dart'; import 'package:mobdr/cubit/language/app_localizations.dart'; class SynchronizationPage extends StatefulWidget { @override _SynchronizationPageState createState() => _SynchronizationPageState(); } class _SynchronizationPageState extends State with SingleTickerProviderStateMixin { // initialize reusable widget final _reusableWidget = ReusableWidget(); late AnimationController _animationController; late Animation _rotationAnimation; late ApiProvider _apiProvider; bool _isSyncing = false; bool _isInternetConnexion = true; bool _syncCompleted = false; bool _backofficeSyncCompleted = false; bool _photosSyncCompleted = false; bool _logSyncCompleted = false; bool _syncLog = false; @override void initState() { super.initState(); _apiProvider = ApiProvider(); _animationController = AnimationController( vsync: this, duration: Duration(seconds: 2), ); _rotationAnimation = Tween(begin: 0, end: 1).animate(_animationController); } @override void dispose() { _animationController.dispose(); _apiProvider.close(); super.dispose(); } void doSync() async { var connectivityResult = await (Connectivity().checkConnectivity()); // if no internet connection if (connectivityResult == ConnectivityResult.none) { // log tracker LoggerUtil.dblog('ERR', 'MOBDR', 'Pas de connexion internet', 0); setState(() { _isInternetConnexion = false; }); } else { _isInternetConnexion = true; // check if the guid is valid if (await _apiProvider.guidActif() == true) { // synchronization start await startSync(); // check is new version is available if (await _apiProvider.checkAppVersion()) { // Display the update message showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text(AppLocalizations.of(context)!.translate('i18n_label_new_version')), content: Text(AppLocalizations.of(context)!.translate('i18n_label_new_version_install')), actions: [ TextButton( child: Text(AppLocalizations.of(context)!.translate('i18n_label_no')), onPressed: () { Navigator.pop(context); // Closes the dialog box }, ), TextButton( child: Text(AppLocalizations.of(context)! .translate('i18n_label_yes')), onPressed: () { Navigator.pop(context); // Closes the dialog box Navigator.push( context, MaterialPageRoute( builder: (context) => UpdateCheckPage()), ); }, ), ], ); }, ); } } else { Navigator.push( context, MaterialPageRoute(builder: (context) => SigninPage()), ); } } } Future _uploadVisitPhotos() async { List _visitPhotosList = objectbox.getAllVisitPhotos(); int _totalUploaded = 0; // parse all photos for (var photo in _visitPhotosList) { int id_photo_mp4 = -1; bool isUpdatePhotoTypologie = true; bool isUpdatePhotoVisibility = true; bool isUpdatePhotoCompetitor = true; bool isUpdatePhotoTags = true; // if photo not already uploaded if (photo.id_photo_mp4 <= 0) { // try upload the photo id_photo_mp4 = await _apiProvider.uploadPhotoServlet( photo.id_visite, photo.getImage()); } else id_photo_mp4 = photo.id_photo_mp4; // the photo is saved in MP4 if (id_photo_mp4 > 0) { // update photo typology isUpdatePhotoTypologie = await _apiProvider.updatePhotoTypology( photo.id_visite, id_photo_mp4, photo.id_photo_typologie); // update photo visibility if (photo.photo_principale == 1 || photo.photo_privee == 1) { isUpdatePhotoVisibility = await _apiProvider.updatePhotoVisibility( photo.id_visite, id_photo_mp4, photo.photo_principale, photo.photo_privee); } // update photo tags if (photo.tags.isNotEmpty) { isUpdatePhotoTags = await _apiProvider.updatePhotoTags( photo.id_visite, id_photo_mp4, photo.tags); } // update photo competitor if (photo.id_concurrence_lien > 0) { isUpdatePhotoCompetitor = await _apiProvider.updatePhotoConpetitor( photo.id_visite, id_photo_mp4, photo.id_concurrence_lien); } } if (id_photo_mp4 > 0 && isUpdatePhotoTypologie == true && isUpdatePhotoVisibility == true && isUpdatePhotoTags == true && isUpdatePhotoCompetitor == true) { // delete photo in database objectbox.delPhotoById(photo.id); // delete the file File file = File(photo.getImage()); if (await file.exists()) { await file.delete(); } _totalUploaded++; } else { // if successful upload if (id_photo_mp4 != -1) { photo.id_photo_mp4 = id_photo_mp4; // save MP4 id in database objectbox.putPhotoIdMP4(photo.id, id_photo_mp4); } } } _photosSyncCompleted = (_totalUploaded == _visitPhotosList.length); } Future _cleanVisitPhotoDir() async { List _visitPhotoListTokeep; Directory photosDir = Directory(SharedPrefs().photosDir); _visitPhotoListTokeep = objectbox.getAllVisitPhotos(); // Get a list of all files in the "photos" directory final List files = await photosDir.list().toList(); // Check each file in the directory for (FileSystemEntity file in files) { if (file is File) { // Extract the file name from the file path String fileName = file.path.split('/').last; // Check if the file exists in the _visitPhotoListTokeep bool existsInList = _visitPhotoListTokeep .any((visitPhoto) => visitPhoto.image_name == fileName); if (!existsInList) { // Delete the file if it doesn't exist in the _visitPhotoListTokeep await file.delete(); print('Deleted file: $fileName'); } } } } Future _clearPhotoCache() async { Directory cacheDir = Directory(SharedPrefs().cacheDir); // Get a list of all files in the "cache" directory final List files = await cacheDir.list().toList(); // Check each file in the directory for (FileSystemEntity file in files) { // Get the file name String fileName = file.path.split('/').last; // Check if the file name starts with "CAP" if (fileName.startsWith("CAP")) { // Delete the file await file.delete(); print('Deleted file: $fileName'); } } } Future startSync() async { // disable navigation bar buttons eventBus.fire(SynchronizationEvent(true)); Stopwatch stopwatch = Stopwatch()..start(); setState(() { _isSyncing = true; _syncCompleted = false; _backofficeSyncCompleted = false; _photosSyncCompleted = false; _logSyncCompleted = false; }); _animationController.repeat(); // log tracker LoggerUtil.dblog('LOG', 'MOBDR', 'Nombre Photo(s): ' + objectbox.getVisitPhotoCount().toString(), 0); if (_syncLog) { // log tracker LoggerUtil.dblog('LOG', 'MOBDR', 'Nombre Log(s): ' + objectbox.getLogCount().toString(), 0); } // upload photo to server await _uploadVisitPhotos(); // delete visits without a photo! await objectbox.DeleteVisitWithoutPhoto(); // synchronous calendar synchronization final syncCalendarResult = await _apiProvider.SyncCalendar(); // backoffice synchronization OK ? if (syncCalendarResult == 'OK') { _backofficeSyncCompleted = true; } // synchronous logs synchronization final syncLogResult = _syncLog ? await _apiProvider.SyncLog() : 'OK'; // log synchronization OK ? if (syncLogResult == 'OK') { _logSyncCompleted = true; } // deletes photos that are no longer in any visits await _cleanVisitPhotoDir(); // deleting photos in the cache directory await _clearPhotoCache(); // last synchronization date SharedPrefs().lastCalendarRefresh = DateFormat('dd/MM/yyyy HH:mm').format(DateTime.now()); // send global event to refresh calendar eventBus.fire(RefreshCalendarEvent(SharedPrefs().lastCalendarRefresh)); setState(() { _isSyncing = false; _syncCompleted = true; }); _animationController.stop(); stopwatch.stop(); // log tracker LoggerUtil.dblog('LOG', 'MOBDR', 'Synchronisation données', stopwatch.elapsedMilliseconds); // enable navigation bar buttons eventBus.fire(SynchronizationEvent(false)); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( iconTheme: IconThemeData( color: GlobalStyle.appBarIconThemeColor, ), elevation: GlobalStyle.appBarElevation, title: Text( AppLocalizations.of(context)!.translate('i18n_label_data_synchronization'), style: GlobalStyle.appBarTitle, ), backgroundColor: GlobalStyle.appBarBackgroundColor, systemOverlayStyle: GlobalStyle.appBarSystemOverlayStyle, bottom: _reusableWidget.bottomAppBar(), ), body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Center( child: Container( width: 100, height: 100, decoration: BoxDecoration( shape: BoxShape.circle, color: _isInternetConnexion ? Colors.blue : Colors .red, // Modifier la couleur en fonction de la connexion Internet ), child: IconButton( icon: AnimatedBuilder( animation: _animationController, builder: (BuildContext context, Widget? child) { return Transform.rotate( angle: _rotationAnimation.value * 2.0 * 3.14, child: Icon( _isInternetConnexion ? Icons.refresh : Icons .signal_wifi_off, // Modifier l'icône en fonction de la connexion Internet color: Colors.white, size: 70, ), ); }, ), onPressed: _isSyncing ? null : doSync, ), ), ), SizedBox(height: 10), Text( !_isInternetConnexion ? AppLocalizations.of(context)! .translate('i18n_label_no_internet') : (SharedPrefs().lastCalendarRefresh.isNotEmpty ? SharedPrefs().lastCalendarRefresh : AppLocalizations.of(context)!.translate('i18n_label_never')), style: TextStyle( fontSize: 16, color: !_isInternetConnexion ? Colors.red : Colors .black, // Modifier la couleur du texte en fonction de votre préférence fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ Text( AppLocalizations.of(context)!.translate('i18n_label_sync_logs'), style: TextStyle(fontSize: 16), ), Switch( value: _syncLog, onChanged: (value) { setState(() { _syncLog = value; }); }, ), ], ), SizedBox(height: 20), SyncItem( icon: Icons.business, title: AppLocalizations.of(context)!.translate('i18n_menu_backoffice'), description: AppLocalizations.of(context)! .translate('i18n_label_backoffice_synchronization'), isSyncing: _isSyncing, syncCompleted: _syncCompleted, isError: _syncCompleted && !_backofficeSyncCompleted, ), SizedBox(height: 20), SyncItem( icon: Icons.photo, title: AppLocalizations.of(context)!.translate('i18n_menu_photos'), description: AppLocalizations.of(context)! .translate('i18n_label_photos_synchronization'), isSyncing: _isSyncing, syncCompleted: _syncCompleted, isError: _syncCompleted && !_photosSyncCompleted, ), SizedBox(height: 20), SyncItem( icon: Icons.warning, title: AppLocalizations.of(context)!.translate('i18n_menu_log'), description: AppLocalizations.of(context)! .translate('i18n_label_logs_synchronization'), isSyncing: _isSyncing, syncCompleted: _syncCompleted, isError: _syncCompleted && !_logSyncCompleted, ), ], ), ), ), ); } } class SyncItem extends StatelessWidget { final IconData icon; final String title; final String description; final bool isSyncing; final bool syncCompleted; final bool isError; const SyncItem({ required this.icon, required this.title, required this.description, required this.isSyncing, required this.syncCompleted, required this.isError, }); @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( icon, color: Colors.blue, size: 24, ), SizedBox(width: 10), Text( title, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), ], ), SizedBox(height: 10), Text(description), SizedBox(height: 10), if (isSyncing) LinearProgressIndicator( backgroundColor: Colors.grey[300], valueColor: AlwaysStoppedAnimation(Colors.blue), ), if (syncCompleted) Row( children: [ Icon( isError ? Icons.error_outline : Icons.check, color: isError ? Colors.red : Colors.green, size: 20, ), SizedBox(width: 10), Text( isError ? AppLocalizations.of(context)! .translate('i18n_label_synchronization_error') : AppLocalizations.of(context)! .translate('i18n_label_synchronization_complete'), style: TextStyle( color: isError ? Colors.red : Colors.green, fontWeight: FontWeight.bold, ), ), ], ), ], ); } }