import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:image/image.dart' as img; import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:mobdr/config/constant.dart'; import 'package:mobdr/config/global_style.dart'; import 'package:mobdr/main.dart'; import 'package:mobdr/ui/visit/visit_photo_tag.dart'; import 'package:mobdr/ui/reusable/reusable_widget.dart'; import 'package:mobdr/db/box_visit_photo.dart'; import 'package:mobdr/db/box_photo_typology.dart'; import 'package:mobdr/db/box_photo_competitor.dart'; import 'package:mobdr/model/visit_model.dart'; import 'package:mobdr/service/plausible.dart'; import 'package:mobdr/service/logger_util.dart'; import 'package:mobdr/cubit/language/app_localizations.dart'; class VisitPhotoTypologyDetailPage extends StatefulWidget { final VisitModel pp_visitModel; final int pp_imageId; final String pp_image; final int pp_id_typologie; // Requiring data parameters VisitPhotoTypologyDetailPage({ required this.pp_visitModel, required this.pp_imageId, required this.pp_image, required this.pp_id_typologie, }); @override _VisitPhotoTypologyDetailPageState createState() => _VisitPhotoTypologyDetailPageState(); } class _VisitPhotoTypologyDetailPageState extends State { // initialize reusable widget final _reusableWidget = ReusableWidget(); bool _isLoading = true; String _errorMessage = ''; late String imageSize; late String imageResolution; late String imageOrientation; // Typology list late List _photoTypologiesList = []; int _photoTypologyIndex = 0; List _visibilities = []; late List tagList = []; late VisitPhoto _visitPhoto; // competitors List _etabCompetitorsList = []; String _etabCompetitor = ''; @override void initState() { super.initState(); getImageDetails(); // track & log page access final page = 'visit_photo_typology_detail'; LoggerUtil.dblog('LOG', 'MOBDR', 'Page : ${page}', 0); PlausibleUtil.addEventAsync(name: 'pageview', page: page); loadData(widget.pp_imageId).then((_) { setState(() { _isLoading = false; }); }); } void getImageDetails() { File imageFile = File(widget.pp_image); if (imageFile.existsSync()) { img.Image? image = img.decodeImage(imageFile.readAsBytesSync()); int imageSizeInBytes = imageFile.lengthSync(); double imageSizeInMb = imageSizeInBytes / (1024 * 1024); int imageWidth = image!.width; int imageHeight = image.height; imageSize = '${imageSizeInMb.toStringAsFixed(2)} MB'; imageResolution = '$imageWidth x $imageHeight'; if (imageWidth > imageHeight) { imageOrientation = 'Landscape'; } else if (imageHeight > imageWidth) { imageOrientation = 'Portrait'; } else { imageOrientation = 'Square'; } } else { // The image file does not exist imageSize = ''; imageResolution = ''; imageOrientation = ''; } } @override void dispose() { super.dispose(); } @override Widget build(BuildContext context) { if (_isLoading) { return Center(child: CircularProgressIndicator()); } else if (_errorMessage.isNotEmpty) { return Center( child: AlertDialog( title: Text(AppLocalizations.of(context)!.translate('i18n_label_error')), content: Text(AppLocalizations.of(context)!.translate('i18n_label_photo_not_found')), actions: [ TextButton( child: Text(AppLocalizations.of(context)!.translate('i18n_label_ok')), onPressed: () { Navigator.of(context).pop(); }, ), ], ), ); } return Scaffold( appBar: AppBar( iconTheme: IconThemeData( color: GlobalStyle.appBarIconThemeColor, ), elevation: GlobalStyle.appBarElevation, title: Text( AppLocalizations.of(context)!.translate('i18n_label_photo'), style: GlobalStyle.appBarTitle, ), backgroundColor: GlobalStyle.appBarBackgroundColor, systemOverlayStyle: GlobalStyle.appBarSystemOverlayStyle, bottom: _reusableWidget.bottomAppBar(), ), body: WillPopScope( onWillPop: () { // fred Map result = { 'change_typologie': _photoTypologyIndex != _photoTypologiesList.indexWhere((typology) => typology.id_photo_typologie == widget.pp_id_typologie), 'tags': tagList.join(","), 'photo_principale': _visibilities.contains('principal') ? 1 : 0, 'photo_privee': _visibilities.contains('private') ? 1 : 0, }; // return to the parent page fred Navigator.pop(context, result); return Future.value(true); }, child: Column( children: [ Flexible( child: ListView( children: [ Image.file(File(widget.pp_image), fit: BoxFit.cover), _buildPhotoDetail(), _buildPhotoTag(context), _buildPhotoVisibility(), _BuildPhotoCompetitor(), _buildPhotoTypology(), SizedBox(height: 16) ], ), ), Container( padding: EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, boxShadow: [ BoxShadow( color: Colors.grey, offset: Offset(0.0, 1.0), //(x,y) blurRadius: 2.0, ), ], ), child: Row( children: [ SizedBox( width: 8, ), Expanded( child: GestureDetector( onTap: () async { bool isSaved = await saveImageToGallery(widget.pp_image); if (isSaved) { // log tracker LoggerUtil.dblog( 'LOG', 'MOBDR', 'Copier dans galerie', 0); Fluttertoast.showToast( msg: AppLocalizations.of(context)!.translate('i18n_label_photo_added_to_gallery'), toastLength: Toast.LENGTH_LONG); } else { // log tracker LoggerUtil.dblog('ERR', 'MOBDR', 'Erreur copier dans galerie', 0); Fluttertoast.showToast( msg: AppLocalizations.of(context)!.translate('i18n_label_error_while_copying'), toastLength: Toast.LENGTH_LONG); } }, child: Container( alignment: Alignment.center, padding: EdgeInsets.fromLTRB(12, 8, 12, 8), 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( AppLocalizations.of(context)! .translate('i18n_label_copy_to_gallery'), style: TextStyle( color: SOFT_BLUE, fontWeight: FontWeight.bold)), ), ), ), ], ), ) ], ), )); } Widget _buildPhotoDetail() { return Container( child: Row( children: [ Padding( padding: EdgeInsets.only(left: 5), // Décalage de 10 pixels à gauche child: Text( imageSize, style: TextStyle( color: Colors.blue, fontWeight: FontWeight.bold, ), ), ), SizedBox(width: 16), Flexible( child: Text( imageResolution, style: TextStyle( color: Colors.blue, fontWeight: FontWeight.bold, ), ), ), SizedBox(width: 16), Flexible( child: Text( '($imageOrientation)', style: TextStyle( color: Colors.blue, fontWeight: FontWeight.bold, ), ), ), ], ), ); } Widget _buildPhotoTypology() { return Container( margin: EdgeInsets.only(top: 12), padding: EdgeInsets.all(16), color: Colors.white, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( AppLocalizations.of(context)! .translate('i18n_label_typology'), style: GlobalStyle.sectionTitle), SizedBox( height: 16, ), Wrap( children: List.generate(_photoTypologiesList.length, (index) { return radioSize(_photoTypologiesList[index].libelle, index); }), ), ], )); } Widget radioSize(String txt, int index) { return GestureDetector( onTap: () async { setState(() { _photoTypologyIndex = index; }); // save photo typology in the database objectbox.putPhotoTypologie( 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: _photoTypologyIndex == index ? SOFT_BLUE : Colors.white, border: Border.all( width: 1, color: _photoTypologyIndex == index ? SOFT_BLUE : Colors.grey[300]!), borderRadius: BorderRadius.all( Radius.circular(10) // <--- border radius here )), child: Text(txt, style: TextStyle( color: _photoTypologyIndex == index ? Colors.white : CHARCOAL)), ), ); } Widget _checkboxVisibility({value, primaryText}) { return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { setState(() { if (_visibilities.contains(value)) { _visibilities.remove(value); } else { _visibilities.add(value); } }); // save photo visibilities in the database objectbox.putPhotoVisibilities(widget.pp_imageId, _visibilities); }, child: Row( children: [ Container( decoration: BoxDecoration( border: Border.all( width: 1, color: (_visibilities.contains(value)) ? PRIMARY_COLOR : BLACK77), borderRadius: BorderRadius.all(Radius.circular(4.0)), ), child: Padding( padding: const EdgeInsets.all(2), child: (_visibilities.contains(value)) ? Icon( Icons.check, size: 12.0, color: PRIMARY_COLOR, ) : Icon( Icons.check_box_outline_blank, size: 12.0, color: Colors.white, ), ), ), SizedBox(width: 16), Text(primaryText, style: TextStyle( fontSize: 13, color: BLACK77, fontWeight: (_visibilities.contains(value)) ? FontWeight.bold : FontWeight.normal)), ], ), ); } Widget _buildPhotoVisibility() { return Container( margin: EdgeInsets.only(top: 12), padding: EdgeInsets.all(16), color: Colors.white, child: Column( children: [ Row( children: [ Text(AppLocalizations.of(context)!.translate('i18n_label_visibility'), style: GlobalStyle.sectionTitle), ], ), SizedBox(height: 16), _checkboxVisibility(value: 'private', primaryText: AppLocalizations.of(context)! .translate('i18n_label_private_visibility')), Divider( height: 32, color: Colors.grey[400], ), _checkboxVisibility(value: 'principal', primaryText: AppLocalizations.of(context)! .translate('i18n_label_main_visibility')) ], ), ); } Widget _buildPhotoTag(BuildContext context) { return GestureDetector( onTap: () async { // Navigation to the PhotoTagPage final newTags = await Navigator.push>( context, MaterialPageRoute( builder: (context) => PhotoTagPage( pp_langage: widget.pp_visitModel.langage, pp_id_distrib: widget.pp_visitModel.id_distrib, pp_photoId: this._visitPhoto.id, pp_currentTags: tagList), ), ); // Checking changes made to tags if (newTags != null && !listEquals(tagList, newTags)) { setState(() { tagList = newTags; }); } }, child: Container( margin: EdgeInsets.only(top: 12), padding: EdgeInsets.all(16), color: Colors.white, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text(AppLocalizations.of(context)! .translate('i18n_label_tags'), style: GlobalStyle.sectionTitle), ], ), Wrap( spacing: 8.0, runSpacing: 8.0, alignment: WrapAlignment.start, children: tagList.map((tag) { return Chip( label: Text(tag), backgroundColor: Colors.grey[300], ); }).toList(), ), ], ), ), ); } Widget _BuildPhotoCompetitor() { return Container( margin: EdgeInsets.only(top: 12), padding: EdgeInsets.all(16), color: Colors.white, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( AppLocalizations.of(context)! .translate('i18n_label_competition'), style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), SizedBox( height: 16, ), GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { showModalBottomSheet( context: context, builder: (BuildContext context) { return _showCompetitionPopup(); }, ); }, child: Container( alignment: Alignment.center, padding: EdgeInsets.fromLTRB(12, 8, 12, 8), margin: EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: Colors.white, border: Border.all(width: 1, color: Colors.grey[300]!), borderRadius: BorderRadius.all( Radius.circular(10) // <--- border radius here )), child: _etabCompetitor == '' ? Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Icon(Icons.thumb_down, color: SOFT_BLUE), SizedBox(width: 12), Text(AppLocalizations.of(context)! .translate('i18n_label_choose_competitor'), style: TextStyle( color: CHARCOAL, fontWeight: FontWeight.bold)), ], ), Icon(Icons.chevron_right, size: 20, color: SOFT_GREY), ], ) : Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(_etabCompetitor, style: TextStyle( color: CHARCOAL, fontWeight: FontWeight.bold)), ], ), Icon(Icons.chevron_right, size: 20, color: SOFT_GREY), ], ), ), ), ], )); } Widget _showCompetitionPopup() { return StatefulBuilder( builder: (BuildContext context, StateSetter mystate) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Container( margin: EdgeInsets.only(top: 12, bottom: 12), width: 40, height: 4, decoration: BoxDecoration( color: Colors.grey[500], borderRadius: BorderRadius.circular(10)), ), ), Container( margin: EdgeInsets.fromLTRB(16, 8, 16, 8), child: Text(AppLocalizations.of(context)! .translate('i18n_label_choose_competitor'), style: GlobalStyle.chooseCourier), ), Flexible( child: ListView.builder( padding: EdgeInsets.all(16), itemCount: _etabCompetitorsList.length, itemBuilder: (BuildContext context, int index) { EtabCompetitor competitor = _etabCompetitorsList[index]; return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { setState(() { _etabCompetitor = competitor.nom; }); // save photo competitor in the database objectbox.putPhotoCompetitor( widget.pp_imageId, competitor.id_concurrence_lien); Navigator.pop(context); }, child: Container( margin: EdgeInsets.only(top: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(competitor.nom, style: GlobalStyle.courierType), ], ), ), ); }, ), ), ], ); }); } Future saveImageToGallery(String imagePath) async { // Request permission to access external storage PermissionStatus status = await Permission.storage.request(); if (status.isGranted) { final result = await ImageGallerySaver.saveFile(imagePath); bool isSuccess = result["isSuccess"]; return isSuccess; } else { // The user has refused permission return false; } } /// Initializes data when the page loads. Future loadData(int photoId) async { String tags = ""; try { // photo typologies initialization _photoTypologiesList = objectbox.getPhotoTypologiesList(); _photoTypologyIndex = _photoTypologiesList.indexWhere( (typology) => typology.id_photo_typologie == widget.pp_id_typologie); // get photo object _visitPhoto = objectbox.getPhotoById(photoId)!; // visibilities initialization if (_visitPhoto.photo_privee == 1) _visibilities.add('private'); if (_visitPhoto.photo_principale == 1) _visibilities.add('principal'); // photo tag initialization tags = _visitPhoto.tags; tagList = tags.isEmpty ? [] : _visitPhoto.tags.split(","); // competitor initialization _etabCompetitorsList = objectbox.getEtabCompetitorList(widget.pp_visitModel.id_etab); final etabCompetitor = objectbox.getEtabCompetitorByLink(_visitPhoto.id_concurrence_lien); _etabCompetitor = etabCompetitor != null ? etabCompetitor.nom : ""; } catch (e) { // set errorMessage for debug _errorMessage = 'Error loading photo: $e'; } } }