import 'dart:async'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:image_picker/image_picker.dart'; import 'package:mime_type/mime_type.dart'; import 'package:video_player/video_player.dart'; import 'flutter_flow_theme.dart'; import 'flutter_flow_util.dart'; const allowedFormats = {'image/png', 'image/jpeg', 'video/mp4', 'image/gif'}; 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?> 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) => ListTile( title: Text( label, textAlign: TextAlign.center, style: GoogleFonts.getFont( pickerFontFamily, color: FlutterFlowTheme.of(context).primaryText, fontWeight: FontWeight.w600, fontSize: 20, ), ), tileColor: FlutterFlowTheme.of(context).primaryBackground, dense: false, onTap: () => 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) ...[ Padding( padding: const EdgeInsets.fromLTRB(0, 8, 0, 0), child: ListTile( title: Text( 'Choose Source', textAlign: TextAlign.center, style: GoogleFonts.getFont( pickerFontFamily, color: FlutterFlowTheme.of(context) .primaryText .withOpacity(0.65), fontWeight: FontWeight.w500, fontSize: 20, ), ), tileColor: FlutterFlowTheme.of(context).primaryBackground, dense: true, ), ), const Divider(), ], if (allowPhoto && allowVideo) ...[ createUploadMediaListTile( 'Gallery (Photo)', MediaSource.photoGallery, ), const Divider(), createUploadMediaListTile( 'Gallery (Video)', MediaSource.videoGallery, ), ] else if (allowPhoto) createUploadMediaListTile( 'Gallery', MediaSource.photoGallery, ) else createUploadMediaListTile( 'Gallery', MediaSource.videoGallery, ), if (!kIsWeb) ...[ const Divider(), createUploadMediaListTile('Camera', MediaSource.camera), const Divider(), ], const SizedBox(height: 10), ], ); }); 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; 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: FlutterFlowTheme.of(context).bodyMedium), ], ), 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;