Main coves client
at main 166 lines 4.9 kB view raw
1import 'dart:developer' as developer; 2import 'dart:io'; 3 4import 'package:flutter/foundation.dart'; 5import 'package:image_picker/image_picker.dart'; 6 7import '../models/picked_image.dart'; 8 9/// Configuration for image picking constraints 10class ImageConstraints { 11 const ImageConstraints({ 12 this.maxWidth = 1024, 13 this.maxHeight = 1024, 14 this.imageQuality = 85, 15 this.maxSizeBytes = 1024 * 1024, // 1 MB 16 this.allowedMimeTypes = const {'image/jpeg', 'image/png', 'image/webp'}, 17 }) : assert(maxWidth > 0, 'maxWidth must be positive'), 18 assert(maxHeight > 0, 'maxHeight must be positive'), 19 assert( 20 imageQuality >= 0 && imageQuality <= 100, 21 'imageQuality must be 0-100', 22 ), 23 assert(maxSizeBytes > 0, 'maxSizeBytes must be positive'); 24 25 /// Maximum width in pixels (image will be resized if larger) 26 final double maxWidth; 27 28 /// Maximum height in pixels (image will be resized if larger) 29 final double maxHeight; 30 31 /// JPEG compression quality (0-100) 32 final int imageQuality; 33 34 /// Maximum file size in bytes after picking 35 final int maxSizeBytes; 36 37 /// Set of allowed MIME types 38 final Set<String> allowedMimeTypes; 39 40 /// Preset for avatar images (profile pics, community avatars) 41 static const avatar = ImageConstraints(); 42 43 /// Preset for larger images (banners, post images) 44 static const banner = ImageConstraints( 45 maxWidth: 2048, 46 maxSizeBytes: 2 * 1024 * 1024, // 2 MB 47 ); 48} 49 50/// Thrown when image validation fails 51class ImageValidationException implements Exception { 52 const ImageValidationException(this.message); 53 54 final String message; 55 56 @override 57 String toString() => message; 58} 59 60/// Image picker utility functions 61/// 62/// Provides reusable image picking, validation, and MIME type detection. 63/// All methods are static and stateless for easy testing. 64class ImagePickerUtils { 65 // Private constructor to prevent instantiation 66 ImagePickerUtils._(); 67 68 static final ImagePicker _picker = ImagePicker(); 69 70 /// Pick an image from the specified source with optional constraints. 71 /// 72 /// Returns [PickedImage] with file, bytes, and MIME type, 73 /// or null if cancelled. 74 /// Throws [ImageValidationException] if image fails validation. 75 /// 76 /// [source] - ImageSource.gallery or ImageSource.camera 77 /// [constraints] - Optional constraints (defaults to avatar preset) 78 static Future<PickedImage?> pickImage( 79 ImageSource source, { 80 ImageConstraints constraints = ImageConstraints.avatar, 81 }) async { 82 final pickedFile = await _picker.pickImage( 83 source: source, 84 maxWidth: constraints.maxWidth, 85 maxHeight: constraints.maxHeight, 86 imageQuality: constraints.imageQuality, 87 ); 88 89 if (pickedFile == null) { 90 return null; 91 } 92 93 final file = File(pickedFile.path); 94 final bytes = await file.readAsBytes(); 95 final mimeType = inferMimeTypeFromExtension(pickedFile.path); 96 97 validateImage( 98 bytes: bytes, 99 mimeType: mimeType, 100 constraints: constraints, 101 ); 102 103 return PickedImage( 104 file: file, 105 bytes: bytes, 106 mimeType: mimeType, 107 ); 108 } 109 110 /// Infer MIME type from file path extension. 111 /// 112 /// Returns the MIME type string based on file extension. 113 /// Logs a warning and defaults to 'image/jpeg' for unknown extensions. 114 static String inferMimeTypeFromExtension(String path) { 115 final extension = path.split('.').last.toLowerCase(); 116 switch (extension) { 117 case 'jpg': 118 case 'jpeg': 119 return 'image/jpeg'; 120 case 'png': 121 return 'image/png'; 122 case 'webp': 123 return 'image/webp'; 124 case 'gif': 125 return 'image/gif'; 126 case 'heic': 127 case 'heif': 128 return 'image/heic'; 129 default: 130 developer.log( 131 'Unknown image extension ".$extension", defaulting to image/jpeg', 132 name: 'ImagePickerUtils', 133 level: 900, // Warning level 134 ); 135 return 'image/jpeg'; 136 } 137 } 138 139 /// Validate image bytes and MIME type against constraints. 140 /// 141 /// Throws [ImageValidationException] if validation fails. 142 static void validateImage({ 143 required Uint8List bytes, 144 required String mimeType, 145 required ImageConstraints constraints, 146 }) { 147 // Check file size 148 if (bytes.length > constraints.maxSizeBytes) { 149 final maxSizeMB = constraints.maxSizeBytes / (1024 * 1024); 150 throw ImageValidationException( 151 'Image size exceeds maximum of ${maxSizeMB.toStringAsFixed(0)} MB. ' 152 'Please choose a smaller image.', 153 ); 154 } 155 156 // Check MIME type 157 if (!constraints.allowedMimeTypes.contains(mimeType)) { 158 final allowed = constraints.allowedMimeTypes 159 .map((t) => t.split('/').last.toUpperCase()) 160 .join(', '); 161 throw ImageValidationException( 162 'Unsupported image type. Please use $allowed.', 163 ); 164 } 165 } 166}