import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:provider/provider.dart'; import '../models/vehicle.dart'; import '../services/app_state.dart'; import '../services/ocr_service.dart'; import '../services/vin_parser.dart'; import '../widgets/image_source_sheet.dart'; /// Add/edit form for a vehicle. Pass an existing [vehicle] to edit it, or /// omit it to create a new one. class AddEditVehicleScreen extends StatefulWidget { final Vehicle? vehicle; const AddEditVehicleScreen({super.key, this.vehicle}); @override State createState() => _AddEditVehicleScreenState(); } class _AddEditVehicleScreenState extends State { final _formKey = GlobalKey(); late final TextEditingController _nicknameController; late final TextEditingController _vinController; bool _saving = false; bool _scanningVin = false; String? _error; bool get _isEditing => widget.vehicle != null; @override void initState() { super.initState(); _nicknameController = TextEditingController(text: widget.vehicle?.nickname ?? ''); _vinController = TextEditingController(text: widget.vehicle?.vin ?? ''); } @override void dispose() { _nicknameController.dispose(); _vinController.dispose(); super.dispose(); } Future _scanVin() async { final source = await chooseImageSource(context); if (source == null || !mounted) return; final picker = ImagePicker(); XFile? photo; try { photo = await picker.pickImage(source: source, imageQuality: 85); } catch (e) { if (mounted) { final sourceLabel = source == ImageSource.camera ? 'camera' : 'photo library'; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Could not open $sourceLabel: $e')), ); } return; } if (photo == null) return; setState(() => _scanningVin = true); final ocrService = OcrService(); String recognizedText = ''; try { recognizedText = await ocrService.recognizeText(File(photo.path)); } catch (_) { recognizedText = ''; } finally { ocrService.dispose(); } if (!mounted) return; final vin = VinParser.parse(recognizedText); setState(() => _scanningVin = false); if (vin != null) { _vinController.text = vin; } else if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("Couldn't read a VIN from that photo. Please enter it manually."), ), ); } } Future _save() async { if (!_formKey.currentState!.validate()) return; setState(() { _saving = true; _error = null; }); final nickname = _nicknameController.text.trim(); final appState = context.read(); try { if (_isEditing) { await appState.updateVehicle(widget.vehicle!.copyWith( vin: _vinController.text.trim(), nickname: nickname.isEmpty ? null : nickname, clearNickname: nickname.isEmpty, )); } else { await appState.addVehicle( vin: _vinController.text.trim(), nickname: nickname.isEmpty ? null : nickname, ); } if (mounted) Navigator.of(context).pop(); } on DuplicateVinException catch (e) { setState(() => _error = e.toString()); } finally { if (mounted) setState(() => _saving = false); } } Future _confirmDelete() async { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Delete vehicle?'), content: Text( 'This will also delete all fuel entries and receipt photos logged for ' '${widget.vehicle!.displayLabel}. This cannot be undone.', ), actions: [ TextButton(onPressed: () => Navigator.of(context).pop(false), child: const Text('Cancel')), TextButton( onPressed: () => Navigator.of(context).pop(true), child: const Text('Delete', style: TextStyle(color: Colors.red)), ), ], ), ); if (confirmed == true && mounted) { await context.read().deleteVehicle(widget.vehicle!.id); if (mounted) { Navigator.of(context) ..pop() ..pop(); } } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(_isEditing ? 'Edit Vehicle' : 'Add Vehicle'), actions: [ if (_isEditing) IconButton( icon: const Icon(Icons.delete_outline), tooltip: 'Delete vehicle', onPressed: _confirmDelete, ), ], ), body: Form( key: _formKey, child: ListView( padding: const EdgeInsets.all(16), children: [ TextFormField( controller: _nicknameController, decoration: const InputDecoration( labelText: 'Nickname (optional)', hintText: 'e.g. Mom\'s Car, Red Ford F-150', ), textCapitalization: TextCapitalization.words, ), const SizedBox(height: 12), TextFormField( controller: _vinController, decoration: InputDecoration( labelText: 'VIN *', hintText: 'Vehicle Identification Number', helperText: 'Required — tap the camera to scan it from a photo', suffixIcon: _scanningVin ? const Padding( padding: EdgeInsets.all(12), child: SizedBox( height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2), ), ) : IconButton( icon: const Icon(Icons.camera_alt_outlined), tooltip: 'Scan VIN from a photo', onPressed: _scanVin, ), ), textCapitalization: TextCapitalization.characters, validator: (v) => (v == null || v.trim().isEmpty) ? 'Required' : null, ), if (_error != null) ...[ const SizedBox(height: 12), Text(_error!, style: TextStyle(color: Theme.of(context).colorScheme.error)), ], const SizedBox(height: 24), FilledButton( onPressed: _saving ? null : _save, child: _saving ? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2)) : Text(_isEditing ? 'Save Changes' : 'Add Vehicle'), ), ], ), ), ); } }