feat: Add logger service and improve expense dialog with enhanced receipt management and calculation logic.
This commit is contained in:
@@ -14,17 +14,18 @@
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// final storageService = StorageService();
|
||||
///
|
||||
///
|
||||
/// // Upload a receipt image
|
||||
/// final receiptUrl = await storageService.uploadReceiptImage(groupId, imageFile);
|
||||
///
|
||||
///
|
||||
/// // Upload a profile image
|
||||
/// final profileUrl = await storageService.uploadProfileImage(userId, imageFile);
|
||||
///
|
||||
///
|
||||
/// // Delete a file
|
||||
/// await storageService.deleteFile(fileUrl);
|
||||
/// ```
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:firebase_storage/firebase_storage.dart';
|
||||
@@ -36,34 +37,32 @@ import 'error_service.dart';
|
||||
class StorageService {
|
||||
/// Firebase Storage instance for file operations
|
||||
final FirebaseStorage _storage;
|
||||
|
||||
|
||||
/// Service for error handling and logging
|
||||
final ErrorService _errorService;
|
||||
|
||||
/// Constructor for StorageService.
|
||||
///
|
||||
///
|
||||
/// Args:
|
||||
/// [storage]: Optional Firebase Storage instance (auto-created if null)
|
||||
/// [errorService]: Optional error service instance (auto-created if null)
|
||||
StorageService({
|
||||
FirebaseStorage? storage,
|
||||
ErrorService? errorService,
|
||||
}) : _storage = storage ?? FirebaseStorage.instance,
|
||||
_errorService = errorService ?? ErrorService();
|
||||
StorageService({FirebaseStorage? storage, ErrorService? errorService})
|
||||
: _storage = storage ?? FirebaseStorage.instance,
|
||||
_errorService = errorService ?? ErrorService();
|
||||
|
||||
/// Uploads a receipt image for an expense with automatic compression.
|
||||
///
|
||||
///
|
||||
/// Validates the image file, compresses it to JPEG format with 85% quality,
|
||||
/// generates a unique filename, and uploads it with appropriate metadata.
|
||||
/// Monitors upload progress and logs it for debugging purposes.
|
||||
///
|
||||
///
|
||||
/// Args:
|
||||
/// [groupId]: ID of the group this receipt belongs to
|
||||
/// [imageFile]: The image file to upload
|
||||
///
|
||||
///
|
||||
/// Returns:
|
||||
/// A Future<String> containing the download URL of the uploaded image
|
||||
///
|
||||
/// A `Future<String>` containing the download URL of the uploaded image
|
||||
///
|
||||
/// Throws:
|
||||
/// Exception if file validation fails or upload encounters an error
|
||||
Future<String> uploadReceiptImage(String groupId, File imageFile) async {
|
||||
@@ -95,8 +94,12 @@ class StorageService {
|
||||
|
||||
// Progress monitoring (optional)
|
||||
uploadTask.snapshotEvents.listen((TaskSnapshot snapshot) {
|
||||
final progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
|
||||
_errorService.logInfo('StorageService', 'Upload progress: ${progress.toStringAsFixed(1)}%');
|
||||
final progress =
|
||||
(snapshot.bytesTransferred / snapshot.totalBytes) * 100;
|
||||
_errorService.logInfo(
|
||||
'StorageService',
|
||||
'Upload progress: ${progress.toStringAsFixed(1)}%',
|
||||
);
|
||||
});
|
||||
|
||||
// Wait for completion
|
||||
@@ -105,9 +108,11 @@ class StorageService {
|
||||
// Get download URL
|
||||
final downloadUrl = await snapshot.ref.getDownloadURL();
|
||||
|
||||
_errorService.logSuccess('StorageService', 'Image uploaded successfully: $fileName');
|
||||
_errorService.logSuccess(
|
||||
'StorageService',
|
||||
'Image uploaded successfully: $fileName',
|
||||
);
|
||||
return downloadUrl;
|
||||
|
||||
} catch (e) {
|
||||
_errorService.logError('StorageService', 'Error uploading image: $e');
|
||||
rethrow;
|
||||
@@ -115,10 +120,10 @@ class StorageService {
|
||||
}
|
||||
|
||||
/// Deletes a receipt image from storage.
|
||||
///
|
||||
///
|
||||
/// Extracts the storage reference from the provided URL and deletes the file.
|
||||
/// Does not throw errors to avoid blocking expense deletion operations.
|
||||
///
|
||||
///
|
||||
/// Args:
|
||||
/// [imageUrl]: The download URL of the image to delete
|
||||
Future<void> deleteReceiptImage(String imageUrl) async {
|
||||
@@ -137,17 +142,17 @@ class StorageService {
|
||||
}
|
||||
|
||||
/// Compresses an image to optimize storage space and upload speed.
|
||||
///
|
||||
///
|
||||
/// Reads the image file, decodes it, resizes it if too large (max 1024x1024),
|
||||
/// and encodes it as JPEG with 85% quality for optimal balance between
|
||||
/// file size and image quality.
|
||||
///
|
||||
///
|
||||
/// Args:
|
||||
/// [imageFile]: The image file to compress
|
||||
///
|
||||
///
|
||||
/// Returns:
|
||||
/// A Future<Uint8List> containing the compressed image bytes
|
||||
///
|
||||
/// A `Future<Uint8List>` containing the compressed image bytes
|
||||
///
|
||||
/// Throws:
|
||||
/// Exception if the image cannot be decoded or processed
|
||||
Future<Uint8List> _compressImage(File imageFile) async {
|
||||
@@ -175,9 +180,11 @@ class StorageService {
|
||||
|
||||
// Encode as JPEG with compression
|
||||
final compressedBytes = img.encodeJpg(image, quality: 85);
|
||||
|
||||
_errorService.logInfo('StorageService',
|
||||
'Image compressed: ${bytes.length} → ${compressedBytes.length} bytes');
|
||||
|
||||
_errorService.logInfo(
|
||||
'StorageService',
|
||||
'Image compressed: ${bytes.length} → ${compressedBytes.length} bytes',
|
||||
);
|
||||
|
||||
return Uint8List.fromList(compressedBytes);
|
||||
} catch (e) {
|
||||
@@ -188,13 +195,13 @@ class StorageService {
|
||||
}
|
||||
|
||||
/// Validates an image file before upload.
|
||||
///
|
||||
///
|
||||
/// Checks file existence, size constraints (max 10MB), and file extension
|
||||
/// to ensure only valid image files are processed for upload.
|
||||
///
|
||||
///
|
||||
/// Args:
|
||||
/// [imageFile]: The image file to validate
|
||||
///
|
||||
///
|
||||
/// Throws:
|
||||
/// Exception if validation fails (file doesn't exist, too large, or invalid extension)
|
||||
void _validateImageFile(File imageFile) {
|
||||
@@ -219,13 +226,13 @@ class StorageService {
|
||||
}
|
||||
|
||||
/// Generates a unique filename for a receipt image.
|
||||
///
|
||||
///
|
||||
/// Creates a filename using timestamp, microseconds, and group ID to ensure
|
||||
/// uniqueness and prevent naming conflicts when multiple receipts are uploaded.
|
||||
///
|
||||
///
|
||||
/// Args:
|
||||
/// [groupId]: ID of the group this receipt belongs to
|
||||
///
|
||||
///
|
||||
/// Returns:
|
||||
/// A unique filename string for the receipt image
|
||||
String _generateReceiptFileName(String groupId) {
|
||||
@@ -235,21 +242,23 @@ class StorageService {
|
||||
}
|
||||
|
||||
/// Uploads multiple images simultaneously (for future features).
|
||||
///
|
||||
///
|
||||
/// Processes multiple image files in parallel for batch upload scenarios.
|
||||
/// Each image is validated, compressed, and uploaded with unique filenames.
|
||||
///
|
||||
///
|
||||
/// Args:
|
||||
/// [groupId]: ID of the group these images belong to
|
||||
/// [imageFiles]: List of image files to upload
|
||||
///
|
||||
///
|
||||
/// Returns:
|
||||
/// A Future<List<String>> containing download URLs of uploaded images
|
||||
/// A `Future<List<String>>` containing download URLs of uploaded images
|
||||
Future<List<String>> uploadMultipleImages(
|
||||
String groupId,
|
||||
String groupId,
|
||||
List<File> imageFiles,
|
||||
) async {
|
||||
final uploadTasks = imageFiles.map((file) => uploadReceiptImage(groupId, file));
|
||||
final uploadTasks = imageFiles.map(
|
||||
(file) => uploadReceiptImage(groupId, file),
|
||||
);
|
||||
return await Future.wait(uploadTasks);
|
||||
}
|
||||
|
||||
@@ -259,7 +268,10 @@ class StorageService {
|
||||
final ref = _storage.refFromURL(imageUrl);
|
||||
return await ref.getMetadata();
|
||||
} catch (e) {
|
||||
_errorService.logError('StorageService', 'Erreur récupération metadata: $e');
|
||||
_errorService.logError(
|
||||
'StorageService',
|
||||
'Erreur récupération metadata: $e',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -274,14 +286,17 @@ class StorageService {
|
||||
// Vérifier l'âge du fichier
|
||||
final metadata = await ref.getMetadata();
|
||||
final uploadDate = metadata.timeCreated;
|
||||
|
||||
|
||||
if (uploadDate != null) {
|
||||
final daysSinceUpload = DateTime.now().difference(uploadDate).inDays;
|
||||
|
||||
|
||||
// Supprimer les fichiers de plus de 30 jours sans dépense associée
|
||||
if (daysSinceUpload > 30) {
|
||||
await ref.delete();
|
||||
_errorService.logInfo('StorageService', 'Image orpheline supprimée: ${ref.name}');
|
||||
_errorService.logInfo(
|
||||
'StorageService',
|
||||
'Image orpheline supprimée: ${ref.name}',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,17 +310,20 @@ class StorageService {
|
||||
try {
|
||||
final groupRef = _storage.ref().child('receipts/$groupId');
|
||||
final listResult = await groupRef.listAll();
|
||||
|
||||
|
||||
int totalSize = 0;
|
||||
for (final ref in listResult.items) {
|
||||
final metadata = await ref.getMetadata();
|
||||
totalSize += metadata.size ?? 0;
|
||||
}
|
||||
|
||||
|
||||
return totalSize;
|
||||
} catch (e) {
|
||||
_errorService.logError('StorageService', 'Erreur calcul taille storage: $e');
|
||||
_errorService.logError(
|
||||
'StorageService',
|
||||
'Erreur calcul taille storage: $e',
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user