import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:image/image.dart' as img; import 'package:supabase_flutter/supabase_flutter.dart'; import '../main.dart'; /// Handles uploading / deleting car images in Supabase Storage. /// /// Bucket: `car-images` (private) /// Path: `{auth.uid()}/{hotwheels.id}.jpg` class StorageService { StorageService._(); static const _bucket = 'car-images'; static const int _maxImageBytes = 500 * 1024; static const int _maxWidth = 1080; static const int _signedUrlExpirySeconds = 3600; /// Upload a car image for a specific hotwheels entry. /// Returns the storage path on success (e.g. `uid/123.jpg`). static Future uploadCarImage({ required File file, required int entryId, String? oldPath, }) async { try { if (oldPath != null && oldPath.isNotEmpty) { await deleteCarImage(oldPath); } final userId = supabase.auth.currentUser!.id; final path = '$userId/$entryId.jpg'; final compressed = await _compressImage(file); await supabase.storage.from(_bucket).uploadBinary( path, compressed, fileOptions: const FileOptions( upsert: true, contentType: 'image/jpeg', ), ); return path; } catch (e) { debugPrint('StorageService.uploadCarImage error: $e'); return null; } } /// Generates a temporary signed URL for a private image path. static Future createSignedUrl(String? path) async { if (path == null || path.isEmpty) return null; try { return await supabase.storage .from(_bucket) .createSignedUrl(path, _signedUrlExpirySeconds); } catch (e) { debugPrint('StorageService.createSignedUrl error: $e'); return null; } } /// Deletes an image using its storage path. static Future deleteCarImage(String? path) async { if (path == null || path.isEmpty) return; try { await supabase.storage.from(_bucket).remove([path]); } catch (e) { debugPrint('StorageService.deleteCarImage error: $e'); } } static Future _compressImage(File source) async { final bytes = await source.readAsBytes(); final decoded = img.decodeImage(bytes); if (decoded == null) { throw Exception('Invalid image file.'); } img.Image working = decoded.width > _maxWidth ? img.copyResize(decoded, width: _maxWidth) : decoded; var quality = 85; Uint8List out = Uint8List.fromList(img.encodeJpg(working, quality: quality)); // First pass: reduce JPEG quality. while (out.lengthInBytes > _maxImageBytes && quality > 35) { quality -= 5; out = Uint8List.fromList(img.encodeJpg(working, quality: quality)); } // Second pass: reduce dimensions progressively if still above the limit. while (out.lengthInBytes > _maxImageBytes && working.width > 320) { final nextWidth = (working.width * 0.85).round(); working = img.copyResize(working, width: nextWidth); out = Uint8List.fromList(img.encodeJpg(working, quality: quality)); } if (out.lengthInBytes > _maxImageBytes) { throw Exception( 'Image is too large after compression (${out.lengthInBytes} bytes).', ); } return out; } }