cloud_media 0.0.2
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
- Setup
- Images
- Videos
- Audio
- Files & PDF
- List & Filter
- Upload & Sync
- Delete & Restore
- Download & Share
- Cache
- UI Widgets
- Riverpod Providers
- Error Handling
- Platform Support
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);
Search #
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