media_picker_guard 0.0.1-beta.1
media_picker_guard: ^0.0.1-beta.1 copied to clipboard
Validates media file size, duration, and format before upload with friendly error messages. Prevents server rejections and improves user experience.
example/lib/main.dart
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:media_picker_guard/media_picker_guard.dart';
void main() {
runApp(const MediaPickerGuardExampleApp());
}
class MediaPickerGuardExampleApp extends StatelessWidget {
const MediaPickerGuardExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Media Picker Guard Example',
theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
home: const MediaPickerGuardDemo(),
);
}
}
class MediaPickerGuardDemo extends StatefulWidget {
const MediaPickerGuardDemo({super.key});
@override
State<MediaPickerGuardDemo> createState() => _MediaPickerGuardDemoState();
}
class _MediaPickerGuardDemoState extends State<MediaPickerGuardDemo> {
File? selectedFile;
MediaValidationResult? validationResult;
MediaValidationConfig? currentConfig;
bool isValidating = false;
String selectedValidationType = 'Image';
final ImagePicker _imagePicker = ImagePicker();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('π‘οΈ Media Picker Guard Demo'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildValidationTypeSelector(),
const SizedBox(height: 20),
_buildConfigurationCard(),
const SizedBox(height: 20),
_buildFileSelectionCard(),
const SizedBox(height: 20),
if (selectedFile != null) _buildFileInfoCard(),
const SizedBox(height: 20),
if (validationResult != null) _buildValidationResultCard(),
],
),
),
);
}
Widget _buildValidationTypeSelector() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'π― Validation Type',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
children: ['Image', 'Video', 'Audio', 'Document', 'Custom'].map((
type,
) {
return ChoiceChip(
label: Text(type),
selected: selectedValidationType == type,
onSelected: (selected) {
if (selected) {
setState(() {
selectedValidationType = type;
selectedFile = null;
validationResult = null;
currentConfig = _getConfigForType(type);
});
}
},
);
}).toList(),
),
],
),
),
);
}
Widget _buildConfigurationCard() {
final config = _getConfigForType(selectedValidationType);
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'βοΈ Configuration',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
_buildConfigItem('Media Type', config.mediaType.displayName),
if (config.maxSizeBytes != null)
_buildConfigItem(
'Max Size',
'${(config.maxSizeBytes! / (1024 * 1024)).toStringAsFixed(1)} MB',
),
if (config.minSizeBytes != null)
_buildConfigItem(
'Min Size',
'${(config.minSizeBytes! / 1024).toStringAsFixed(1)} KB',
),
if (config.maxDuration != null)
_buildConfigItem(
'Max Duration',
_formatDuration(config.maxDuration!),
),
if (config.allowedExtensions != null &&
config.allowedExtensions!.isNotEmpty)
_buildConfigItem(
'Allowed Extensions',
config.allowedExtensions!.join(', '),
),
],
),
),
);
}
Widget _buildConfigItem(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 120,
child: Text(
'$label:',
style: const TextStyle(fontWeight: FontWeight.w500),
),
),
Expanded(child: Text(value)),
],
),
);
}
Widget _buildFileSelectionCard() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'π File Selection',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _pickFileFromGallery,
icon: const Icon(Icons.photo_library),
label: const Text('Pick from Gallery'),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
onPressed: _pickFileFromCamera,
icon: const Icon(Icons.camera_alt),
label: const Text('Take Photo'),
),
),
],
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _pickFileFromStorage,
icon: const Icon(Icons.folder_open),
label: const Text('Pick from Files'),
),
),
if (selectedFile != null) ...[
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
const Icon(Icons.attach_file, color: Colors.blue),
const SizedBox(width: 8),
Expanded(
child: Text(
selectedFile!.path.split('/').last,
style: const TextStyle(fontWeight: FontWeight.w500),
),
),
IconButton(
onPressed: () {
setState(() {
selectedFile = null;
validationResult = null;
});
},
icon: const Icon(Icons.close, color: Colors.red),
),
],
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: isValidating ? null : _validateFile,
icon: isValidating
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.security),
label: Text(isValidating ? 'Validating...' : 'Validate File'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
),
),
],
],
),
),
);
}
Widget _buildFileInfoCard() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'π File Information',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
FutureBuilder<Map<String, dynamic>?>(
future: MediaPickerGuard.getFileInfo(selectedFile!),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError || snapshot.data == null) {
return const Text('Unable to get file information');
}
final fileInfo = snapshot.data!;
return Column(
children: [
_buildInfoRow('File Name', fileInfo['fileName']),
_buildInfoRow('File Size', fileInfo['fileSizeFormatted']),
_buildInfoRow('Extension', fileInfo['fileExtension']),
if (fileInfo['mimeType'] != null)
_buildInfoRow('MIME Type', fileInfo['mimeType']),
_buildInfoRow('Is Image', fileInfo['isImage'] ? 'β
' : 'β'),
_buildInfoRow('Is Video', fileInfo['isVideo'] ? 'β
' : 'β'),
_buildInfoRow('Is Audio', fileInfo['isAudio'] ? 'β
' : 'β'),
],
);
},
),
],
),
),
);
}
Widget _buildInfoRow(String label, dynamic value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
children: [
SizedBox(
width: 100,
child: Text(
'$label:',
style: const TextStyle(fontWeight: FontWeight.w500),
),
),
Expanded(child: Text(value?.toString() ?? 'N/A')),
],
),
);
}
Widget _buildValidationResultCard() {
final result = validationResult!;
final isValid = result.isValid;
return Card(
color: isValid ? Colors.green[50] : Colors.red[50],
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
isValid ? Icons.check_circle : Icons.error,
color: isValid ? Colors.green : Colors.red,
size: 24,
),
const SizedBox(width: 8),
Text(
isValid ? 'β
Validation Passed' : 'β Validation Failed',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: isValid ? Colors.green[800] : Colors.red[800],
),
),
],
),
const SizedBox(height: 12),
if (!isValid) ...[
const Text(
'Errors:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
...result.friendlyErrorMessages.map(
(error) => Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('β’ ', style: TextStyle(color: Colors.red)),
Expanded(
child: Text(
error,
style: TextStyle(color: Colors.red[700]),
),
),
],
),
),
),
] else ...[
Text(
'π Your file meets all the validation requirements and is ready for upload!',
style: TextStyle(
color: Colors.green[700],
fontWeight: FontWeight.w500,
),
),
],
if (result.metadata != null) ...[
const SizedBox(height: 12),
const Text(
'Metadata:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
...result.metadata!.entries.map(
(entry) => Text(
'${entry.key}: ${entry.value}',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
),
],
],
),
),
);
}
MediaValidationConfig _getConfigForType(String type) {
switch (type) {
case 'Image':
return MediaPickerGuard.imageUploadConfig(
maxSizeMB: 5,
allowedExtensions: ['jpg', 'jpeg', 'png', 'gif'],
customErrorMessages: {
'fileSizeExceeded':
'π· Image is too large! Please choose an image under 5MB.',
'formatNotAllowed':
'πΌοΈ Only JPG, PNG, and GIF images are allowed.',
},
);
case 'Video':
return MediaPickerGuard.videoUploadConfig(
maxSizeMB: 50,
maxDurationMinutes: 5,
allowedExtensions: ['mp4', 'mov', 'avi'],
customErrorMessages: {
'fileSizeExceeded':
'π¬ Video file is too large! Maximum size is 50MB.',
'durationExceeded':
'β±οΈ Video is too long! Maximum duration is 5 minutes.',
'formatNotAllowed':
'ποΈ Only MP4, MOV, and AVI videos are supported.',
},
);
case 'Audio':
return MediaPickerGuard.audioUploadConfig(
maxSizeMB: 20,
maxDurationMinutes: 10,
allowedExtensions: ['mp3', 'wav', 'aac'],
customErrorMessages: {
'fileSizeExceeded':
'π΅ Audio file is too large! Maximum size is 20MB.',
'durationExceeded':
'β±οΈ Audio is too long! Maximum duration is 10 minutes.',
'formatNotAllowed':
'πΆ Only MP3, WAV, and AAC audio files are supported.',
},
);
case 'Document':
return MediaPickerGuard.documentUploadConfig(
maxSizeMB: 10,
allowedExtensions: ['pdf', 'doc', 'docx', 'txt'],
customErrorMessages: {
'fileSizeExceeded':
'π Document is too large! Maximum size is 10MB.',
'formatNotAllowed':
'π Only PDF, DOC, DOCX, and TXT documents are allowed.',
},
);
case 'Custom':
return MediaValidationConfig(
maxSizeBytes: 2 * 1024 * 1024, // 2MB
minSizeBytes: 1024, // 1KB
allowedExtensions: ['jpg', 'png', 'pdf', 'txt'],
mediaType: MediaType.any,
customErrorMessages: {
'fileSizeExceeded': 'β οΈ File exceeds 2MB limit!',
'fileSizeTooSmall': 'β οΈ File must be at least 1KB!',
'formatNotAllowed': 'β οΈ Only JPG, PNG, PDF, and TXT files allowed!',
},
);
default:
return MediaPickerGuard.imageUploadConfig();
}
}
Future<void> _pickFileFromGallery() async {
try {
final XFile? image = await _imagePicker.pickImage(
source: ImageSource.gallery,
);
if (image != null) {
setState(() {
selectedFile = File(image.path);
validationResult = null;
});
}
} catch (e) {
_showErrorSnackBar('Error picking image from gallery: $e');
}
}
Future<void> _pickFileFromCamera() async {
try {
final XFile? image = await _imagePicker.pickImage(
source: ImageSource.camera,
);
if (image != null) {
setState(() {
selectedFile = File(image.path);
validationResult = null;
});
}
} catch (e) {
_showErrorSnackBar('Error taking photo: $e');
}
}
Future<void> _pickFileFromStorage() async {
try {
FilePickerResult? result = await FilePicker.platform.pickFiles();
if (result != null) {
setState(() {
selectedFile = File(result.files.single.path!);
validationResult = null;
});
}
} catch (e) {
_showErrorSnackBar('Error picking file: $e');
}
}
Future<void> _validateFile() async {
if (selectedFile == null) return;
setState(() {
isValidating = true;
});
try {
final config = _getConfigForType(selectedValidationType);
final result = await MediaPickerGuard.validateFile(
selectedFile!,
config: config,
);
setState(() {
validationResult = result;
currentConfig = config;
});
// Show success/error snackbar
if (result.isValid) {
_showSuccessSnackBar('β
File validation passed!');
} else {
_showErrorSnackBar(
'β File validation failed: ${result.firstFriendlyErrorMessage}',
);
}
} catch (e) {
_showErrorSnackBar('Error during validation: $e');
} finally {
setState(() {
isValidating = false;
});
}
}
void _showSuccessSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.green,
duration: const Duration(seconds: 3),
),
);
}
void _showErrorSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
duration: const Duration(seconds: 3),
),
);
}
String _formatDuration(Duration duration) {
final minutes = duration.inMinutes;
final seconds = duration.inSeconds % 60;
if (minutes > 0) {
return '${minutes}m ${seconds}s';
} else {
return '${seconds}s';
}
}
}