import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import '../main.dart'; import '../services/collection_service.dart'; import '../services/storage_service.dart'; import '../theme/app_colors.dart'; import '../widgets/car_card.dart'; /// The "My Garage" screen — shows a collection's cars in a grid. class GarageScreen extends StatefulWidget { final String collectionId; final String collectionName; final String userRole; const GarageScreen({ super.key, required this.collectionId, required this.collectionName, this.userRole = 'owner', }); bool get isOwner => userRole == 'owner'; bool get isMember => userRole == 'member'; bool get isViewer => userRole == 'viewer'; bool get canModifyCars => isOwner || isMember; @override State createState() => GarageScreenState(); } class GarageScreenState extends State { List> _cars = []; int _totalCarsCount = 0; int _recentCarsCount = 0; bool _isLoading = true; bool _isLoadingMore = false; bool _hasMore = true; int _page = 0; static const int _pageSize = 40; String? _error; String _searchQuery = ''; final _searchController = TextEditingController(); final _scrollController = ScrollController(); bool _selectionMode = false; final Set _selectedIds = {}; final Set _refreshingImageIds = {}; @override void initState() { super.initState(); _scrollController.addListener(_onScroll); _loadCars(reset: true); } @override void dispose() { _scrollController.dispose(); _searchController.dispose(); super.dispose(); } /// Public method so other screens can trigger a refresh. void refresh() => _loadCars(reset: true); void _onScroll() { if (!_scrollController.hasClients || _isLoadingMore || !_hasMore) return; final pos = _scrollController.position; if (pos.pixels >= pos.maxScrollExtent - 320) { _loadCars(); } } Future _loadCars({bool reset = false}) async { if (reset) { _page = 0; _hasMore = true; _selectedIds.clear(); _selectionMode = false; _loadCollectionStats(); } if (!_hasMore && !reset) return; setState(() { if (reset || _cars.isEmpty) { _isLoading = true; } else { _isLoadingMore = true; } if (reset) _error = null; }); try { final from = _page * _pageSize; final to = from + _pageSize - 1; final data = await supabase .from('hotwheels') .select( 'id, created_at, hw_id, notes, user_image_url, global_cars(name, series, year, color, is_verified, confirmation_count)') .eq('collection_id', widget.collectionId) .order('created_at', ascending: false) .range(from, to); final rows = List>.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; if (_hasMore) _page += 1; _isLoading = false; _isLoadingMore = false; }); } catch (e) { if (!mounted) return; setState(() { _error = e.toString(); _isLoading = false; _isLoadingMore = false; }); } } Future _loadCollectionStats() async { try { final weekAgoIso = DateTime.now() .subtract(const Duration(days: 7)) .toUtc() .toIso8601String(); final totalResult = await supabase .from('hotwheels') .select('id') .eq('collection_id', widget.collectionId); final recentResult = await supabase .from('hotwheels') .select('id') .eq('collection_id', widget.collectionId) .gte('created_at', weekAgoIso); if (!mounted) return; setState(() { _totalCarsCount = totalResult.length; _recentCarsCount = recentResult.length; }); } catch (_) { if (!mounted) return; setState(() { _totalCarsCount = _cars.length; _recentCarsCount = _cars.where((c) { final ts = c['created_at']; final d = ts == null ? null : DateTime.tryParse(ts.toString()); final weekAgo = DateTime.now().subtract(const Duration(days: 7)); return d != null && d.isAfter(weekAgo); }).length; }); } } List> get _filteredCars { if (_searchQuery.isEmpty) return _cars; final q = _searchQuery.toLowerCase(); return _cars.where((car) { final global = car['global_cars'] as Map?; final id = (car['hw_id'] as String? ?? '').toLowerCase(); final name = (global?['name'] as String? ?? '').toLowerCase(); final series = (global?['series'] as String? ?? '').toLowerCase(); return id.contains(q) || name.contains(q) || series.contains(q); }).toList(); } @override Widget build(BuildContext context) { return Scaffold( body: RefreshIndicator( onRefresh: () => _loadCars(reset: true), child: CustomScrollView( controller: _scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverAppBar( expandedHeight: 140, pinned: true, actions: [ if (_selectionMode && !widget.isViewer) IconButton( tooltip: 'Cancel Selection', onPressed: () => _toggleSelectionMode(false), icon: const Icon(Icons.close), ) else if (!widget.isViewer) IconButton( tooltip: 'Select Cars', onPressed: () => _toggleSelectionMode(true), icon: const Icon(Icons.select_all), ), ], flexibleSpace: FlexibleSpaceBar( titlePadding: const EdgeInsets.only(left: 20, bottom: 16), title: Text( widget.collectionName, style: TextStyle( fontFamily: 'Poppins', fontWeight: FontWeight.w700, fontSize: 22, color: Colors.white, shadows: [ Shadow( color: Colors.black.withValues(alpha: 0.3), blurRadius: 4, ), ], ), ), background: Container( decoration: BoxDecoration( image: const DecorationImage( image: AssetImage('assets/img/login_bg.jpg'), fit: BoxFit.cover, ), color: Colors.black.withValues(alpha: 0.15), ), child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black.withValues(alpha: 0.3), Colors.black.withValues(alpha: 0.55), ], ), ), child: Align( alignment: Alignment.centerRight, child: Padding( padding: const EdgeInsets.only(right: 24), child: Icon( Icons.directions_car_filled, size: 72, color: Colors.white.withValues(alpha: 0.18), ), ), ), ), ), ), ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), child: Row( children: [ _StatChip( icon: Icons.directions_car, label: '$_totalCarsCount', subtitle: 'Total Cars', ), const SizedBox(width: 12), _StatChip( icon: Icons.new_releases, label: '$_recentCarsCount', subtitle: 'This Week', ), ], ), ), ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), child: TextField( controller: _searchController, onChanged: (v) => setState(() => _searchQuery = v), decoration: InputDecoration( hintText: 'Search by ID, name, or series…', prefixIcon: const Icon(Icons.search), suffixIcon: _searchQuery.isEmpty ? null : IconButton( icon: const Icon(Icons.clear), onPressed: () { _searchController.clear(); setState(() => _searchQuery = ''); }, ), ), ), ), ), if (_isLoading) const SliverFillRemaining( child: Center(child: CircularProgressIndicator()), ) else if (_error != null) SliverFillRemaining( child: _ErrorView( message: _error!, onRetry: () => _loadCars(reset: true), ), ) else if (_filteredCars.isEmpty) SliverFillRemaining( child: _EmptyGarage(hasSearch: _searchQuery.isNotEmpty), ) else SliverPadding( padding: const EdgeInsets.fromLTRB(8, 4, 8, 100), sliver: SliverGrid( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, childAspectRatio: 0.72, mainAxisSpacing: 4, crossAxisSpacing: 0, ), delegate: SliverChildBuilderDelegate( (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?, series: global?['series'] as String?, year: global?['year'] as int?, color: global?['color'] as String?, isVerified: global?['is_verified'] == true, imageUrl: car['signed_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), onLongPress: widget.isViewer ? null : () => _toggleCarSelection(car), ); }, childCount: _filteredCars.length, ), ), ), if (_isLoadingMore) const SliverToBoxAdapter( child: Padding( padding: EdgeInsets.symmetric(vertical: 18), child: Center(child: CircularProgressIndicator(strokeWidth: 2)), ), ), ], ), ), bottomNavigationBar: _selectionMode && !widget.isViewer ? SafeArea( top: false, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( color: Theme.of(context).cardTheme.color, border: Border( top: BorderSide( color: AppColors.textHint.withValues(alpha: 0.25), ), ), ), child: Row( children: [ Expanded( child: Text( '${_selectedIds.length} selected', style: const TextStyle(fontWeight: FontWeight.w600), ), ), OutlinedButton( onPressed: _selectedIds.isEmpty ? null : _relocateSelectedCars, child: Text(widget.isOwner ? 'Move to...' : 'Copy to...'), ), ], ), ), ) : null, ); } void _toggleSelectionMode(bool enabled) { if (widget.isViewer && enabled) return; setState(() { _selectionMode = enabled; if (!enabled) { _selectedIds.clear(); } }); } void _toggleCarSelection(Map car) { if (widget.isViewer) return; final id = car['id'] as int; setState(() { _selectionMode = true; if (_selectedIds.contains(id)) { _selectedIds.remove(id); } else { _selectedIds.add(id); } if (_selectedIds.isEmpty) { _selectionMode = false; } }); } Future _relocateSelectedCars() async { if (_selectedIds.isEmpty) return; if (widget.isViewer) { showGlobalSnackBar('Viewer role is read-only for this collection.'); return; } try { final isOwner = widget.isOwner; final collections = await CollectionService.getMyCollections(); if (!mounted) return; final candidates = collections .where((c) => c.id != widget.collectionId) .toList(growable: false); if (candidates.isEmpty) { showGlobalSnackBar('No other collection available.', isError: true); return; } String? targetId; final confirmed = await showDialog( context: context, builder: (_) => StatefulBuilder( builder: (context, setSheetState) => AlertDialog( title: Text(isOwner ? 'Move Selected Cars' : 'Copy Selected Cars'), content: DropdownButtonFormField( initialValue: targetId, decoration: const InputDecoration( labelText: 'Target collection', ), items: candidates .map( (c) => DropdownMenuItem( value: c.id, child: Text(c.name), ), ) .toList(), onChanged: (value) => setSheetState(() => targetId = value), ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('Cancel'), ), ElevatedButton( onPressed: targetId == null ? null : () => Navigator.pop(context, true), child: Text(isOwner ? 'Move' : 'Copy'), ), ], ), ), ); if (confirmed != true || targetId == null) return; if (isOwner) { await supabase .from('hotwheels') .update({'collection_id': targetId}) .inFilter('id', _selectedIds.toList()); } else { final sourceCars = _cars .where((car) => _selectedIds.contains(car['id'] as int)) .toList(growable: false); final insertRows = sourceCars.map((car) { final notes = car['notes'] as String?; final imagePath = car['user_image_url'] as String?; return { 'hw_id': car['hw_id'] as String, 'user_id': supabase.auth.currentUser!.id, 'collection_id': targetId, if (notes != null && notes.trim().isNotEmpty) 'notes': notes, if (imagePath != null && imagePath.isNotEmpty) 'user_image_url': imagePath, }; }).toList(growable: false); if (insertRows.isNotEmpty) { await supabase.from('hotwheels').insert(insertRows); } } if (!mounted) return; showGlobalSnackBar( isOwner ? '${_selectedIds.length} car(s) moved.' : '${_selectedIds.length} car(s) copied.', ); _toggleSelectionMode(false); await _loadCars(reset: true); } catch (e) { showGlobalSnackBar( widget.isOwner ? 'Failed to move cars: $e' : 'Failed to copy cars: $e', isError: true, ); } } 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); return; } final global = car['global_cars'] as Map?; final hwId = car['hw_id'] as String? ?? '???'; final name = global?['name'] as String?; final series = global?['series'] as String?; final year = global?['year'] as int?; 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?; showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), builder: (_) => DraggableScrollableSheet( initialChildSize: 0.55, minChildSize: 0.3, maxChildSize: 0.85, expand: false, builder: (context, scrollController) => ListView( controller: scrollController, padding: const EdgeInsets.all(24), children: [ // Drag handle Center( child: Container( width: 40, height: 4, margin: const EdgeInsets.only(bottom: 20), decoration: BoxDecoration( color: Colors.grey.shade300, borderRadius: BorderRadius.circular(2), ), ), ), // ── Car image ── ClipRRect( borderRadius: BorderRadius.circular(16), child: AspectRatio( aspectRatio: 16 / 10, child: imageUrl != null && imageUrl.isNotEmpty ? CachedNetworkImage( imageUrl: imageUrl, fit: BoxFit.cover, fadeInDuration: Duration.zero, fadeOutDuration: Duration.zero, errorWidget: (context, url, error) => 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(), ), ), const SizedBox(height: 8), // Change / Add photo button if (widget.canModifyCars) ...[ Align( alignment: Alignment.centerRight, child: TextButton.icon( onPressed: () => _updatePhoto(car, context), icon: Icon( (car['user_image_url'] as String?) != null ? Icons.camera_alt : Icons.add_a_photo, size: 18, ), label: Text( (car['user_image_url'] as String?) != null ? 'Change Photo' : 'Add Photo', ), ), ), const SizedBox(height: 4), ], // ID badge Align( alignment: Alignment.centerLeft, child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6), decoration: BoxDecoration( gradient: AppColors.brandGradient, borderRadius: BorderRadius.circular(8), ), child: Text( hwId, style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w700, fontSize: 18, letterSpacing: 1.5, ), ), ), ), if (name != null && name.isNotEmpty) ...[ const SizedBox(height: 16), Text( name, style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.w600, ), ), ], const SizedBox(height: 12), const Divider(), const SizedBox(height: 8), if (series != null && series.isNotEmpty) _DetailRow(icon: Icons.collections, label: 'Series', value: series), if (year != null) _DetailRow(icon: Icons.calendar_today, label: 'Year', value: '$year'), _DetailRow( icon: verified ? Icons.verified : Icons.hourglass_bottom, label: 'Validation', value: verified ? 'Verified by community' : 'Pending verification', ), _DetailRow( icon: Icons.how_to_vote, label: 'Confirmations', value: '$confirmations', ), if (notes != null && notes.isNotEmpty) _DetailRow(icon: Icons.notes, label: 'Notes', value: notes), const SizedBox(height: 24), if (!verified) ...[ SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: () => _confirmCatalogEntry(hwId, context), icon: const Icon(Icons.thumb_up_alt_outlined, size: 18), label: const Text('Confirm Catalog Entry'), ), ), const SizedBox(height: 10), ], SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: () => _reportCatalogEntry(car, context), icon: const Icon(Icons.flag_outlined, size: 18), label: const Text('Report Catalog Issue'), ), ), const SizedBox(height: 12), if (widget.canModifyCars) ...[ Row( children: [ Expanded( child: OutlinedButton.icon( onPressed: () => _relocateSingleCar(car, context), icon: Icon( widget.isOwner ? Icons.drive_file_move_outline : Icons.copy_outlined, size: 18, ), label: Text(widget.isOwner ? 'Move' : 'Copy'), ), ), const SizedBox(width: 8), Expanded( child: ElevatedButton.icon( onPressed: () => _editCar(car, context), icon: const Icon(Icons.edit, size: 18), label: const Text('Edit'), ), ), ], ), const SizedBox(height: 8), SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: () => _deleteCar(car, context), icon: const Icon(Icons.delete_outline, color: AppColors.error), label: const Text( 'Remove', style: TextStyle(color: AppColors.error), ), style: OutlinedButton.styleFrom( side: const BorderSide(color: AppColors.error), ), ), ), ] else const Text( 'Viewer access: read-only', textAlign: TextAlign.center, style: TextStyle( color: AppColors.textSecondary, fontWeight: FontWeight.w500, ), ), ], ), ), ); } Widget _imagePlaceholder() { final isDark = Theme.of(context).brightness == Brightness.dark; return Container( color: isDark ? AppColors.surfaceDark : AppColors.backgroundLight, child: Padding( padding: const EdgeInsets.all(18), child: Image.asset( 'assets/img/icon_bg_removed.png', fit: BoxFit.contain, errorBuilder: (context, error, stackTrace) => Center( child: Icon( Icons.directions_car_filled, size: 48, color: isDark ? Colors.white.withValues(alpha: 0.15) : AppColors.orange.withValues(alpha: 0.25), ), ), ), ), ); } /// Take a new photo and update the image_url for this car. Future _updatePhoto( Map car, BuildContext sheetContext) async { if (!widget.canModifyCars) { showGlobalSnackBar('Viewer role is read-only for this collection.'); return; } final picker = ImagePicker(); final xFile = await picker.pickImage( source: ImageSource.camera, maxWidth: 1600, maxHeight: 1600, imageQuality: 90, ); if (xFile == null) return; showGlobalSnackBar('Uploading photo…'); final oldPath = car['user_image_url'] as String?; String newPath; try { newPath = await StorageService.uploadCarImage( file: File(xFile.path), entryId: car['id'] as int, oldPath: oldPath, ); } catch (e) { showGlobalSnackBar( 'Failed to upload photo. Please try a smaller/clearer image. ($e)', isError: true, ); return; } try { await supabase .from('hotwheels') .update({'user_image_url': newPath}) .eq('id', car['id']); showGlobalSnackBar('Photo updated!'); if (sheetContext.mounted) Navigator.pop(sheetContext); _loadCars(reset: true); // refresh grid } catch (e) { showGlobalSnackBar('Failed to save: $e', isError: true); } } /// Open an edit dialog for this car, then update Supabase. Future _editCar( Map car, BuildContext sheetContext) async { if (!widget.canModifyCars) { showGlobalSnackBar('Viewer role is read-only for this collection.'); return; } final updated = await showDialog>( context: sheetContext, builder: (_) => _EditCarDialog(car: car), ); if (updated == null) return; try { await supabase .from('hotwheels') .update(updated) .eq('id', car['id']); showGlobalSnackBar('Car updated!'); if (sheetContext.mounted) Navigator.pop(sheetContext); _loadCars(reset: true); } catch (e) { showGlobalSnackBar('Failed to update: $e', isError: true); } } Future _confirmCatalogEntry(String hwId, BuildContext sheetContext) async { final user = supabase.auth.currentUser; if (user == null) return; try { final existingVote = await supabase .from('car_votes') .select('id') .eq('hw_id', hwId) .eq('user_id', user.id) .maybeSingle(); if (existingVote != null) { showGlobalSnackBar('You already confirmed this catalog entry.'); return; } await supabase.from('car_votes').insert({ 'hw_id': hwId, 'user_id': user.id, }); showGlobalSnackBar('Thanks! Your validation vote was recorded.'); if (sheetContext.mounted) Navigator.pop(sheetContext); _loadCars(reset: true); } catch (e) { showGlobalSnackBar('Failed to submit validation vote: $e', isError: true); } } Future _reportCatalogEntry( Map car, BuildContext sheetContext, ) async { final user = supabase.auth.currentUser; if (user == null) return; final payload = await showDialog<_CarReportDraft>( context: sheetContext, builder: (_) => const _ReportCarDialog(), ); if (payload == null) return; final hwId = car['hw_id'] as String?; if (hwId == null || hwId.isEmpty) { showGlobalSnackBar('Cannot report this item: missing hw_id.', isError: true); return; } final hotwheelsId = car['id'] as int?; try { final existingOpen = await supabase .from('car_reports') .select('id') .eq('hw_id', hwId) .eq('reporter_user_id', user.id) .eq('status', 'open') .maybeSingle(); if (existingOpen != null) { showGlobalSnackBar('You already have an open report for this car.'); return; } await supabase.from('car_reports').insert({ 'hw_id': hwId, 'hotwheels_id': hotwheelsId, 'reporter_user_id': user.id, 'reason': payload.reason, 'note': payload.note, }); showGlobalSnackBar('Thanks for reporting. We will review this entry.'); } catch (e) { showGlobalSnackBar('Failed to submit report: $e', isError: true); } } Future _pickTargetCollection() async { final collections = await CollectionService.getMyCollections(); if (!mounted) return null; final candidates = collections .where((c) => c.id != widget.collectionId) .toList(growable: false); if (candidates.isEmpty) { showGlobalSnackBar('No other collection available.', isError: true); return null; } String? targetId; final confirmed = await showDialog( context: context, builder: (_) => StatefulBuilder( builder: (context, setSheetState) => AlertDialog( title: Text(widget.isOwner ? 'Move Car' : 'Copy Car'), content: DropdownButtonFormField( initialValue: targetId, decoration: const InputDecoration( labelText: 'Target collection', ), items: candidates .map( (c) => DropdownMenuItem( value: c.id, child: Text(c.name), ), ) .toList(), onChanged: (value) => setSheetState(() => targetId = value), ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('Cancel'), ), ElevatedButton( onPressed: targetId == null ? null : () => Navigator.pop(context, true), child: Text(widget.isOwner ? 'Move' : 'Copy'), ), ], ), ), ); if (confirmed != true || targetId == null) return null; return targetId; } Future _relocateSingleCar( Map car, BuildContext sheetContext) async { if (widget.isViewer) { showGlobalSnackBar('Viewer role is read-only for this collection.'); return; } try { final targetId = await _pickTargetCollection(); if (targetId == null) return; if (widget.isOwner) { await supabase .from('hotwheels') .update({'collection_id': targetId}) .eq('id', car['id']); } else { final notes = car['notes'] as String?; final imagePath = car['user_image_url'] as String?; await supabase.from('hotwheels').insert({ 'hw_id': car['hw_id'] as String, 'user_id': supabase.auth.currentUser!.id, 'collection_id': targetId, if (notes != null && notes.trim().isNotEmpty) 'notes': notes, if (imagePath != null && imagePath.isNotEmpty) 'user_image_url': imagePath, }); } if (!mounted) return; if (sheetContext.mounted) Navigator.pop(sheetContext); showGlobalSnackBar(widget.isOwner ? '${car['hw_id']} moved to another collection.' : '${car['hw_id']} copied to another collection.'); await _loadCars(reset: true); } catch (e) { showGlobalSnackBar( widget.isOwner ? 'Failed to move car: $e' : 'Failed to copy car: $e', isError: true, ); } } Future _deleteCar( Map car, BuildContext sheetContext) async { if (!widget.canModifyCars) { showGlobalSnackBar('Viewer role is read-only for this collection.'); return; } if (!sheetContext.mounted) return; final confirmed = await showDialog( context: sheetContext, builder: (_) => AlertDialog( title: const Text('Remove Car?'), content: Text( 'Remove ${car['hw_id']} from your garage? This cannot be undone.'), actions: [ TextButton( onPressed: () => Navigator.pop(sheetContext, false), child: const Text('Cancel'), ), ElevatedButton( onPressed: () => Navigator.pop(sheetContext, true), style: ElevatedButton.styleFrom(backgroundColor: AppColors.error), child: const Text('Remove'), ), ], ), ); if (confirmed != true) return; try { // Delete image from storage first (best-effort). await StorageService.deleteCarImage(car['user_image_url'] as String?); await supabase .from('hotwheels') .delete() .eq('id', car['id']); if (!mounted) return; if (sheetContext.mounted) { Navigator.pop(sheetContext); // close bottom sheet } showGlobalSnackBar('${car['hw_id']} removed from your garage.'); _loadCars(reset: true); } catch (e) { showGlobalSnackBar('Failed to remove: $e', isError: true); } } } // ── Helper widgets ────────────────────────────────────────────────── class _StatChip extends StatelessWidget { final IconData icon; final String label; final String subtitle; const _StatChip({ required this.icon, required this.label, required this.subtitle, }); @override Widget build(BuildContext context) { return Expanded( child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(14), boxShadow: const [ BoxShadow( color: Colors.black12, blurRadius: 6, offset: Offset(0, 2), ), ], ), child: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: AppColors.orange.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(10), ), child: Icon(icon, size: 20, color: AppColors.orange), ), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: const TextStyle( fontSize: 20, fontWeight: FontWeight.w700, ), ), Text( subtitle, style: const TextStyle( fontSize: 11, color: AppColors.textSecondary, ), ), ], ), ], ), ), ); } } class _EmptyGarage extends StatelessWidget { final bool hasSearch; const _EmptyGarage({required this.hasSearch}); @override Widget build(BuildContext context) { return Center( child: Padding( padding: const EdgeInsets.all(40), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( hasSearch ? Icons.search_off : Icons.garage_outlined, size: 64, color: AppColors.textHint, ), const SizedBox(height: 16), Text( hasSearch ? 'No cars match your search' : 'Your garage is empty', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: AppColors.textSecondary, ), ), const SizedBox(height: 8), Text( hasSearch ? 'Try a different search term' : 'Scan your first Hot Wheels car to get started!', textAlign: TextAlign.center, style: const TextStyle(color: AppColors.textHint), ), ], ), ), ); } } class _ErrorView extends StatelessWidget { final String message; final VoidCallback onRetry; const _ErrorView({required this.message, required this.onRetry}); @override Widget build(BuildContext context) { return Center( child: Padding( padding: const EdgeInsets.all(40), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.error_outline, size: 56, color: AppColors.error), const SizedBox(height: 16), Text(message, textAlign: TextAlign.center, style: const TextStyle(color: AppColors.textSecondary)), const SizedBox(height: 16), ElevatedButton.icon( onPressed: onRetry, icon: const Icon(Icons.refresh), label: const Text('Retry'), ), ], ), ), ); } } // ── Edit Car Dialog ───────────────────────────────────────────────── class _EditCarDialog extends StatefulWidget { final Map car; const _EditCarDialog({required this.car}); @override State<_EditCarDialog> createState() => _EditCarDialogState(); } class _EditCarDialogState extends State<_EditCarDialog> { late final TextEditingController _notesCtrl; @override void initState() { super.initState(); _notesCtrl = TextEditingController(text: widget.car['notes'] as String? ?? ''); } @override void dispose() { _notesCtrl.dispose(); super.dispose(); } void _save() { final updates = {}; final notes = _notesCtrl.text.trim(); updates['notes'] = notes.isEmpty ? null : notes; Navigator.pop(context, updates); } @override Widget build(BuildContext context) { final hwId = widget.car['hw_id'] as String? ?? '???'; return AlertDialog( icon: Container( padding: const EdgeInsets.all(12), decoration: const BoxDecoration( gradient: AppColors.brandGradient, shape: BoxShape.circle, ), child: const Icon(Icons.edit, color: Colors.white, size: 28), ), title: Text('Edit $hwId'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: _notesCtrl, maxLines: 4, decoration: const InputDecoration( labelText: 'Notes', hintText: 'Any extra info…', ), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Cancel'), ), ElevatedButton( onPressed: _save, child: const Text('Save'), ), ], ); } } class _CarReportDraft { final String reason; final String? note; const _CarReportDraft({ required this.reason, this.note, }); } class _ReportCarDialog extends StatefulWidget { const _ReportCarDialog(); @override State<_ReportCarDialog> createState() => _ReportCarDialogState(); } class _ReportCarDialogState extends State<_ReportCarDialog> { static const List _reasons = [ 'Wrong model name', 'Wrong series or year', 'Duplicate catalog entry', 'Invalid image', 'Other', ]; String _selectedReason = _reasons.first; final TextEditingController _noteCtrl = TextEditingController(); @override void dispose() { _noteCtrl.dispose(); super.dispose(); } void _submit() { final note = _noteCtrl.text.trim(); Navigator.pop( context, _CarReportDraft( reason: _selectedReason, note: note.isEmpty ? null : note, ), ); } @override Widget build(BuildContext context) { return AlertDialog( icon: const Icon(Icons.flag_outlined, color: AppColors.orange, size: 32), title: const Text('Report catalog issue'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ DropdownButtonFormField( isExpanded: true, initialValue: _selectedReason, decoration: const InputDecoration( labelText: 'Reason', ), items: _reasons .map( (reason) => DropdownMenuItem( value: reason, child: Text( reason, overflow: TextOverflow.ellipsis, maxLines: 1, ), ), ) .toList(), onChanged: (value) { if (value == null) return; setState(() => _selectedReason = value); }, ), const SizedBox(height: 12), TextField( controller: _noteCtrl, maxLines: 3, decoration: const InputDecoration( labelText: 'Details (optional)', hintText: 'Add a short note to help moderation…', ), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Cancel'), ), ElevatedButton( onPressed: _submit, child: const Text('Submit Report'), ), ], ); } } class _DetailRow extends StatelessWidget { final IconData icon; final String label; final String value; const _DetailRow({ required this.icon, required this.label, required this.value, }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row( children: [ Icon(icon, size: 18, color: AppColors.textHint), const SizedBox(width: 10), Text( '$label: ', style: const TextStyle( fontWeight: FontWeight.w500, color: AppColors.textSecondary, ), ), Expanded( child: Text(value, style: const TextStyle(fontWeight: FontWeight.w500)), ), ], ), ); } }