perf(images): lazy-load signed storage URLs for garage cards

This commit is contained in:
Lukas Müllner 2026-03-05 14:44:36 +01:00
parent c2d8e754a2
commit 7c25ab7c6f
2 changed files with 58 additions and 20 deletions

View file

@ -109,21 +109,11 @@ class GarageScreenState extends State<GarageScreen> {
.range(from, to);
final rows = List<Map<String, dynamic>>.from(data);
final withSignedUrls = await Future.wait(
rows.map((row) async {
final path = row['user_image_url'] as String?;
final signed = await StorageService.createSignedUrl(path);
return {
...row,
'signed_image_url': signed,
};
}),
);
if (!mounted) return;
setState(() {
_cars = reset ? withSignedUrls : [..._cars, ...withSignedUrls];
_hasMore = withSignedUrls.length == _pageSize;
_cars = reset ? rows : [..._cars, ...rows];
_hasMore = rows.length == _pageSize;
if (_hasMore) _page += 1;
_isLoading = false;
_isLoadingMore = false;
@ -334,14 +324,19 @@ class GarageScreenState extends State<GarageScreen> {
color: global?['color'] as String?,
isVerified: global?['is_verified'] == true,
imageUrl: car['signed_image_url'] as String?,
imagePath: car['user_image_url'] as String?,
isSelected: _selectedIds.contains(carId),
addedAt: car['created_at'] != null
? DateTime.tryParse(car['created_at'])
: null,
onImageError: () => _refreshSignedUrlForCar(carId),
onTap: () => _selectionMode
? _toggleCarSelection(car)
: _showCarDetails(car),
onTap: () {
if (_selectionMode) {
_toggleCarSelection(car);
} else {
_showCarDetails(car);
}
},
onLongPress:
widget.isViewer ? null : () => _toggleCarSelection(car),
);
@ -557,7 +552,7 @@ class GarageScreenState extends State<GarageScreen> {
}
}
void _showCarDetails(Map<String, dynamic> car) {
Future<void> _showCarDetails(Map<String, dynamic> car) async {
if (_selectionMode) {
_toggleCarSelection(car);
return;
@ -571,7 +566,24 @@ class GarageScreenState extends State<GarageScreen> {
final verified = global?['is_verified'] == true;
final confirmations = (global?['confirmation_count'] as num?)?.toInt() ?? 0;
final notes = car['notes'] as String?;
final imageUrl = car['signed_image_url'] as String?;
String? imageUrl = car['signed_image_url'] as String?;
if (imageUrl == null || imageUrl.isEmpty) {
final path = car['user_image_url'] as String?;
final signed = await StorageService.createSignedUrl(path);
if (!mounted) return;
if (signed != null && signed.isNotEmpty) {
imageUrl = signed;
final index = _cars.indexWhere((c) => c['id'] == car['id']);
if (index != -1) {
setState(() {
_cars[index] = {
..._cars[index],
'signed_image_url': signed,
};
});
}
}
}
showModalBottomSheet(
context: context,

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import '../services/storage_service.dart';
import '../theme/app_colors.dart';
/// A styled card for displaying a single Hot Wheels car in the garage.
@ -10,6 +11,7 @@ class CarCard extends StatelessWidget {
final int? year;
final String? color;
final String? imageUrl;
final String? imagePath;
final bool isVerified;
final DateTime? addedAt;
final VoidCallback? onTap;
@ -25,6 +27,7 @@ class CarCard extends StatelessWidget {
this.year,
this.color,
this.imageUrl,
this.imagePath,
this.isVerified = false,
this.addedAt,
this.onTap,
@ -37,6 +40,9 @@ class CarCard extends StatelessWidget {
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final resolvedImageUrl = imageUrl;
final hasResolvedUrl = resolvedImageUrl != null && resolvedImageUrl.isNotEmpty;
final hasImagePath = imagePath != null && imagePath!.isNotEmpty;
return Card(
clipBehavior: Clip.antiAlias,
@ -66,9 +72,9 @@ class CarCard extends StatelessWidget {
)
: AppColors.brandGradientSoft,
),
child: imageUrl != null && imageUrl!.isNotEmpty
child: hasResolvedUrl
? CachedNetworkImage(
imageUrl: imageUrl!,
imageUrl: resolvedImageUrl,
fit: BoxFit.cover,
fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
@ -77,7 +83,27 @@ class CarCard extends StatelessWidget {
return _PlaceholderIcon(isDark: isDark);
},
)
: _PlaceholderIcon(isDark: isDark),
: hasImagePath
? FutureBuilder<String?>(
future: StorageService.createSignedUrl(imagePath),
builder: (context, snapshot) {
final signedUrl = snapshot.data;
if (signedUrl == null || signedUrl.isEmpty) {
return _PlaceholderIcon(isDark: isDark);
}
return CachedNetworkImage(
imageUrl: signedUrl,
fit: BoxFit.cover,
fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
errorWidget: (context, url, error) {
onImageError?.call();
return _PlaceholderIcon(isDark: isDark);
},
);
},
)
: _PlaceholderIcon(isDark: isDark),
),
),