import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:file_picker/file_picker.dart'; class CommonUploadBottomSheet { static Future show({ required BuildContext context, required Function(String filePath, String fileName, String fileExtension) onFileSelected, List? allowedFileTypes, double maxFileSizeMB = 5.0, bool showCamera = true, bool showGallery = true, bool showDocument = true, String title = "Upload File", Function(String errorMessage)? onError, }) async { await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (BuildContext context) { return Container( decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewPadding.bottom, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Drag Handle Container( margin: const EdgeInsets.only(top: 12), width: 40, height: 4, decoration: BoxDecoration( color: Colors.grey.shade300, borderRadius: BorderRadius.circular(2), ), ), const SizedBox(height: 16), // Title Text( title, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 20), // Options Row Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ if (showCamera) _UploadOptionButton( icon: Icons.camera_alt, label: "Camera", color: Colors.blue, onTap: () async { Navigator.pop(context); await _pickFromCamera( onFileSelected: onFileSelected, onError: onError, maxFileSizeMB: maxFileSizeMB, allowedFileTypes: allowedFileTypes, ); }, ), if (showGallery) _UploadOptionButton( icon: Icons.image, label: "Gallery", color: Colors.purple, onTap: () async { Navigator.pop(context); await _pickFromGallery( onFileSelected: onFileSelected, onError: onError, maxFileSizeMB: maxFileSizeMB, allowedFileTypes: allowedFileTypes, ); }, ), if (showDocument) _UploadOptionButton( icon: Icons.insert_drive_file, label: "Document", color: Colors.orange, onTap: () async { Navigator.pop(context); await _pickDocument( onFileSelected: onFileSelected, onError: onError, maxFileSizeMB: maxFileSizeMB, allowedFileTypes: allowedFileTypes, ); }, ), ], ), ), const SizedBox(height: 20), Container( color: Colors.white, padding: EdgeInsetsDirectional.only( bottom: MediaQuery.of(context).viewPadding.bottom, ), ), ], ), ); }, ); } // Pick from Camera static Future _pickFromCamera({ required Function(String, String, String) onFileSelected, Function(String)? onError, required double maxFileSizeMB, List? allowedFileTypes, }) async { try { final ImagePicker picker = ImagePicker(); final XFile? image = await picker.pickImage( source: ImageSource.camera, imageQuality: 100, ); if (image != null) { final fileBytes = await image.readAsBytes(); final fileSizeMB = fileBytes.length / (1024 * 1024); if (fileSizeMB > maxFileSizeMB) { onError?.call( "File size (${fileSizeMB.toStringAsFixed(2)}MB) exceeds maximum allowed size of ${maxFileSizeMB}MB", ); return; } final extension = image.path.split('.').last.toLowerCase(); final fileName = image.path.split('/').last; if (allowedFileTypes != null && !allowedFileTypes.contains(extension)) { onError?.call( "File type .$extension is not allowed. Allowed types: ${allowedFileTypes.join(', ')}", ); return; } onFileSelected(image.path, fileName, extension); } } catch (e) { onError?.call("Failed to capture image: $e"); } } // Pick from Gallery static Future _pickFromGallery({ required Function(String, String, String) onFileSelected, Function(String)? onError, required double maxFileSizeMB, List? allowedFileTypes, }) async { try { final ImagePicker picker = ImagePicker(); // Check if video extensions are in allowedFileTypes final isVideoOnly = allowedFileTypes != null && allowedFileTypes.every( (ext) => ['mp4', 'm4v'].contains(ext.toLowerCase()), ); XFile? file; if (isVideoOnly) { // Pick video if only video types are allowed file = await picker.pickVideo(source: ImageSource.gallery); } else { // Pick image for image types file = await picker.pickImage( source: ImageSource.gallery, imageQuality: 100, ); } if (file != null) { final fileBytes = await file.readAsBytes(); final fileSizeMB = fileBytes.length / (1024 * 1024); if (fileSizeMB > maxFileSizeMB) { onError?.call( "File size (${fileSizeMB.toStringAsFixed(2)}MB) exceeds maximum allowed size of ${maxFileSizeMB}MB", ); return; } final extension = file.path.split('.').last.toLowerCase(); final fileName = file.path.split('/').last; if (allowedFileTypes != null && !allowedFileTypes.contains(extension)) { onError?.call( "File type .$extension is not allowed. Allowed types: ${allowedFileTypes.join(', ')}", ); return; } onFileSelected(file.path, fileName, extension); } } catch (e) { onError?.call("Failed to pick file: $e"); } } // Pick Document static Future _pickDocument({ required Function(String, String, String) onFileSelected, Function(String)? onError, required double maxFileSizeMB, List? allowedFileTypes, }) async { try { final result = await FilePicker.platform.pickFiles( type: allowedFileTypes != null ? FileType.custom : FileType.any, allowedExtensions: allowedFileTypes, ); if (result != null && result.files.single.path != null) { final file = result.files.single; final filePath = file.path!; final fileSizeMB = file.size / (1024 * 1024); final extension = file.extension?.toLowerCase() ?? ''; final fileName = file.name; if (fileSizeMB > maxFileSizeMB) { onError?.call( "File size (${fileSizeMB.toStringAsFixed(2)}MB) exceeds maximum allowed size of ${maxFileSizeMB}MB", ); return; } if (allowedFileTypes != null && !allowedFileTypes.contains(extension)) { onError?.call( "File type .$extension is not allowed. Allowed types: ${allowedFileTypes.join(', ')}", ); return; } onFileSelected(filePath, fileName, extension); } } catch (e) { onError?.call("Failed to pick document: $e"); } } } // Upload Option Button Widget class _UploadOptionButton extends StatelessWidget { final IconData icon; final String label; final Color color; final VoidCallback onTap; const _UploadOptionButton({ required this.icon, required this.label, required this.color, required this.onTap, }); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 60, height: 60, decoration: BoxDecoration( color: color.withValues(alpha: 0.1), shape: BoxShape.circle, border: Border.all( color: color.withValues(alpha: 0.3), width: 1.5, ), ), child: Icon(icon, color: color, size: 28), ), const SizedBox(height: 8), Text( label, style: TextStyle( fontSize: 12, color: Colors.grey.shade700, fontWeight: FontWeight.w500, ), ), ], ), ); } }