- Custom Material 3 theme with Hot Wheels branding (orange/red gradient, navy accents) - Locally bundled Poppins font (Regular, Medium, SemiBold, Bold) - Redesigned login screen with full-bleed background image and frosted glass form - Bottom navigation shell: My Garage / Scan / Profile tabs - My Garage screen with grid view, search, stats, detail bottom sheet - Edit and delete car details from the detail sheet - Skip button for quick-add with minimal info - Camera photo upload to Supabase Storage (UUID-based unguessable paths) - Change/add photo from car detail view - Profile screen with change password, about dialog, sign out - Scan tab with camera scanner and manual entry - Styled scanner screen with crosshair overlay and gradient buttons - Custom app icon with transparent background and adaptive icon support - Native splash screen with brand colors - Auto-refresh garage on tab switch
72 lines
2.2 KiB
Dart
72 lines
2.2 KiB
Dart
import 'dart:io';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
|
import 'package:uuid/uuid.dart';
|
|
import '../main.dart';
|
|
|
|
/// Handles uploading / deleting car images in Supabase Storage.
|
|
///
|
|
/// Bucket: `car-images` (public, but URLs are unguessable)
|
|
/// Path: `cars/{uuid}.jpg` — random UUID per image.
|
|
///
|
|
/// Shared garage — any authenticated user can upload / replace / delete.
|
|
class StorageService {
|
|
StorageService._();
|
|
|
|
static const _bucket = 'car-images';
|
|
static const _uuid = Uuid();
|
|
|
|
/// Upload a photo from [file].
|
|
///
|
|
/// If [oldImageUrl] is provided the previous file is deleted first.
|
|
/// Returns the public URL on success, or `null` on failure.
|
|
static Future<String?> uploadCarImage({
|
|
required File file,
|
|
String? oldImageUrl,
|
|
}) async {
|
|
try {
|
|
// Clean up old image if re-uploading.
|
|
if (oldImageUrl != null) {
|
|
await _deleteByUrl(oldImageUrl);
|
|
}
|
|
|
|
final path = 'cars/${_uuid.v4()}.jpg';
|
|
|
|
await supabase.storage.from(_bucket).upload(
|
|
path,
|
|
file,
|
|
fileOptions: const FileOptions(
|
|
contentType: 'image/jpeg',
|
|
),
|
|
);
|
|
|
|
// Return the public URL.
|
|
final url = supabase.storage.from(_bucket).getPublicUrl(path);
|
|
return url;
|
|
} catch (e) {
|
|
debugPrint('StorageService.uploadCarImage error: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Delete the image at the given public [imageUrl].
|
|
static Future<void> deleteCarImage(String? imageUrl) async {
|
|
if (imageUrl == null || imageUrl.isEmpty) return;
|
|
await _deleteByUrl(imageUrl);
|
|
}
|
|
|
|
/// Extract the storage path from a public URL and remove the file.
|
|
static Future<void> _deleteByUrl(String imageUrl) async {
|
|
try {
|
|
// Public URLs look like:
|
|
// .../storage/v1/object/public/car-images/cars/<uuid>.jpg
|
|
final marker = '/object/public/$_bucket/';
|
|
final idx = imageUrl.indexOf(marker);
|
|
if (idx == -1) return;
|
|
final path = imageUrl.substring(idx + marker.length);
|
|
await supabase.storage.from(_bucket).remove([path]);
|
|
} catch (e) {
|
|
debugPrint('StorageService._deleteByUrl error: $e');
|
|
}
|
|
}
|
|
}
|