cloud_media 0.0.3 copy "cloud_media: ^0.0.3" to clipboard
cloud_media: ^0.0.3 copied to clipboard

A complete Firebase-native media management Flutter package. One line to pick. Everything else automated.

CloudMedia v0.0.2 #

A complete Firebase-native media management Flutter package.
One line to pick. One model returned. Everything else automated.

Now with ONNX-powered background removal, video thumbnails, and native image cropping!

final items = await CloudMedia.pick();

Table of Contents #


New in v0.0.2 #

  • Background Removal — On-device ONNX model (image_background_remover) with 30s timeout and progress indicator
  • Video Thumbnails — Automatic video previews (flutter_video_thumbnail_plus)
  • Image Cropping — Full native cropping UI (image_cropper) with rectangle/circle support
  • Improved Performance — Faster thumbnail generation with square crop
  • Better Error Handling — 13 typed exceptions covering all scenarios
  • Enhanced Documentation — Full API reference with examples

Installation #

Add to your pubspec.yaml:

dependencies:
  cloud_media:
    path: ../cloud_media
    # or from git:
    # git:
    #   url: https://github.com/yourusername/cloud_media.git

Then run:

flutter pub get

Setup #

Android — android/app/src/main/AndroidManifest.xml #

<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>

iOS — ios/Runner/Info.plist #

<key>NSCameraUsageDescription</key>
<string>Required to pick photos and videos</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Required to access your media library</string>
<key>NSMicrophoneUsageDescription</key>
<string>Required to record audio</string>

Firebase Security Rules #

Firestore:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId}/media/{mediaId} {
      allow read, write: if request.auth.uid == userId;
    }
  }
}

Storage:

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /users/{userId}/{allPaths=**} {
      allow read, write: if request.auth.uid == userId;
    }
  }
}

main.dart #

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:permission_handler_package/permission_handler_package.dart';
import 'package:riverpod_offline_sync/riverpod_offline_sync.dart';
import 'package:cloud_media/cloud_media.dart';
import 'firebase_options.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  await PermissionHandler.initialize();

  await OfflineSyncLayer.instance.initialize(
    config: const SyncConfig(
      autoSyncOnReconnect: true,
      syncImmediately: true,
      maxConcurrentOperations: 2,
      enableMetrics: true,
      enableDebugLogging: false,
      syncOnWiFiOnly: false,
      maxRetries: 5,
      initialRetryDelay: Duration(seconds: 2),
      maxQueueSize: 500,
    ),
  );

  await CloudMedia.initialize(
    config: const CloudMediaConfig(
      maxCacheSizeMb: 500,
      imageQuality: 85,
      thumbnailSize: 200,
      maxSelection: 20,
      enableOfflineSync: true,
      enableReviewScreen: true,
      enableBackgroundRemoval: true,
      enableLogging: true,
      compressAutomatically: true,
      autoGenerateThumbnails: true,
    ),
  );

  runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ScreenUtilInit(
      designSize: const Size(375, 812),
      minTextAdapt: true,
      builder: (context, child) {
        return MaterialApp(
          title: 'My App',
          theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
          home: child,
        );
      },
      child: const MyHomePage(),
    );
  }
}

Images #

Pick single image #

final items = await CloudMedia.pick();
final image = items.first;

Pick multiple images #

final items = await CloudMedia.pick(
  type: CloudMediaType.image,
  maxCount: 10,
);

Pick with crop and rotate #

final items = await CloudMedia.pick(
  type: CloudMediaType.image,
  enableEditing: true,  // Shows cropping UI automatically
);

Pick with background removal #

Now uses ONNX Runtime + u2net model — runs entirely on-device, no API calls!

final items = await CloudMedia.pick(
  type: CloudMediaType.image,
  enableBackgroundRemoval: true,  // ONNX-powered, 30s timeout
);

The background removal screen shows:

  • Animated progress indicator (0-100%)
  • Cancel button to use original image
  • Retry option on timeout/failure
  • 30-second timeout with fallback

Performance: ~2-5 seconds on modern devices for 1080p images.

Display image #

CloudImage(
  media: item,
  width: 200,
  height: 200,
  fit: BoxFit.cover,
)

Display with zoom #

CloudImage(
  media: item,
  enableZoom: true,
)

Display with Hero animation #

Hero(
  tag: 'img_${item.id}',
  child: CloudImage(media: item),
)

Get all images #

final images = await CloudMedia.list(
  type: CloudMediaType.image,
);

Get recent images (last 7 days) #

