import 'dart:io'; import 'dart:typed_data'; import 'package:firebase_storage/firebase_storage.dart'; import 'package:path/path.dart' as path; import 'package:image/image.dart' as img; import 'error_service.dart'; class StorageService { final FirebaseStorage _storage; final ErrorService _errorService; StorageService({ FirebaseStorage? storage, ErrorService? errorService, }) : _storage = storage ?? FirebaseStorage.instance, _errorService = errorService ?? ErrorService(); /// Upload d'une image de reçu pour une dépense Future uploadReceiptImage(String groupId, File imageFile) async { try { // Validation du fichier _validateImageFile(imageFile); // Compression de l'image final compressedImage = await _compressImage(imageFile); // Génération du nom de fichier unique final fileName = _generateReceiptFileName(groupId); // Référence vers le storage final storageRef = _storage.ref().child('receipts/$groupId/$fileName'); // Métadonnées pour optimiser le cache et la compression final metadata = SettableMetadata( contentType: 'image/jpeg', customMetadata: { 'groupId': groupId, 'uploadedAt': DateTime.now().toIso8601String(), 'compressed': 'true', }, ); // Upload du fichier final uploadTask = storageRef.putData(compressedImage, metadata); // Monitoring du progrès (optionnel) uploadTask.snapshotEvents.listen((TaskSnapshot snapshot) { final progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100; _errorService.logInfo('StorageService', 'Upload progress: ${progress.toStringAsFixed(1)}%'); }); // Attendre la completion final snapshot = await uploadTask; // Récupérer l'URL de téléchargement final downloadUrl = await snapshot.ref.getDownloadURL(); _errorService.logSuccess('StorageService', 'Image uploadée avec succès: $fileName'); return downloadUrl; } catch (e) { _errorService.logError('StorageService', 'Erreur upload image: $e'); rethrow; } } /// Supprimer une image de reçu Future deleteReceiptImage(String imageUrl) async { try { if (imageUrl.isEmpty) return; // Extraire la référence depuis l'URL final ref = _storage.refFromURL(imageUrl); await ref.delete(); _errorService.logSuccess('StorageService', 'Image supprimée avec succès'); } catch (e) { _errorService.logError('StorageService', 'Erreur suppression image: $e'); // Ne pas rethrow pour éviter de bloquer la suppression de la dépense } } /// Compresser une image pour optimiser le stockage Future _compressImage(File imageFile) async { try { // Lire l'image final bytes = await imageFile.readAsBytes(); img.Image? image = img.decodeImage(bytes); if (image == null) { throw Exception('Impossible de décoder l\'image'); } // Redimensionner si l'image est trop grande const maxWidth = 1024; const maxHeight = 1024; if (image.width > maxWidth || image.height > maxHeight) { image = img.copyResize( image, width: image.width > image.height ? maxWidth : null, height: image.height > image.width ? maxHeight : null, interpolation: img.Interpolation.linear, ); } // Encoder en JPEG avec compression final compressedBytes = img.encodeJpg(image, quality: 85); _errorService.logInfo('StorageService', 'Image compressée: ${bytes.length} → ${compressedBytes.length} bytes'); return Uint8List.fromList(compressedBytes); } catch (e) { _errorService.logError('StorageService', 'Erreur compression image: $e'); // Fallback: retourner l'image originale si la compression échoue return await imageFile.readAsBytes(); } } /// Valider le fichier image void _validateImageFile(File imageFile) { // Vérifier que le fichier existe if (!imageFile.existsSync()) { throw Exception('Le fichier image n\'existe pas'); } // Vérifier la taille du fichier (max 10MB) const maxSizeBytes = 10 * 1024 * 1024; // 10MB final fileSize = imageFile.lengthSync(); if (fileSize > maxSizeBytes) { throw Exception('La taille du fichier dépasse 10MB'); } // Vérifier l'extension final extension = path.extension(imageFile.path).toLowerCase(); const allowedExtensions = ['.jpg', '.jpeg', '.png', '.webp']; if (!allowedExtensions.contains(extension)) { throw Exception('Format d\'image non supporté. Utilisez JPG, PNG ou WebP'); } } /// Générer un nom de fichier unique pour un reçu String _generateReceiptFileName(String groupId) { final timestamp = DateTime.now().millisecondsSinceEpoch; final random = DateTime.now().microsecond; return 'receipt_${groupId}_${timestamp}_$random.jpg'; } /// Upload multiple d'images (pour futures fonctionnalités) Future> uploadMultipleImages( String groupId, List imageFiles, ) async { final uploadTasks = imageFiles.map((file) => uploadReceiptImage(groupId, file)); return await Future.wait(uploadTasks); } /// Récupérer les métadonnées d'une image Future getImageMetadata(String imageUrl) async { try { final ref = _storage.refFromURL(imageUrl); return await ref.getMetadata(); } catch (e) { _errorService.logError('StorageService', 'Erreur récupération metadata: $e'); return null; } } /// Nettoyer les images orphelines d'un groupe Future cleanupGroupImages(String groupId) async { try { final groupRef = _storage.ref().child('receipts/$groupId'); final listResult = await groupRef.listAll(); for (final ref in listResult.items) { // 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}'); } } } } catch (e) { _errorService.logError('StorageService', 'Erreur nettoyage images: $e'); } } /// Calculer la taille totale utilisée par un groupe Future getGroupStorageSize(String groupId) async { 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'); return 0; } } }