import 'dart:async'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart'; import 'package:hub/flutter_flow/nav/nav.dart'; import 'package:hub/shared/utils/dialog_util.dart'; import 'package:hub/shared/utils/limited_text_size.dart'; import 'package:image_picker/image_picker.dart'; import 'package:mime_type/mime_type.dart'; import 'flutter_flow_theme.dart'; import 'flutter_flow_util.dart'; const allowedFormats = {'image/png', 'image/jpeg'}; class SelectedFile { const SelectedFile({ this.storagePath = '', this.filePath, required this.bytes, this.dimensions, this.blurHash, }); final String storagePath; final String? filePath; final Uint8List bytes; final MediaDimensions? dimensions; final String? blurHash; } class MediaDimensions { const MediaDimensions({ this.height, this.width, }); final double? height; final double? width; } enum MediaSource { photoGallery, videoGallery, camera, } Future?> selectMediaWithSourceBottomSheetandFaceDetection({ required BuildContext context, String? storageFolderPath, double? maxWidth, double? maxHeight, int? imageQuality, required bool allowPhoto, bool allowVideo = false, String pickerFontFamily = 'Roboto', // Color textColor = const Color(0xFF111417), // Color backgroundColor = const Color(0xFFF5F5F5), bool includeDimensions = false, bool includeBlurHash = false, }) async { final faceDetector = FaceDetector( options: FaceDetectorOptions( enableContours: true, enableLandmarks: true, ), ); createUploadMediaListTile(String label, MediaSource mediaSource, IconData icon) => ListTile( title: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, color: FlutterFlowTheme.of(context).primaryText), const SizedBox(width: 5), Text( label, textAlign: TextAlign.center, style: GoogleFonts.getFont( pickerFontFamily, color: FlutterFlowTheme.of(context).primaryText, fontWeight: FontWeight.w600, fontSize: 12, ), ), ], ), tileColor: FlutterFlowTheme.of(context).primaryBackground, dense: false, onTap: () => context.pop(mediaSource), ); final mediaSource = await showModalBottomSheet( context: context, backgroundColor: FlutterFlowTheme.of(context).primaryBackground, builder: (context) { return Column( mainAxisSize: MainAxisSize.min, children: [ if (!kIsWeb) ...[ Container( margin: const EdgeInsets.only(top: 10), width: 40, height: 5, decoration: BoxDecoration( color: Colors.grey[300], borderRadius: BorderRadius.circular(10), ), ), Padding( padding: const EdgeInsets.fromLTRB(0, 8, 0, 0), child: ListTile( title: Text( FFLocalizations.of(context) .getVariableText(ptText: "Escolha uma das opções", enText: "Choose one of the options"), textAlign: TextAlign.center, style: GoogleFonts.getFont( pickerFontFamily, color: FlutterFlowTheme.of(context).primaryText.withOpacity(0.65), fontWeight: FontWeight.w500, fontSize: 14, ), ), tileColor: FlutterFlowTheme.of(context).primaryBackground, dense: true, ), ), ], Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ if (allowPhoto && allowVideo) ...[ Expanded( child: createUploadMediaListTile( FFLocalizations.of(context) .getVariableText(ptText: "Galeria (Foto)", enText: "Gallery (Photo)"), MediaSource.photoGallery, Icons.camera), ), Expanded( child: createUploadMediaListTile( FFLocalizations.of(context) .getVariableText(ptText: "Galeria (Video)", enText: "Gallery (Video)"), MediaSource.videoGallery, Icons.videocam), ) ] else if (allowPhoto) Expanded( child: createUploadMediaListTile( FFLocalizations.of(context).getVariableText(ptText: "Galeria", enText: "Gallery"), MediaSource.photoGallery, Icons.photo)) else Expanded( child: createUploadMediaListTile( FFLocalizations.of(context).getVariableText(ptText: "Galeria", enText: "Gallery"), MediaSource.videoGallery, Icons.videocam), ), if (!kIsWeb) ...[ Expanded( child: createUploadMediaListTile( FFLocalizations.of(context).getVariableText(ptText: "Camera", enText: "Camera"), MediaSource.camera, Icons.camera_alt), ) ], ], ), const SizedBox(height: 15), ], ); }); if (mediaSource == null) { return null; } final selectedMedia = await selectMedia( storageFolderPath: storageFolderPath, maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: imageQuality, isVideo: mediaSource == MediaSource.videoGallery || (mediaSource == MediaSource.camera && allowVideo && !allowPhoto), mediaSource: mediaSource, includeDimensions: includeDimensions, includeBlurHash: includeBlurHash, ); if (selectedMedia == null || selectedMedia.isEmpty) { return null; } // Verifique se há faces na imagem for (final media in selectedMedia) { final inputImage = InputImage.fromFilePath(media.filePath!); final faces = await faceDetector.processImage(inputImage); if (faces.isEmpty) { final String message = FFLocalizations.of(context) .getVariableText(ptText: "Nenhuma face detectada na imagem", enText: "No face detected in the image"); DialogUtil.error(context, message); return null; } if (faces.length > 1) { final String message = FFLocalizations.of(context).getVariableText( ptText: "Mais de uma face detectada na imagem", enText: "More than one face detected in the image"); DialogUtil.error(context, message); return null; } } return selectedMedia; } Future?> selectMediaWithSourceBottomSheet({ required BuildContext context, String? storageFolderPath, double? maxWidth, double? maxHeight, int? imageQuality, required bool allowPhoto, bool allowVideo = false, String pickerFontFamily = 'Roboto', // Color textColor = const Color(0xFF111417), // Color backgroundColor = const Color(0xFFF5F5F5), bool includeDimensions = false, bool includeBlurHash = false, }) async { createUploadMediaListTile(String label, MediaSource mediaSource, IconData icon) => ListTile( title: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, color: FlutterFlowTheme.of(context).primaryText), const SizedBox(width: 5), Text( label, textAlign: TextAlign.center, style: GoogleFonts.getFont( pickerFontFamily, color: FlutterFlowTheme.of(context).primaryText, fontWeight: FontWeight.w600, fontSize: 12, ), ), ], ), tileColor: FlutterFlowTheme.of(context).primaryBackground, dense: false, onTap: () => context.pop(mediaSource), // Navigator.pop(context,mediaSource,), ); final mediaSource = await showModalBottomSheet( context: context, backgroundColor: FlutterFlowTheme.of(context).primaryBackground, builder: (context) { return Column( mainAxisSize: MainAxisSize.min, children: [ if (!kIsWeb) ...[ Container( margin: const EdgeInsets.only(top: 10), width: 40, height: 5, decoration: BoxDecoration( color: Colors.grey[300], borderRadius: BorderRadius.circular(10), ), ), Padding( padding: const EdgeInsets.fromLTRB(0, 8, 0, 0), child: ListTile( title: Text( FFLocalizations.of(context) .getVariableText(ptText: "Escolha uma das opções", enText: "Choose one of the options"), textAlign: TextAlign.center, style: GoogleFonts.getFont( pickerFontFamily, color: FlutterFlowTheme.of(context).primaryText.withOpacity(0.65), fontWeight: FontWeight.w500, fontSize: 14, ), ), tileColor: FlutterFlowTheme.of(context).primaryBackground, dense: true, ), ), ], Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ if (allowPhoto && allowVideo) ...[ Expanded( child: createUploadMediaListTile( FFLocalizations.of(context) .getVariableText(ptText: "Galeria (Foto)", enText: "Gallery (Photo)"), MediaSource.photoGallery, Icons.camera), ), Expanded( child: createUploadMediaListTile( FFLocalizations.of(context) .getVariableText(ptText: "Galeria (Video)", enText: "Gallery (Video)"), MediaSource.videoGallery, Icons.videocam), ) ] else if (allowPhoto) Expanded( child: createUploadMediaListTile( FFLocalizations.of(context).getVariableText(ptText: "Galeria", enText: "Gallery"), MediaSource.photoGallery, Icons.photo)) else Expanded( child: createUploadMediaListTile( FFLocalizations.of(context).getVariableText(ptText: "Galeria", enText: "Gallery"), MediaSource.videoGallery, Icons.videocam), ), if (!kIsWeb) ...[ Expanded( child: createUploadMediaListTile( FFLocalizations.of(context).getVariableText(ptText: "Camera", enText: "Camera"), MediaSource.camera, Icons.camera_alt), ) ], ], ), const SizedBox(height: 15), ], ); }); if (mediaSource == null) { return null; } return selectMedia( storageFolderPath: storageFolderPath, maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: imageQuality, isVideo: mediaSource == MediaSource.videoGallery || (mediaSource == MediaSource.camera && allowVideo && !allowPhoto), mediaSource: mediaSource, includeDimensions: includeDimensions, includeBlurHash: includeBlurHash, ); } Future?> selectMedia({ String? storageFolderPath, double? maxWidth, double? maxHeight, int? imageQuality, bool isVideo = false, MediaSource mediaSource = MediaSource.camera, bool multiImage = false, bool includeDimensions = false, bool includeBlurHash = false, }) async { final picker = ImagePicker(); if (multiImage) { final pickedMediaFuture = picker.pickMultiImage( maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: imageQuality, ); final pickedMedia = await pickedMediaFuture; if (pickedMedia.isEmpty) { return null; } return Future.wait(pickedMedia.asMap().entries.map((e) async { final index = e.key; final media = e.value; final mediaBytes = await media.readAsBytes(); final path = _getStoragePath(storageFolderPath, media.name, false, index); final dimensions = includeDimensions ? isVideo ? _getVideoDimensions(media.path) : _getImageDimensions(mediaBytes) : null; return SelectedFile( storagePath: path, filePath: media.path, bytes: mediaBytes, dimensions: await dimensions, ); })); } final source = mediaSource == MediaSource.camera ? ImageSource.camera : ImageSource.gallery; final pickedMediaFuture = isVideo ? picker.pickVideo(source: source) : picker.pickImage( maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: imageQuality, source: source, ); final pickedMedia = await pickedMediaFuture; final mediaBytes = await pickedMedia?.readAsBytes(); if (mediaBytes == null) { return null; } final path = _getStoragePath(storageFolderPath, pickedMedia!.name, isVideo); final dimensions = includeDimensions ? isVideo ? _getVideoDimensions(pickedMedia.path) : _getImageDimensions(mediaBytes) : null; return [ SelectedFile( storagePath: path, filePath: pickedMedia.path, bytes: mediaBytes, dimensions: await dimensions, ), ]; } bool validateFileFormat(String filePath, BuildContext context) { if (allowedFormats.contains(mime(filePath))) { return true; } ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(SnackBar( content: Text('Invalid file format: ${mime(filePath)}'), )); return false; } Future selectFile({ String? storageFolderPath, List? allowedExtensions, }) => selectFiles( storageFolderPath: storageFolderPath, allowedExtensions: allowedExtensions, multiFile: false, ).then((value) => value?.first); Future?> selectFiles({ String? storageFolderPath, List? allowedExtensions, bool multiFile = false, }) async { final pickedFiles = await FilePicker.platform.pickFiles( type: allowedExtensions != null ? FileType.custom : FileType.any, allowedExtensions: allowedExtensions, withData: true, allowMultiple: multiFile, ); if (pickedFiles == null || pickedFiles.files.isEmpty) { return null; } if (multiFile) { return Future.wait(pickedFiles.files.asMap().entries.map((e) async { final index = e.key; final file = e.value; final storagePath = _getStoragePath(storageFolderPath, file.name, false, index); return SelectedFile( storagePath: storagePath, filePath: isWeb ? null : file.path, bytes: file.bytes!, ); })); } final file = pickedFiles.files.first; if (file.bytes == null) { return null; } final storagePath = _getStoragePath(storageFolderPath, file.name, false); return [ SelectedFile( storagePath: storagePath, filePath: isWeb ? null : file.path, bytes: file.bytes!, ) ]; } List selectedFilesFromUploadedFiles( List uploadedFiles, { String? storageFolderPath, bool isMultiData = false, }) => uploadedFiles.asMap().entries.map( (entry) { final index = entry.key; final file = entry.value; return SelectedFile( storagePath: _getStoragePath( storageFolderPath, file.name!, false, isMultiData ? index : null, ), bytes: file.bytes!); }, ).toList(); Future _getImageDimensions(Uint8List mediaBytes) async { final image = await decodeImageFromList(mediaBytes); return MediaDimensions( width: image.width.toDouble(), height: image.height.toDouble(), ); } Future _getVideoDimensions(String path) async { // final VideoPlayerController videoPlayerController = // VideoPlayerController.asset(path); // await videoPlayerController.initialize(); // final size = videoPlayerController.value.size; //mock final size = const Size(1920, 1080); return MediaDimensions(width: size.width, height: size.height); } String _getStoragePath( String? pathPrefix, String filePath, bool isVideo, [ int? index, ]) { pathPrefix = _removeTrailingSlash(pathPrefix); final timestamp = DateTime.now().microsecondsSinceEpoch; // Workaround fixed by https://github.com/flutter/plugins/pull/3685 // (not yet in stable). final ext = isVideo ? 'mp4' : filePath.split('.').last; final indexStr = index != null ? '_$index' : ''; return '$pathPrefix/$timestamp$indexStr.$ext'; } String getSignatureStoragePath([String? pathPrefix]) { pathPrefix = _removeTrailingSlash(pathPrefix); final timestamp = DateTime.now().microsecondsSinceEpoch; return '$pathPrefix/signature_$timestamp.png'; } void showUploadMessage( BuildContext context, String message, { bool showLoading = false, }) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( SnackBar( content: Row( children: [ if (showLoading) Padding( padding: const EdgeInsetsDirectional.only(end: 10.0), child: CircularProgressIndicator( valueColor: Theme.of(context).brightness == Brightness.dark ? AlwaysStoppedAnimation(FlutterFlowTheme.of(context).info) : null, ), ), Text(message, style: TextStyle( color: FlutterFlowTheme.of(context).primaryText, fontSize: LimitedFontSizeUtil.getBodyFontSize(context), )), ], ), backgroundColor: FlutterFlowTheme.of(context).primary, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( topLeft: Radius.circular(15), topRight: Radius.circular(15), ), ), duration: showLoading ? const Duration(days: 1) : const Duration(seconds: 4), ), ); } String? _removeTrailingSlash(String? path) => path != null && path.endsWith('/') ? path.substring(0, path.length - 1) : path;