final recent = await CloudMedia.list(
  type: CloudMediaType.image,
  startDate: DateTime.now().subtract(const Duration(days: 7)),
);

Image Cropping #

Crop after picking (built-in) #

final items = await CloudMedia.pick(
  type: CloudMediaType.image,
  enableEditing: true,  // ← Shows cropping UI automatically
);

Manual cropping dialog #

import 'package:cloud_media/cloud_media.dart';

// Square crop (1:1) — perfect for profile pictures
final cropped = await CroppingDialog.showSquareCrop(
  context: context,
  imagePath: '/path/to/image.jpg',
);

// Circle crop — perfect for avatars
final cropped = await CroppingDialog.showCircleCrop(
  context: context,
  imagePath: '/path/to/image.jpg',
);

// Wide screen crop (16:9)
final cropped = await CroppingDialog.showWideScreenCrop(
  context: context,
  imagePath: '/path/to/image.jpg',
);

// Custom aspect ratio
final cropped = await CroppingDialog.show(
  context: context,
  imagePath: '/path/to/image.jpg',
  cropStyle: CropStyle.rectangle,  // rectangle or circle
  aspectRatio: 16 / 9,             // locked aspect ratio
  allowRotation: true,             // show rotation controls
);

if (cropped != null) {
  print('Cropped image saved to: ${cropped.path}');
  final size = await cropped.length();
  print('File size: ${FileUtils.formatFileSize(size)}');
}

CroppingDialog API #

Method Description
CroppingDialog.show() Full customizable crop dialog
CroppingDialog.showSquareCrop() 1:1 aspect ratio (profile pictures)
CroppingDialog.showCircleCrop() Circular crop (avatars)
CroppingDialog.showWideScreenCrop() 16:9 aspect ratio (banners)

Parameters for .show():

Parameter Type Default Description
cropStyle CropStyle rectangle rectangle or circle
aspectRatio double? null Locked aspect ratio
allowRotation bool true Show rotation controls

Videos #

Pick single video #

final items = await CloudMedia.pick(
  type: CloudMediaType.video,
);

Pick with editing #

final items = await CloudMedia.pick(
  type: CloudMediaType.video,
  enableEditing: true,
);

Display video player #

CloudVideo(
  media: item,
  width: double.infinity,
  height: 300,
  autoPlay: true,
  showControls: true,
)

Display as thumbnail (no controls) #

CloudVideo(
  media: item,
  showControls: false,
  height: 150,
)

Full width player #

CloudVideo(
  media: item,
  width: MediaQuery.of(context).size.width,
  showControls: true,
)

Get all videos #

final videos = await CloudMedia.list(
  type: CloudMediaType.video,
);

Video Thumbnails (Automatic) #

Video thumbnails are automatically generated using flutter_video_thumbnail_plus:

final items = await CloudMedia.pick(type: CloudMediaType.video);
// Thumbnail generated at 1 second mark, 200×200 WebP, 80% quality

CloudVideo(media: items.first);  // Shows thumbnail before play

Thumbnail settings are configurable:

CloudMedia.initialize(
  config: CloudMediaConfig(
    thumbnailSize: 300,              // default: 200
    autoGenerateThumbnails: true,    // default: true
  ),
);

Audio #

Pick single audio file #

final items = await CloudMedia.pick(
  type: CloudMediaType.audio,
);

Pick multiple audio files #

final items = await CloudMedia.pick(
  type: CloudMediaType.audio,
  maxCount: 5,
);

Display audio player #

CloudAudio(
  media: item,
  autoPlay: false,
)

Auto-play audio #

CloudAudio(
  media: item,
  autoPlay: true,
)

Get all audio files #

final audio = await CloudMedia.list(
  type: CloudMediaType.audio,
);

Files & PDF #

Pick PDF #

final items = await CloudMedia.pick(
  type: CloudMediaType.file,
);

Display file tile #

CloudFile(
  media: item,
  onDownload: () async => await CloudMedia.download(item.id),
  onShare: () async => await CloudMedia.share(item.id),
)

Get all documents #

final docs = await CloudMedia.list(
  type: CloudMediaType.file,
);

List & Filter #

All media #

final all = await CloudMedia.list();

First 20 items #

final recent = await CloudMedia.list(limit: 20);

Filter by type #

final images = await CloudMedia.list(
  type: CloudMediaType.image,
);

Filter by date range #

final filtered = await CloudMedia.list(
  startDate: DateTime(2025, 1, 1),
  endDate: DateTime.now(),
);

Pagination #

