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); .range(from, to);
final rows = List<Map<String, dynamic>>.from(data); 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; if (!mounted) return;
setState(() { setState(() {
_cars = reset ? withSignedUrls : [..._cars, ...withSignedUrls]; _cars = reset ? rows : [..._cars, ...rows];
_hasMore = withSignedUrls.length == _pageSize; _hasMore = rows.length == _pageSize;
if (_hasMore) _page += 1; if (_hasMore) _page += 1;
_isLoading = false; _isLoading = false;
_isLoadingMore = false; _isLoadingMore = false;
@ -334,14 +324,19 @@ class GarageScreenState extends State<GarageScreen> {
color: global?['color'] as String?, color: global?['color'] as String?,
isVerified: global?['is_verified'] == true, isVerified: global?['is_verified'] == true,
imageUrl: car['signed_image_url'] as String?, imageUrl: car['signed_image_url'] as String?,
imagePath: car['user_image_url'] as String?,
isSelected: _selectedIds.contains(carId), isSelected: _selectedIds.contains(carId),
addedAt: car['created_at'] != null addedAt: car['created_at'] != null
? DateTime.tryParse(car['created_at']) ? DateTime.tryParse(car['created_at'])
: null, : null,
onImageError: () => _refreshSignedUrlForCar(carId), onImageError: () => _refreshSignedUrlForCar(carId),
onTap: () => _selectionMode onTap: () {
? _toggleCarSelection(car) if (_selectionMode) {
: _showCarDetails(car), _toggleCarSelection(car);
} else {
_showCarDetails(car);
}
},
onLongPress: onLongPress:
widget.isViewer ? null : () => _toggleCarSelection(car), 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) { if (_selectionMode) {
_toggleCarSelection(car); _toggleCarSelection(car);
return; return;
@ -571,7 +566,24 @@ class GarageScreenState extends State<GarageScreen> {
final verified = global?['is_verified'] == true; final verified = global?['is_verified'] == true;
final confirmations = (global?['confirmation_count'] as num?)?.toInt() ?? 0; final confirmations = (global?['confirmation_count'] as num?)?.toInt() ?? 0;
final notes = car['notes'] as String?; 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( showModalBottomSheet(
context: context, context: context,

View file

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