diff --git a/lib/scanner_screen.dart b/lib/scanner_screen.dart index 9cf0e2d..a708a28 100644 --- a/lib/scanner_screen.dart +++ b/lib/scanner_screen.dart @@ -10,7 +10,9 @@ import 'theme/app_colors.dart'; /// /// The detected ID is returned via Navigator.pop(context, hwId). class ScannerScreen extends StatefulWidget { - const ScannerScreen({super.key}); + final Future Function(String hwId)? onDetected; + + const ScannerScreen({super.key, this.onDetected}); @override State createState() => _ScannerScreenState(); @@ -94,6 +96,9 @@ class _ScannerScreenState extends State { if (found != null) { setState(() => _lastDetected = found); + if (widget.onDetected != null) { + await _submitDetected(found); + } } else { // Show all detected text so user knows what was seen. final allText = recognized.blocks.map((b) => b.text).join('\n'); @@ -118,6 +123,29 @@ class _ScannerScreenState extends State { } } + Future _submitDetected(String hwId) async { + if (widget.onDetected == null) { + if (mounted) Navigator.of(context).pop(hwId); + return; + } + + try { + final keepScanning = await widget.onDetected!(hwId); + if (!mounted) return; + + if (keepScanning) { + setState(() => _lastDetected = null); + } else { + Navigator.of(context).pop(); + } + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Process error: $e'), backgroundColor: Colors.red), + ); + } + } + @override void dispose() { _cameraController?.dispose(); @@ -229,7 +257,9 @@ class _ScannerScreenState extends State { ), ), ElevatedButton( - onPressed: () => Navigator.of(context).pop(_lastDetected), + onPressed: _isBusy || _lastDetected == null + ? null + : () => _submitDetected(_lastDetected!), child: const Text('Use This'), ), ], @@ -298,7 +328,7 @@ class _ScannerScreenState extends State { ); if (result != null && context.mounted) { - Navigator.of(context).pop(result); + await _submitDetected(result); } } } diff --git a/lib/screens/garage_screen.dart b/lib/screens/garage_screen.dart index f6b860a..642ca99 100644 --- a/lib/screens/garage_screen.dart +++ b/lib/screens/garage_screen.dart @@ -40,6 +40,7 @@ class GarageScreenState extends State { bool _selectionMode = false; final Set _selectedIds = {}; + final Set _refreshingImageIds = {}; @override void initState() { @@ -266,6 +267,7 @@ class GarageScreenState extends State { (context, index) { final car = _filteredCars[index]; final global = car['global_cars'] as Map?; + final carId = car['id'] as int; return CarCard( hwId: car['hw_id'] as String? ?? '???', name: global?['name'] as String?, @@ -273,10 +275,11 @@ class GarageScreenState extends State { year: global?['year'] as int?, color: global?['color'] as String?, imageUrl: car['signed_image_url'] as String?, - isSelected: _selectedIds.contains(car['id'] as int), + isSelected: _selectedIds.contains(carId), addedAt: car['created_at'] != null ? DateTime.tryParse(car['created_at']) : null, + onImageError: () => _refreshSignedUrlForCar(carId), onTap: () => _selectionMode ? _toggleCarSelection(car) : _showCarDetails(car), @@ -434,6 +437,30 @@ class GarageScreenState extends State { } } + Future _refreshSignedUrlForCar(int carId) async { + if (_refreshingImageIds.contains(carId)) return; + + final index = _cars.indexWhere((c) => c['id'] == carId); + if (index == -1) return; + + final path = _cars[index]['user_image_url'] as String?; + if (path == null || path.isEmpty) return; + + _refreshingImageIds.add(carId); + try { + final signed = await StorageService.createSignedUrl(path); + if (!mounted || signed == null) return; + setState(() { + _cars[index] = { + ..._cars[index], + 'signed_image_url': signed, + }; + }); + } finally { + _refreshingImageIds.remove(carId); + } + } + void _showCarDetails(Map car) { if (_selectionMode) { _toggleCarSelection(car); @@ -485,7 +512,20 @@ class GarageScreenState extends State { ? Image.network( imageUrl, fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => _imagePlaceholder(), + errorBuilder: (context, error, stackTrace) => Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded(child: _imagePlaceholder()), + Padding( + padding: const EdgeInsets.only(bottom: 10), + child: TextButton.icon( + onPressed: () => _refreshSignedUrlForCar(car['id'] as int), + icon: const Icon(Icons.refresh, size: 16), + label: const Text('Refresh image link'), + ), + ), + ], + ), ) : _imagePlaceholder(), ), diff --git a/lib/screens/scan_tab.dart b/lib/screens/scan_tab.dart index 73ac8a0..d2b69d5 100644 --- a/lib/screens/scan_tab.dart +++ b/lib/screens/scan_tab.dart @@ -209,12 +209,13 @@ class ScanTabState extends State { } Future _openScanner() async { - final hwId = await navigatorKey.currentState!.push( - MaterialPageRoute(builder: (_) => const ScannerScreen()), + await navigatorKey.currentState!.push( + MaterialPageRoute( + builder: (_) => ScannerScreen( + onDetected: (hwId) => _processHwId(hwId), + ), + ), ); - - if (hwId == null || !mounted) return; - await _processHwId(hwId); } Future _manualEntry() async { @@ -253,9 +254,9 @@ class ScanTabState extends State { await _processHwId(result); } - Future _processHwId(String hwId) async { + Future _processHwId(String hwId) async { final collection = _selectedCollection; - if (collection == null) return; + if (collection == null) return false; setState(() => _isBusy = true); @@ -268,10 +269,10 @@ class ScanTabState extends State { .maybeSingle(); if (existing != null) { - if (!mounted) return; + if (!mounted) return false; setState(() => _isBusy = false); await showDialog( - context: context, + context: navigatorKey.currentContext ?? context, builder: (_) => AlertDialog( icon: const Icon(Icons.check_circle, color: AppColors.success, size: 48), @@ -285,7 +286,7 @@ class ScanTabState extends State { ], ), ); - return; + return true; } final globalCar = await supabase @@ -294,12 +295,12 @@ class ScanTabState extends State { .eq('hw_id', hwId) .maybeSingle(); - if (!mounted) return; + if (!mounted) return false; setState(() => _isBusy = false); if (globalCar != null) { final addConfirmed = await showModalBottomSheet( - context: context, + context: navigatorKey.currentContext ?? context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24)), @@ -312,12 +313,12 @@ class ScanTabState extends State { if (addConfirmed == true) { await _addToCollection(collection.id, hwId); - if (!mounted) return; + if (!mounted) return false; showGlobalSnackBar('$hwId added to "${collection.name}"! 🎉'); } } else { final discovery = await showModalBottomSheet<_NewDiscoveryData>( - context: context, + context: navigatorKey.currentContext ?? context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24)), @@ -336,13 +337,15 @@ class ScanTabState extends State { year: discovery.year, ); await _addToCollection(collection.id, hwId, notes: discovery.notes); - if (!mounted) return; + if (!mounted) return false; showGlobalSnackBar('$hwId added to "${collection.name}"! 🎉'); } } + return true; } catch (e) { if (mounted) setState(() => _isBusy = false); showGlobalSnackBar('DB error: $e', isError: true); + return true; } } diff --git a/lib/widgets/car_card.dart b/lib/widgets/car_card.dart index 7375e64..28a547f 100644 --- a/lib/widgets/car_card.dart +++ b/lib/widgets/car_card.dart @@ -12,6 +12,7 @@ class CarCard extends StatelessWidget { final DateTime? addedAt; final VoidCallback? onTap; final VoidCallback? onLongPress; + final VoidCallback? onImageError; final bool isSelected; const CarCard({ @@ -25,6 +26,7 @@ class CarCard extends StatelessWidget { this.addedAt, this.onTap, this.onLongPress, + this.onImageError, this.isSelected = false, }); @@ -65,8 +67,10 @@ class CarCard extends StatelessWidget { ? Image.network( imageUrl!, fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => - _PlaceholderIcon(isDark: isDark), + errorBuilder: (context, error, stackTrace) { + onImageError?.call(); + return _PlaceholderIcon(isDark: isDark); + }, ) : _PlaceholderIcon(isDark: isDark), ),