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

A complete Firebase-native media management Flutter package.

CloudMedia #

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

final items = await CloudMedia.pick();

Table of Contents #


Installation #

Add to your pubspec.yaml:

dependencies:
  cloud_media:
    path: ../cloud_media

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 'firebase_options.dart';

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

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

  await PermissionHandler.initialize();

  await OfflineSyncLayer.instance.initialize();

  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',
          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,
);

Pick with background removal #

final items = await CloudMedia.pick(
  type: CloudMediaType.image,
  enableBackgroundRemoval: true,
);

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)),
);

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,
);

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(),
);

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),
);

Watch status changes #

final watcher = CloudMediaWatcher();

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

Watch multiple items #

final watcher = CloudMediaWatcher();

watcher.watchMultiple([id1, id2, id3]).listen((items) {
  print('${items.length} items updated');
});

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');

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();

UI Widgets #

Media grid #

MediaGrid(
  mediaItems: items,
  crossAxisCount: 3,
  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
SyncStatusIndicator(showAsFloatingAction: true)

Permission-aware picker widget #

PermissionAwareMediaPicker(
  mediaType: CloudMediaType.image,
  maxCount: 5,
  onMediaSelected: (files) {
    print('Got ${files.length} files');
  },
  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 #

MediaLibraryScreen(
  type: CloudMediaType.image,
  onMediaTap: (item) => print('tapped: ${item.id}'),
)

Riverpod Providers #

Watch sync state #

final isSyncing = ref.watch(isMediaSyncingProvider);

if (isSyncing) {
  return const CircularProgressIndicator();
}

Watch pending count #

final pendingAsync = ref.watch(pendingMediaCountProvider);

pendingAsync.when(
  data: (count) => Text('$count pending'),
  loading: () => const CircularProgressIndicator(),
  error: (e, _) => Text('Error: $e'),
);

Watch single media item #

final mediaAsync = ref.watch(mediaStreamProvider(item.id));

mediaAsync.when(
  data: (item) => item != null
      ? Text('Status: ${item!.status.displayName}')
      : const Text('Not found'),
  loading: () => const CircularProgressIndicator(),
  error: (e, _) => Text('Error: $e'),
);

Watch sync status text #

final statusText = ref.watch(syncStatusTextProvider);
// Returns: 'Syncing...', '3 pending items', 'All synced'

Text(statusText)

Error Handling #

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

try {
  final items = await CloudMedia.pick();
} on CloudMediaPermissionDeniedException {
  print('Permission denied');
} 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');
} 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');
}

CloudMediaItem Model #

Every operation returns a CloudMediaItem:

final item = items.first;

item.id           // unique media ID
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
item.width        // image/video width (nullable)
item.height       // image/video height (nullable)
item.duration     // audio/video duration (nullable)
item.storagePath  // Firebase Storage path
item.downloadUrl  // Firebase download URL (empty until synced)
item.thumbnailUrl // thumbnail URL (empty until synced)
item.status       // CloudMediaStatus.pending/syncing/synced/failed/deleted
item.localPath    // local file path (available immediately after pick)
item.createdAt    // DateTime
item.syncedAt     // DateTime (nullable — set when synced)
item.deletedAt    // DateTime (nullable — set when soft deleted)
item.metadata     // Map<String, dynamic> for custom data

Status Lifecycle #

pending → processing → syncing → synced
                               → failed
Status Meaning
pending Selected, waiting in queue
processing Compressing, generating thumbnail
syncing Uploading to Firebase
synced Upload complete, URL available
failed Upload failed permanently
deleted Soft deleted

Configuration #

await CloudMedia.initialize(
  config: const CloudMediaConfig(
    maxCacheSizeMb: 500,        // disk cache limit
    imageQuality: 85,           // WebP compression quality (1–100)
    thumbnailSize: 200,         // thumbnail dimensions (px)
    maxSelection: 20,           // default multi-select limit
    enableOfflineSync: true,    // queue uploads when offline
    enableReviewScreen: true,   // show review screen after picking
    enableBackgroundRemoval: true, // enable BG removal option
    compressAutomatically: true,   // auto compress images to WebP
    autoGenerateThumbnails: true,  // auto generate 200×200 thumbnails
    uploadTimeout: Duration(minutes: 5),
    maxRetries: 3,
    enableLogging: false,       // set true for debug
  ),
);

Platform Support #

Platform Status
Android ✅ Fully supported
iOS ✅ Fully supported
macOS ✅ Supported
Windows ✅ Supported
Linux ✅ Supported
Web ⚠️ Partial — dart:io guards needed

What CloudMedia Handles Automatically #

Feature Automated
Permission requests
Media picker UI
Review screen
Image compression (WebP)
Thumbnail generation (200×200 WebP)
Firebase Storage upload
Firestore metadata write
Offline queue
Auto sync on reconnect
Status tracking
LRU cache (500MB, 30-day TTL)
Error handling (13 typed exceptions)
Background removal (pluggable)
Pause / resume / cancel uploads

License #

MIT