final page1 = await CloudMedia.list(limit: 20, offset: 0);
final page2 = await CloudMedia.list(limit: 20, offset: 20);
final results = await CloudMedia.list(
  searchQuery: 'vacation',
);

With options #

final results = await CloudMedia.list(
  type: CloudMediaType.image,
  limit: 50,
  startDate: DateTime(2025, 1, 1),
  endDate: DateTime.now(),
);

Using CloudMediaListExtension #

// List with options object
final options = CloudMediaListOptions(
  type: CloudMediaType.image,
  limit: 20,
  startDate: DateTime(2025, 1, 1),
);
final items = await CloudMedia.listWithOptions(options);

// List by type
final images = await CloudMedia.listByType(CloudMediaType.image, limit: 30);

// List recent
final recent = await CloudMedia.listRecent(limit: 10);

// List by date range
final range = await CloudMedia.listByDateRange(
  DateTime(2025, 1, 1),
  DateTime(2025, 12, 31),
  type: CloudMediaType.image,
);

Upload & Sync #

Watch upload progress #

final watcher = CloudMediaWatcher();

watcher.watchUploadProgress(item.id).listen((progress) {
  print('${(progress * 100).toStringAsFixed(0)}%');
});

Wait until synced #

final watcher = CloudMediaWatcher();

final synced = await watcher.watchUntil(
  item.id,
  CloudMediaStatus.synced,
  timeout: const Duration(seconds: 60),
);
print('Upload complete! URL: ${synced.downloadUrl}');

Watch status changes #

final watcher = CloudMediaWatcher();

watcher.watchStatus(item.id).listen((status) {
  print(status.displayName); // Pending, Syncing, Synced, Failed
});

Watch with status filter #

final watcher = CloudMediaWatcher();

watcher.watchWithStatusFilter(
  item.id,
  [CloudMediaStatus.syncing, CloudMediaStatus.synced],
).listen((item) {
  print('Status changed: ${item.status}');
});

Watch multiple items #

final watcher = CloudMediaWatcher();

watcher.watchMultiple([id1, id2, id3]).listen((items) {
  print('${items.length} items updated');
  final synced = items.where((i) => i.status == CloudMediaStatus.synced);
  print('${synced.length} completed');
});

Watch with Riverpod #

CloudMedia.watch(item.id).listen((updated) {
  print(updated.status);
  print(updated.downloadUrl);
});

Force sync #

await CloudMedia.sync();

Check pending count #

final count = await CloudMedia.getPendingCount();
print('$count items waiting to upload');

Pause/Resume/Cancel upload #

// Using StorageQueueService
StorageQueueService.pauseUpload(mediaId);
StorageQueueService.resumeUpload(mediaId);
StorageQueueService.cancelUpload(mediaId);

Delete & Restore #

Delete by ID #

await CloudMedia.delete(item.id);

Delete by item reference #

await CloudMedia.deleteRef(item);

Delete all images #

final images = await CloudMedia.list(type: CloudMediaType.image);
for (final item in images) {
  await CloudMedia.delete(item.id);
}

Restore soft-deleted item #

await CloudMedia.restore(item.id);

Download & Share #

Download to device #

final localPath = await CloudMedia.download(item.id);
print('Saved to: $localPath');

Share via platform sheet #

await CloudMedia.share(item.id);

Get single item #

final item = await CloudMedia.get(mediaId);
print(item.fileName);
print(item.status.displayName);
print(item.type.displayName);

Share latest item #

final latest = (await CloudMedia.list(limit: 1)).first;
await CloudMedia.share(latest.id);

Cache #

Clear all cache #

await CloudMedia.clearCache();

Check cache size #

final cacheService = CacheService(config: CloudMediaConfig());
await cacheService.initialize();
final size = await cacheService.getCacheSize();
print('Cache size: ${FileUtils.formatFileSize(size)}');

UI Widgets #

Media grid #

MediaGrid(
  mediaItems: items,
  crossAxisCount: 3,
  crossAxisSpacing: 4,
  mainAxisSpacing: 4,
  onItemTap: (item) => print('tapped: ${item.id}'),
  onItemLongPress: (item) => print('long pressed: ${item.id}'),
)

Upload progress bar #

UploadProgress(
  uploadId: item.id,
  showDetails: true,
)

Upload progress with pause/resume/cancel #

UploadProgressIndicator(
  mediaId: item.id,
  onComplete: () => print('Done!'),
)

Sync status indicator #

// In AppBar actions
SyncStatusIndicator(showLabel: false)

// Inline with label
SyncStatusIndicator(showLabel: true)

// Floating action button style
SyncStatusIndicator(showAsFloatingAction: true)

Permission-aware picker widget #

PermissionAwareMediaPicker(
  mediaType: CloudMediaType.image,
  maxCount: 5,
  permissionTitle: 'Photos Access',
  permissionMessage: 'This app needs access to your photos.',
  onMediaSelected: (files) {
    print('Got ${files.length} files');
    for (final file in files) {
      print('File: ${file.name}');
    }
  },
  child: ElevatedButton(
    onPressed: () {},
    child: const Text('Pick Images'),
  ),
)

Permission-aware FAB #

PermissionAwareMediaPicker(
  mediaType: CloudMediaType.image,
  maxCount: 10,
  onMediaSelected: (files) => loadAllMedia(),
  child: FloatingActionButton(
    onPressed: () {},
    child: const Icon(Icons.add_photo_alternate),
  ),
)

Full media library screen #

Navigator.push(
  context,
  MaterialPageRoute(
    builder: (_) => MediaLibraryScreen(
      type: CloudMediaType.image,  // optional filter
      onMediaTap: (item) => print('tapped: ${item.id}'),
    ),
  ),
);

Review screen #

final items = await CloudMedia.pick();
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (_) => ReviewScreen(
      mediaItems: items,
      onConfirm: () => print('Confirmed!'),
      onCancel: () => print('Cancelled'),
    ),
  ),
);

Dialogs #

Error Dialog #

ErrorDialog.show(
  context,
  title: 'Upload Failed',
  message: 'Unable to upload image. Please check your connection.',
  onRetry: () => retryUpload(),
  onDismiss: () => print('Dismissed'),
);

Loading Dialog #

// Show
LoadingDialog.show(
  context,
  message: 'Processing...',
);

// Show with progress
LoadingDialog.show(
  context,
  message: 'Uploading...',
  showProgress: true,
  progress: 0.65,
);

// Hide
LoadingDialog.hide(context);

Confirmation Dialog #

final confirmed = await ConfirmationDialog.show(
  context,
  title: 'Delete Media',
  message: 'Are you sure you want to delete this file?',
  confirmText: 'Delete',
  cancelText: 'Cancel',
  confirmColor: Colors.red,
  onConfirm: () => deleteFile(),
);

if (confirmed == true) {
  print('User confirmed');
}

Cropping Dialog #

// Square crop
final cropped = await CroppingDialog.showSquareCrop(
  context: context,
  imagePath: imagePath,
);

// Circle crop
final cropped = await CroppingDialog.showCircleCrop(
  context: context,
  imagePath: imagePath,
);

// Custom crop
final cropped = await CroppingDialog.show(
  context: context,
  imagePath: imagePath,
  cropStyle: CropStyle.rectangle,
  aspectRatio: 16 / 9,
  allowRotation: true,
);

Riverpod Providers #

Watch sync state #

class SyncStatusWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final isSyncing = ref.watch(isMediaSyncingProvider);
    
    if (isSyncing) {
      return const CircularProgressIndicator();
    }
    return const Icon(Icons.cloud_done);
  }
}

Watch pending count #

class PendingBadge extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final pendingAsync = ref.watch(pendingMediaCountProvider);
    
    return pendingAsync.when(
      data: (count) => count > 0
          ? Badge(label: Text('$count'), child: Icon(Icons.sync))
          : const SizedBox.shrink(),
      loading: () => const CircularProgressIndicator(),
      error: (e, _) => const Icon(Icons.error),
    );
  }
}

Watch single media item #

class MediaStatusWidget extends ConsumerWidget {
  final String mediaId;
  
  const MediaStatusWidget({required this.mediaId});
  
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final mediaAsync = ref.watch(mediaStreamProvider(mediaId));
    
    return mediaAsync.when(
      data: (item) => item != null
          ? Row(
              children: [
                Icon(item!.status.icon, color: item.status.color),
                const SizedBox(width: 8),
                Text(item.status.displayName),
              ],
            )
          : const Text('Not found'),
      loading: () => const CircularProgressIndicator(),
      error: (e, _) => Text('Error: $e'),
    );
  }
}

Watch sync status text #

class SyncStatusText extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final statusText = ref.watch(syncStatusTextProvider);
    // Returns: 'Syncing...', '3 pending items', 'All synced'
    
    return Text(statusText);
  }
}

Watch connectivity #

class ConnectivityWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final isConnected = ref.watch(isConnectedProvider);
    
    return Icon(
      isConnected ? Icons.wifi : Icons.wifi_off,
      color: isConnected ? Colors.green : Colors.red,
    );
  }
}

Get queue breakdown #

class QueueBreakdown extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final breakdownAsync = ref.watch(queueBreakdownProvider);
    
    return breakdownAsync.when(
      data: (breakdown) => Column(
        children: breakdown.entries.map((e) => 
          Text('${e.key}: ${e.value}')
        ).toList(),
      ),
      loading: () => const CircularProgressIndicator(),
      error: (e, _) => Text('Error: $e'),
    );
  }
}

Error Handling #

All exceptions are typed. No need to inspect Firebase errors directly.

try {
  final items = await CloudMedia.pick();
} on CloudMediaPermissionDeniedException {
  print('Permission denied');
  // Show user-friendly message
} on CloudMediaPermissionPermanentlyDeniedException {
  print('Permanently denied — opening settings');
  await PermissionService.openSettings();
} on CloudMediaFileTooLargeException catch (e) {
  print(e.message); // 'File 15MB exceeds 10MB limit'
} on CloudMediaUnsupportedFileTypeException catch (e) {
  print(e.message); // 'File type .gif not supported'
} on CloudMediaSelectionLimitExceededException {
  print('Too many files selected (max 100)');
} on CloudMediaUploadFailedException {
  print('Upload failed — will retry when online');
} on CloudMediaNetworkException {
  print('No internet connection');
} on CloudMediaNotFoundException {
  print('Media not found');
} on CloudMediaSyncException {
  print('Sync failed');
} on CloudMediaOfflineQueueException {
  print('Queue error');
} on CloudMediaCompressionException {
  print('Compression failed');
} on CloudMediaThumbnailGenerationException {
  print('Thumbnail generation failed');
} on CloudMediaBackgroundRemovalTimeoutException {
  print('Background removal timed out after 30 seconds');
} catch (e) {
  print('Unknown error: $e');
}

Using ErrorHandler #

try {
  await CloudMedia.pick();
} catch (e) {
  final error = ErrorHandler.handle(e);
  print('Code: ${error.code}');
  print('Message: ${error.message}');
  
  if (ErrorHandler.isNetworkError(e)) {
    print('Network issue detected');
  }
  
  if (ErrorHandler.isPermissionError(e)) {
    print('Permission issue detected');
  }
}

CloudMediaItem Model #

Every operation returns a CloudMediaItem:

final item = items.first;

// Basic info
item.id           // unique media ID (UUID)
item.userId       // Firebase Auth UID
item.type         // CloudMediaType.image / video / audio / file
item.fileName     // original file name
item.mimeType     // image/jpeg, video/mp4, etc.
item.size         // file size in bytes

// Media dimensions
item.width        // image/video width (nullable)
item.height       // image/video height (nullable)
item.duration     // audio/video duration in seconds (nullable)

// Storage
item.storagePath  // Firebase Storage path
item.downloadUrl  // Firebase download URL (empty until synced)
item.thumbnailUrl // thumbnail URL (empty until synced)
item.localPath    // local file path (available immediately after pick)

// Status
item.status       // CloudMediaStatus.pending/syncing/synced/failed/deleted

// Timestamps
item.createdAt    // DateTime when picked
item.syncedAt     // DateTime when synced (nullable)
item.deletedAt    // DateTime when soft deleted (nullable)

// Metadata
item.metadata     // Map<String, dynamic> for custom data

CopyWith method #

final updated = item.copyWith(
  status: CloudMediaStatus.synced,
  downloadUrl: 'https://...',
  metadata: {'custom': 'data'},
);

Status Lifecycle #

pending → processing → syncing → synced
                               → failed
Status Icon Color Meaning
pending Orange Selected, waiting in queue
processing 🔧 Blue Compressing, generating thumbnail
syncing 🔄 Purple Uploading to Firebase
synced Green Upload complete, URL available
failed Red Upload failed permanently
deleted 🗑️ Grey Soft deleted

Status helpers #

item.status.isFinal        // true for synced/failed/deleted
item.status.isUploading    // true for processing/syncing
item.status.displayName    // Human-readable name
item.status.icon           // Material icon
item.status.color          // Status color

Configuration #

