import 'dart:typed_data'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; import '../../../l10n/generated/app_localizations.dart'; import '../../locations/data/locations_provider.dart'; import '../data/plant_recognition_service.dart'; import '../data/plants_provider.dart'; import '../domain/plant.dart'; /// Anlegen und Bearbeiten einer Pflanze. /// /// Mit Foto: Kamera/Galerie → Cloud Function (PlantNet + Claude) erkennt die /// Art und füllt Art, Beschreibung, Pflegehinweise und Intervalle vor — /// der Nutzer prüft, passt an und speichert. Ohne Foto: manuell ausfüllen. class PlantFormScreen extends ConsumerStatefulWidget { const PlantFormScreen({super.key, this.existing}); final Plant? existing; @override ConsumerState createState() => _PlantFormScreenState(); } class _PlantFormScreenState extends ConsumerState { final _formKey = GlobalKey(); final _picker = ImagePicker(); late final TextEditingController _nicknameController; late final TextEditingController _speciesController; late final TextEditingController _wateringController; late final TextEditingController _fertilizingController; String? _locationId; Uint8List? _photoBytes; bool _recognizing = false; bool _saving = false; String _description = ''; String _careNotes = ''; @override void initState() { super.initState(); final existing = widget.existing; _nicknameController = TextEditingController(text: existing?.nickname ?? ''); _speciesController = TextEditingController(text: existing?.species ?? ''); _wateringController = TextEditingController( text: existing?.wateringIntervalDays.toString() ?? '7'); _fertilizingController = TextEditingController( text: existing?.fertilizingIntervalDays.toString() ?? '28'); _locationId = existing?.locationId; _description = existing?.description ?? ''; _careNotes = existing?.careNotes ?? ''; } @override void dispose() { _nicknameController.dispose(); _speciesController.dispose(); _wateringController.dispose(); _fertilizingController.dispose(); super.dispose(); } Future _pickImage(ImageSource source) async { final picked = await _picker.pickImage( source: source, maxWidth: 1280, imageQuality: 80, ); if (picked == null) return; final bytes = await picked.readAsBytes(); setState(() => _photoBytes = bytes); await _recognize(bytes); } Future _recognize(Uint8List bytes) async { final l10n = AppLocalizations.of(context); setState(() => _recognizing = true); try { final result = await ref.read(plantRecognitionServiceProvider).identify(bytes); if (!mounted) return; setState(() { _speciesController.text = result.displaySpecies; _wateringController.text = result.wateringIntervalDays.toString(); _fertilizingController.text = result.fertilizingIntervalDays.toString(); _description = result.description; _careNotes = result.careNotes; if (_nicknameController.text.trim().isEmpty && result.germanName.isNotEmpty) { _nicknameController.text = result.germanName; } }); ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(SnackBar( content: Text(l10n.recognitionSuccess(result.displaySpecies)), )); } catch (_) { if (!mounted) return; ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(SnackBar(content: Text(l10n.recognitionFailed))); } finally { if (mounted) setState(() => _recognizing = false); } } Future _save() async { final l10n = AppLocalizations.of(context); if (!_formKey.currentState!.validate()) return; setState(() => _saving = true); try { String? photoUrl = widget.existing?.photoUrl; if (_photoBytes != null) { photoUrl = await ref .read(plantRecognitionServiceProvider) .uploadPhoto(_photoBytes!); } final repository = ref.read(plantRepositoryProvider); final existing = widget.existing; if (existing != null) { await repository.updatePlant(existing.copyWith( nickname: _nicknameController.text.trim(), species: _speciesController.text.trim(), locationId: () => _locationId, description: _description, careNotes: _careNotes, photoUrl: photoUrl, wateringIntervalDays: int.parse(_wateringController.text.trim()), fertilizingIntervalDays: int.parse(_fertilizingController.text.trim()), )); } else { await repository.addPlant(Plant( id: '', // wird von Firestore vergeben nickname: _nicknameController.text.trim(), species: _speciesController.text.trim(), locationId: _locationId, description: _description, careNotes: _careNotes, photoUrl: photoUrl, wateringIntervalDays: int.parse(_wateringController.text.trim()), fertilizingIntervalDays: int.parse(_fertilizingController.text.trim()), )); } if (mounted) context.pop(); } catch (error) { // Der konkrete Grund (z. B. firebase_storage/unauthorized vs. // cloud_firestore/permission-denied) entscheidet, wo der Fehler liegt — // deshalb wird er mit angezeigt statt verschluckt. debugPrint('Pflanze speichern fehlgeschlagen: $error'); final detail = error is FirebaseException ? '${error.plugin}/${error.code}' : error.runtimeType.toString(); if (mounted) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(SnackBar(content: Text(l10n.saveErrorDetail(detail)))); } } finally { if (mounted) setState(() => _saving = false); } } String? _validateRequired(String? value) { final l10n = AppLocalizations.of(context); if (value == null || value.trim().isEmpty) return l10n.requiredField; return null; } String? _validateInterval(String? value) { final l10n = AppLocalizations.of(context); final parsed = int.tryParse(value?.trim() ?? ''); if (parsed == null || parsed <= 0) return l10n.invalidNumber; return null; } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final locations = ref.watch(locationsProvider).value ?? const []; return Scaffold( appBar: AppBar( title: Text( widget.existing != null ? l10n.plantDetailTitle : l10n.addPlant), ), body: Form( key: _formKey, child: ListView( padding: const EdgeInsets.all(16), children: [ _PhotoSection( photoBytes: _photoBytes, existingPhotoUrl: widget.existing?.photoUrl, recognizing: _recognizing, onTakePhoto: () => _pickImage(ImageSource.camera), onPickFromGallery: () => _pickImage(ImageSource.gallery), ), const SizedBox(height: 24), TextFormField( controller: _nicknameController, decoration: InputDecoration( labelText: l10n.nicknameLabel, border: const OutlineInputBorder(), ), textInputAction: TextInputAction.next, validator: _validateRequired, ), const SizedBox(height: 16), TextFormField( controller: _speciesController, decoration: InputDecoration( labelText: l10n.speciesLabel, border: const OutlineInputBorder(), ), textInputAction: TextInputAction.next, validator: _validateRequired, ), const SizedBox(height: 16), DropdownButtonFormField( initialValue: _locationId, decoration: InputDecoration( labelText: l10n.locationLabel, border: const OutlineInputBorder(), ), items: [ DropdownMenuItem( value: null, child: Text(l10n.locationNone), ), for (final location in locations) DropdownMenuItem( value: location.id, child: Text(location.name), ), ], onChanged: (value) => setState(() => _locationId = value), ), const SizedBox(height: 16), TextFormField( controller: _wateringController, decoration: InputDecoration( labelText: l10n.wateringIntervalLabel, border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.water_drop), ), keyboardType: TextInputType.number, textInputAction: TextInputAction.next, validator: _validateInterval, ), const SizedBox(height: 16), TextFormField( controller: _fertilizingController, decoration: InputDecoration( labelText: l10n.fertilizingIntervalLabel, border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.compost), ), keyboardType: TextInputType.number, textInputAction: TextInputAction.done, validator: _validateInterval, ), const SizedBox(height: 24), FilledButton.icon( onPressed: _saving || _recognizing ? null : _save, icon: _saving ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.check), label: Text(l10n.save), ), const SizedBox(height: 8), TextButton( onPressed: _saving ? null : () => context.pop(), child: Text(l10n.cancel), ), ], ), ), ); } } class _PhotoSection extends StatelessWidget { const _PhotoSection({ required this.photoBytes, required this.existingPhotoUrl, required this.recognizing, required this.onTakePhoto, required this.onPickFromGallery, }); final Uint8List? photoBytes; final String? existingPhotoUrl; final bool recognizing; final VoidCallback onTakePhoto; final VoidCallback onPickFromGallery; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final theme = Theme.of(context); Widget preview; if (photoBytes != null) { preview = Image.memory(photoBytes!, fit: BoxFit.cover); } else if (existingPhotoUrl != null) { preview = Image.network(existingPhotoUrl!, fit: BoxFit.cover); } else { preview = Container( color: theme.colorScheme.surfaceContainerHighest, child: Icon( Icons.local_florist, size: 64, color: theme.colorScheme.onSurfaceVariant, ), ); } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ ClipRRect( borderRadius: BorderRadius.circular(16), child: SizedBox( height: 200, child: Stack( fit: StackFit.expand, children: [ preview, if (recognizing) Container( color: Colors.black45, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const CircularProgressIndicator(color: Colors.white), const SizedBox(height: 12), Text( l10n.recognizing, style: const TextStyle(color: Colors.white), ), ], ), ), ], ), ), ), const SizedBox(height: 12), Row( children: [ Expanded( child: FilledButton.tonalIcon( onPressed: recognizing ? null : onTakePhoto, icon: const Icon(Icons.photo_camera), label: Text(l10n.takePhoto), ), ), const SizedBox(width: 12), Expanded( child: FilledButton.tonalIcon( onPressed: recognizing ? null : onPickFromGallery, icon: const Icon(Icons.photo_library), label: Text(l10n.fromGallery), ), ), ], ), ], ); } }