perf(images): lazy-load signed storage URLs for garage cards
This commit is contained in:
parent
c2d8e754a2
commit
7c25ab7c6f
2 changed files with 58 additions and 20 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,7 +83,27 @@ class CarCard extends StatelessWidget {
|
||||||
return _PlaceholderIcon(isDark: isDark);
|
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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue