media_picker_guard 0.0.1-beta.1 copy "media_picker_guard: ^0.0.1-beta.1" to clipboard
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';
    }
  }
}
1
likes
150
points
2
downloads

Publisher

verified publishermuz.satech.dev

Weekly Downloads

Validates media file size, duration, and format before upload with friendly error messages. Prevents server rejections and improves user experience.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

flutter, mime, path

More

Packages that depend on media_picker_guard