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
6
downloads

Documentation

API reference

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

License

MIT (license)

Dependencies

flutter, mime, path

More

Packages that depend on media_picker_guard