import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../main.dart'; import '../scanner_screen.dart'; import '../services/collection_service.dart'; import '../services/main_collection_sync.dart'; import '../theme/app_colors.dart'; import '../utils/preferences_utils.dart'; class ScanTab extends StatefulWidget { const ScanTab({super.key}); @override State createState() => ScanTabState(); } class ScanTabState extends State { static const _duplicateCooldown = Duration(seconds: 2); bool _isBusy = false; List _collections = []; Collection? _selectedCollection; bool _loadingCollections = true; String? _lastProcessedHwId; DateTime? _lastProcessedAt; bool get _canAddToSelectedCollection => (_selectedCollection?.canModifyCars ?? false); @override void initState() { super.initState(); MainCollectionSync.changeToken.addListener(_handleMainCollectionChanged); _loadCollections(); } @override void dispose() { MainCollectionSync.changeToken.removeListener(_handleMainCollectionChanged); super.dispose(); } void refresh() => _loadCollections(); void _handleMainCollectionChanged() { if (!mounted) return; _loadCollections(); } Future _loadCollections() async { try { final list = await CollectionService.getMyCollections(); final userId = supabase.auth.currentUser?.id; if (userId == null) { throw Exception('You must be signed in to load collections.'); } final prefs = await SharedPreferences.getInstance(); final persistedId = await readActiveCollectionId(prefs, userId: userId); Collection? selected; if (persistedId != null) { final matching = list.where((c) => c.id == persistedId); if (matching.isNotEmpty) { selected = matching.first; } } selected ??= list.isNotEmpty ? list.first : null; if (!mounted) return; setState(() { _collections = list; _selectedCollection = selected; _loadingCollections = false; }); if (selected != null) { await writeActiveCollectionId( prefs, userId: userId, collectionId: selected.id, ); } } catch (e) { if (!mounted) return; setState(() => _loadingCollections = false); showGlobalError( e, fallback: 'Failed to load collections. Please try again.', ); } } @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 120, height: 120, decoration: BoxDecoration( gradient: AppColors.brandGradientSoft, shape: BoxShape.circle, ), child: const Icon( Icons.qr_code_scanner, size: 56, color: AppColors.orange, ), ), const SizedBox(height: 28), const Text( 'Scan a Hot Wheels Car', style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700), ), const SizedBox(height: 10), const Text( 'Point your camera at the model ID and add cars\ninstantly to your selected collection.', textAlign: TextAlign.center, style: TextStyle( fontSize: 14, color: AppColors.textSecondary, height: 1.5, ), ), const SizedBox(height: 24), if (_loadingCollections) const Padding( padding: EdgeInsets.symmetric(vertical: 8), child: SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ), ) else if (_collections.isEmpty) const Text( 'Create a collection first!', style: TextStyle(color: AppColors.error), ) else Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(14), border: Border.all( color: AppColors.orange.withValues(alpha: 0.4), ), ), child: DropdownButtonHideUnderline( child: DropdownButton( isExpanded: true, value: _selectedCollection?.id, icon: const Icon(Icons.arrow_drop_down, color: AppColors.orange), items: _collections .map((c) => DropdownMenuItem( value: c.id, child: Row( children: [ Icon( c.isOwner ? Icons.collections_bookmark : Icons.group, size: 18, color: AppColors.orange, ), const SizedBox(width: 8), Expanded( child: Text( c.name, overflow: TextOverflow.ellipsis, ), ), ], ), )) .toList(), onChanged: (id) { _setSelectedCollection(id); }, ), ), ), const SizedBox(height: 24), if (_selectedCollection?.isViewer == true) const Padding( padding: EdgeInsets.only(bottom: 12), child: Text( 'Viewer role is read-only. Choose an owner/member collection to add cars.', textAlign: TextAlign.center, style: TextStyle(color: AppColors.textSecondary), ), ), SizedBox( width: double.infinity, height: 56, child: DecoratedBox( decoration: BoxDecoration( gradient: AppColors.brandGradient, borderRadius: BorderRadius.circular(50), boxShadow: [ BoxShadow( color: AppColors.orange.withValues(alpha: 0.35), blurRadius: 14, offset: const Offset(0, 5), ), ], ), child: ElevatedButton.icon( onPressed: _isBusy || !_canAddToSelectedCollection ? null : _openScanner, icon: _isBusy ? const SizedBox( width: 22, height: 22, child: CircularProgressIndicator( strokeWidth: 2.5, color: Colors.white, ), ) : const Icon(Icons.camera_alt, color: Colors.white), label: Text( _isBusy ? 'Processing…' : 'Open Scanner', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white, ), ), style: ElevatedButton.styleFrom( backgroundColor: Colors.transparent, shadowColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(50), ), ), ), ), ), const SizedBox(height: 16), SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: _isBusy || !_canAddToSelectedCollection ? null : _manualEntry, icon: const Icon(Icons.keyboard), label: const Text('Enter ID Manually'), ), ), ], ), ), ), ); } void _setSelectedCollection(String? id) { if (id == null) return; final matching = _collections.where((c) => c.id == id); if (matching.isEmpty) return; setState(() => _selectedCollection = matching.first); } Future _openScanner() async { await navigatorKey.currentState!.push( MaterialPageRoute( builder: (_) => ScannerScreen( collections: _collections, activeCollectionId: _selectedCollection?.id, onCollectionChanged: _setSelectedCollection, onDetected: (hwId) => _processHwId(hwId), ), ), ); } Future _manualEntry() async { final controller = TextEditingController(); final result = await showDialog( context: context, builder: (_) => AlertDialog( title: const Text('Enter HW ID'), content: TextField( controller: controller, autofocus: true, textCapitalization: TextCapitalization.characters, decoration: const InputDecoration(hintText: 'e.g. JKF21'), onSubmitted: (v) { final val = v.trim().toUpperCase(); if (val.isNotEmpty) Navigator.pop(context, val); }, ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Cancel'), ), ElevatedButton( onPressed: () { final val = controller.text.trim().toUpperCase(); if (val.isNotEmpty) Navigator.pop(context, val); }, child: const Text('OK'), ), ], ), ); if (result == null || !mounted) return; await _processHwId(result); } Future _processHwId(String hwId) async { final now = DateTime.now(); if (_lastProcessedHwId == hwId && _lastProcessedAt != null && now.difference(_lastProcessedAt!) < _duplicateCooldown) { return true; } _lastProcessedHwId = hwId; _lastProcessedAt = now; final collection = _selectedCollection; if (collection == null) return false; setState(() => _isBusy = true); try { final existing = await supabase .from('hotwheels') .select('id') .eq('hw_id', hwId) .eq('collection_id', collection.id) .maybeSingle(); if (existing != null) { if (!mounted) return false; setState(() => _isBusy = false); await showDialog( context: navigatorKey.currentContext ?? context, builder: (_) => AlertDialog( icon: const Icon(Icons.check_circle, color: AppColors.success, size: 48), title: const Text('Already in Collection!'), content: Text('$hwId is already in "${collection.name}".'), actions: [ ElevatedButton( onPressed: () => Navigator.pop(context), child: const Text('Got it'), ), ], ), ); return true; } final globalCar = await supabase .from('global_cars') .select('hw_id, name, series, year, color, is_verified, confirmation_count') .eq('hw_id', hwId) .maybeSingle(); if (!mounted) return false; setState(() => _isBusy = false); if (globalCar != null) { final addConfirmed = await showModalBottomSheet( context: navigatorKey.currentContext ?? context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), builder: (_) => _FoundCarSheet( collectionName: collection.name, car: globalCar, ), ); if (addConfirmed == true) { await _addToCollection(collection.id, hwId); await _ensureValidationVote(hwId); if (!mounted) return false; showGlobalSuccess('$hwId added to "${collection.name}"! 🎉'); } } else { final discovery = await showModalBottomSheet<_NewDiscoveryData>( context: navigatorKey.currentContext ?? context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), builder: (_) => _NewDiscoverySheet( hwId: hwId, collectionName: collection.name, ), ); if (discovery != null) { await _createGlobalCarAndVote( hwId: hwId, name: discovery.name, series: discovery.series, year: discovery.year, ); await _addToCollection(collection.id, hwId, notes: discovery.notes); if (!mounted) return false; showGlobalSuccess('$hwId added to "${collection.name}"! 🎉'); } } return true; } catch (e) { if (mounted) setState(() => _isBusy = false); showGlobalError( e, fallback: 'Could not save this car right now. Please try again.', ); return true; } } Future _addToCollection( String collectionId, String hwId, { String? notes, }) async { final userId = supabase.auth.currentUser?.id; if (userId == null) { throw Exception('You must be signed in to add cars.'); } await supabase.from('hotwheels').insert({ 'hw_id': hwId, 'user_id': userId, 'collection_id': collectionId, if (notes != null && notes.trim().isNotEmpty) 'notes': notes.trim(), }); } Future _createGlobalCarAndVote({ required String hwId, required String name, String? series, int? year, }) async { final userId = supabase.auth.currentUser?.id; if (userId == null) { throw Exception('You must be signed in to create catalog entries.'); } final cleanedSeries = series?.trim(); final payload = { 'hw_id': hwId, 'name': name, 'series': (cleanedSeries?.isNotEmpty ?? false) ? cleanedSeries : null, 'year': year, }..removeWhere((key, value) => value == null); await supabase.from('global_cars').insert(payload); await supabase.from('car_votes').insert({ 'hw_id': hwId, 'user_id': userId, }); } Future _ensureValidationVote(String hwId) async { final userId = supabase.auth.currentUser?.id; if (userId == null) { throw Exception('You must be signed in to validate entries.'); } final existingVote = await supabase .from('car_votes') .select('id') .eq('hw_id', hwId) .eq('user_id', userId) .maybeSingle(); if (existingVote != null) return; await supabase.from('car_votes').insert({ 'hw_id': hwId, 'user_id': userId, }); } } class _FoundCarSheet extends StatelessWidget { final String collectionName; final Map car; const _FoundCarSheet({required this.collectionName, required this.car}); @override Widget build(BuildContext context) { final hwId = car['hw_id'] as String? ?? '???'; final name = car['name'] as String? ?? 'Unknown model'; final series = car['series'] as String?; final year = car['year']; final verified = car['is_verified'] == true; final confirmations = (car['confirmation_count'] as num?)?.toInt() ?? 0; return Padding( padding: EdgeInsets.only( left: 20, right: 20, top: 20, bottom: MediaQuery.of(context).viewInsets.bottom + 20, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Found in Catalog', style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w700, ), ), const SizedBox(height: 8), Text('$name ($hwId)', style: const TextStyle(fontWeight: FontWeight.w600)), if (series != null && series.isNotEmpty) ...[ const SizedBox(height: 4), Text('Series: $series'), ], if (year != null) ...[ const SizedBox(height: 4), Text('Year: $year'), ], const SizedBox(height: 6), Text( verified ? 'Verified by community' : 'Unverified catalog entry', style: TextStyle( color: verified ? AppColors.success : AppColors.textSecondary, fontSize: 12, ), ), const SizedBox(height: 4), Text( '$confirmations confirmation${confirmations == 1 ? '' : 's'}', style: const TextStyle( color: AppColors.textHint, fontSize: 12, ), ), const SizedBox(height: 16), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: () => Navigator.pop(context, true), icon: const Icon(Icons.add), label: Text('Add to "$collectionName"'), ), ), const SizedBox(height: 8), SizedBox( width: double.infinity, child: TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('Cancel'), ), ), ], ), ); } } class _NewDiscoveryData { final String name; final String? series; final int? year; final String? notes; const _NewDiscoveryData({ required this.name, this.series, this.year, this.notes, }); } class _NewDiscoverySheet extends StatefulWidget { final String hwId; final String collectionName; const _NewDiscoverySheet({ required this.hwId, required this.collectionName, }); @override State<_NewDiscoverySheet> createState() => _NewDiscoverySheetState(); } class _NewDiscoverySheetState extends State<_NewDiscoverySheet> { final _nameController = TextEditingController(); final _seriesController = TextEditingController(); final _yearController = TextEditingController(); final _notesController = TextEditingController(); bool _isSaving = false; @override void dispose() { _nameController.dispose(); _seriesController.dispose(); _yearController.dispose(); _notesController.dispose(); super.dispose(); } void _save() { final name = _nameController.text.trim(); if (name.isEmpty) { showGlobalSnackBar('Name is required for a new discovery.', isError: true); return; } setState(() => _isSaving = true); Navigator.pop( context, _NewDiscoveryData( name: name, series: _seriesController.text.trim().isEmpty ? null : _seriesController.text.trim(), year: int.tryParse(_yearController.text.trim()), notes: _notesController.text.trim().isEmpty ? null : _notesController.text.trim(), ), ); } @override Widget build(BuildContext context) { return Padding( padding: EdgeInsets.only( left: 20, right: 20, top: 20, bottom: MediaQuery.of(context).viewInsets.bottom + 20, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'New Discovery: ${widget.hwId}', style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w700, ), ), const SizedBox(height: 14), TextField( controller: _nameController, autofocus: true, decoration: const InputDecoration( labelText: 'Name *', hintText: "e.g. '70 Dodge Charger", ), ), const SizedBox(height: 10), TextField( controller: _seriesController, decoration: const InputDecoration(labelText: 'Series'), ), const SizedBox(height: 10), TextField( controller: _yearController, keyboardType: TextInputType.number, decoration: const InputDecoration(labelText: 'Year'), ), const SizedBox(height: 10), TextField( controller: _notesController, maxLines: 2, decoration: const InputDecoration( labelText: 'Notes (for your garage entry)', ), ), const SizedBox(height: 16), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: _isSaving ? null : _save, icon: const Icon(Icons.save_outlined), label: Text('Save & Add to "${widget.collectionName}"'), ), ), const SizedBox(height: 8), SizedBox( width: double.infinity, child: TextButton( onPressed: _isSaving ? null : () => Navigator.pop(context), child: const Text('Cancel'), ), ), ], ), ); } }