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.'); } final resized = decoded.width > _maxWidth ? img.copyResize(decoded, width: _maxWidth) : decoded; var quality = 85; Uint8List out = Uint8List.fromList(img.encodeJpg(resized, quality: quality)); while (out.lengthInBytes > _maxImageBytes && quality > 35) { quality -= 10; out = Uint8List.fromList(img.encodeJpg(resized, quality: quality)); } if (out.lengthInBytes > _maxImageBytes) { final reduced = img.copyResize( resized, width: (resized.width * 0.8).round(), ); out = Uint8List.fromList(img.encodeJpg(reduced, quality: 45)); } return out; } }