await CloudMedia.initialize(
  config: const CloudMediaConfig(
    // Cache
    maxCacheSizeMb: 500,        // disk cache limit (default: 500)
    
    // Image Processing
    imageQuality: 85,           // WebP compression quality 1-100 (default: 85)
    thumbnailSize: 200,         // thumbnail dimensions in px (default: 200)
    
    // Selection
    maxSelection: 20,           // default multi-select limit (default: 20)
    
    // Features
    enableOfflineSync: true,    // queue uploads when offline (default: true)
    enableReviewScreen: true,   // show review screen after picking (default: true)
    enableBackgroundRemoval: true, // enable BG removal option (default: true)
    
    // Auto-processing
    compressAutomatically: true,   // auto compress images to WebP (default: true)
    autoGenerateThumbnails: true,  // auto generate thumbnails (default: true)
    
    // Advanced
    enableVideoCompression: false,  // video compression (pass-through) (default: false)
    videoCompressionBitrate: 1000000, // target bitrate for video (default: 1Mbps)
    
    // Timeouts
    uploadTimeout: Duration(minutes: 5), // upload timeout (default: 5 min)
    maxRetries: 3,               // max upload retries (default: 3)
    
    // Debug
    enableLogging: false,        // set true for debug logs (default: false)
    
    // Storage
    customStorageBucket: null,   // custom Firebase Storage bucket (default: null)
  ),
);

Platform Support #

Platform Status Notes
Android ✅ Fully supported Full feature set
iOS ✅ Fully supported Full feature set
macOS ✅ Supported Most features work
Windows ✅ Supported Most features work
Linux ✅ Supported Most features work
Web ⚠️ Partial dart:io guards needed; limited file picker

What CloudMedia Handles Automatically #

Feature Automated Description
Permission requests Handles all platform permissions
Media picker UI Built-in picker for all types
Review screen Preview before upload
Image compression (WebP) 40-80% size reduction
Image thumbnail generation 200×200 WebP on-device
Video thumbnail generation Frame at 1 second mark
Firebase Storage upload Automatic with retry
Firestore metadata write Status tracking
Offline queue Persists across app restarts
Auto sync on reconnect When connectivity returns
Status tracking Real-time updates
LRU cache 500MB limit, 30-day TTL
Error handling 13 typed exceptions
Background removal ONNX on-device model
Pause/Resume/Cancel uploads Full control
Image cropping Native UI
File validation Type, size, count
Logging Configurable debug logs

Troubleshooting #

Background removal not working? #

Ensure you have added the ONNX model to your assets:

flutter:
  assets:
    - assets/models/u2net.onnx  # Download from release page

Cropping dialog type conflict? #

If you see CropStyle conflicts, use the alias:

import 'package:image_cropper/image_cropper.dart' as image_cropper;

Video thumbnails not generating? #

Add to AndroidManifest.xml:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

For iOS, add to Info.plist:

<key>NSPhotoLibraryAddUsageDescription</key>
<string>Required to save thumbnails</string>

Upload stuck at pending? #

Check:

  1. Internet connection
  2. Firebase Authentication (user must be logged in)
  3. Firebase Storage rules
  4. Run await CloudMedia.sync() to force sync

Cache not clearing? #

// Force clear
await CloudMedia.clearCache();

// Also clear Hive box
final box = await Hive.openBox('cloud_media_cache');
await box.clear();

Permission issues on Android 13+? #

Add these to AndroidManifest.xml:

<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>

Debug logging #

Enable logging to see what's happening:

CloudMedia.initialize(
  config: const CloudMediaConfig(enableLogging: true),
);

// Also enable debug logs
CloudLogger.isDebugEnabled = true;

Dependencies (automatically included) #

Package Version Purpose
firebase_core ^4.10.0 Firebase core
firebase_auth ^6.5.2 Authentication
firebase_storage ^13.4.2 Cloud Storage
cloud_firestore ^6.5.0 Firestore
image_picker ^1.1.2 Media picking
file_picker ^12.0.0-beta.5 File picking
video_player ^2.9.2 Video playback
audioplayers ^6.1.0 Audio playback
flutter_image_compress ^2.3.0 Image compression
image ^4.2.0 Thumbnail generation
image_background_remover ^2.0.0 ONNX background removal
flutter_video_thumbnail_plus ^1.0.6 Video thumbnails
image_cropper ^11.0.0 Native cropping
photo_view ^0.15.0 Zoomable images
cached_network_image ^3.4.1 Image caching
riverpod_offline_sync ^1.0.5 Offline queue
hive_flutter ^1.1.0 Local cache
permission_handler_package ^1.0.6 Permissions
share_plus ^13.1.0 Sharing
intl ^0.20.2 Date formatting

License #

MIT


Made with ❤️ for Flutter community