import 'package:commons_core/commons_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../data/species_repository.dart'; import '../data/variety_repository.dart'; import '../i18n/strings.g.dart'; import '../state/variety_detail_cubit.dart'; import 'quantity_kind_l10n.dart'; /// Read + edit view of a single variety: its photo, category, notes, other /// names and lots. Edits go through [VarietyDetailCubit]; the view is reactive. class VarietyDetailScreen extends StatelessWidget { const VarietyDetailScreen({super.key}); @override Widget build(BuildContext context) { return BlocConsumer( listenWhen: (prev, curr) => !prev.deleted && curr.deleted, listener: (context, state) => Navigator.of(context).maybePop(), builder: (context, state) { if (state.loading) { return const Scaffold( body: Center(child: CircularProgressIndicator()), ); } final detail = state.detail; if (detail == null) { return Scaffold( appBar: AppBar(), body: Center(child: Text(context.t.detail.notFound)), ); } return _DetailView(detail: detail); }, ); } } class _DetailView extends StatelessWidget { const _DetailView({required this.detail}); final VarietyDetail detail; @override Widget build(BuildContext context) { final t = context.t; final cubit = context.read(); return Scaffold( appBar: AppBar( title: Text(detail.label), actions: [ IconButton( key: const Key('detail.edit'), icon: const Icon(Icons.edit_outlined), tooltip: t.common.edit, onPressed: () => _showEditSheet( context, cubit, detail, context.read(), ), ), IconButton( key: const Key('detail.delete'), icon: const Icon(Icons.delete_outline), tooltip: t.common.delete, onPressed: () => _confirmDelete(context, cubit), ), ], ), body: ListView( padding: const EdgeInsets.all(16), children: [ if (detail.scientificName != null) ...[ Text( detail.scientificName!, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontStyle: FontStyle.italic, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 12), ], if (detail.photo != null) ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.memory( detail.photo!, height: 200, width: double.infinity, fit: BoxFit.cover, ), ), if (detail.category != null) ...[ const SizedBox(height: 12), Align( alignment: Alignment.centerLeft, child: Chip(label: Text(detail.category!)), ), ], if (detail.vernacularNames.isNotEmpty) ...[ const SizedBox(height: 16), _SectionTitle(t.detail.names), Wrap( spacing: 8, children: [ for (final name in detail.vernacularNames) Chip(label: Text(name)), ], ), ], if (detail.notes != null && detail.notes!.trim().isNotEmpty) ...[ const SizedBox(height: 16), _SectionTitle(t.detail.notes), Text(detail.notes!), ], const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _SectionTitle(t.detail.lots), TextButton.icon( key: const Key('detail.addLot'), onPressed: () => _showAddLotSheet(context, cubit), icon: const Icon(Icons.add), label: Text(t.detail.addLot), ), ], ), if (detail.lots.isEmpty) Text(t.detail.noLots) else for (final lot in detail.lots) ListTile( contentPadding: EdgeInsets.zero, leading: const Icon(Icons.inventory_2_outlined), title: Text(_lotSubtitle(t, lot)), subtitle: lot.storageLocation == null ? null : Text(lot.storageLocation!), ), ], ), ); } } String _lotSubtitle(Translations t, VarietyLot lot) { final parts = [ if (lot.harvestYear != null) t.detail.year(year: lot.harvestYear!) else t.detail.noYear, if (lot.quantity != null) _quantityLabel(t, lot.quantity!), ]; return parts.join(' ยท '); } String _quantityLabel(Translations t, Quantity q) { final kind = quantityKindLabel(t, q.kind); if (q.precise != null) return '${_trimDouble(q.precise!)} $kind'; return kind; } String _trimDouble(double v) => v == v.roundToDouble() ? v.toStringAsFixed(0) : v.toString(); Future _confirmDelete( BuildContext context, VarietyDetailCubit cubit, ) async { final t = context.t; final confirmed = await showDialog( context: context, builder: (dialogContext) => AlertDialog( content: Text(t.detail.deleteConfirm), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(false), child: Text(t.common.cancel), ), FilledButton( key: const Key('detail.deleteConfirm'), onPressed: () => Navigator.of(dialogContext).pop(true), child: Text(t.common.delete), ), ], ), ); if (confirmed ?? false) await cubit.deleteVariety(); } Future _showEditSheet( BuildContext context, VarietyDetailCubit cubit, VarietyDetail detail, SpeciesRepository species, ) { return showModalBottomSheet( context: context, isScrollControlled: true, builder: (_) => _EditVarietySheet(cubit: cubit, detail: detail, species: species), ); } /// The edit sheet, stateful so it can search the species catalog live and link /// the picked species on save. class _EditVarietySheet extends StatefulWidget { const _EditVarietySheet({ required this.cubit, required this.detail, required this.species, }); final VarietyDetailCubit cubit; final VarietyDetail detail; final SpeciesRepository species; @override State<_EditVarietySheet> createState() => _EditVarietySheetState(); } class _EditVarietySheetState extends State<_EditVarietySheet> { late final TextEditingController _name = TextEditingController( text: widget.detail.label, ); late final TextEditingController _category = TextEditingController( text: widget.detail.category ?? '', ); late final TextEditingController _notes = TextEditingController( text: widget.detail.notes ?? '', ); late final TextEditingController _speciesField = TextEditingController( text: widget.detail.scientificName ?? '', ); List _suggestions = const []; String? _pickedSpeciesId; @override void dispose() { _name.dispose(); _category.dispose(); _notes.dispose(); _speciesField.dispose(); super.dispose(); } Future _onSpeciesQuery(String query) async { final lang = Localizations.localeOf(context).languageCode; final results = await widget.species.search(query, languageCode: lang); if (mounted) setState(() => _suggestions = results); } void _pick(SpeciesMatch match) { setState(() { _pickedSpeciesId = match.id; _speciesField.text = match.displayLabel; _suggestions = const []; }); } void _save() { final name = _name.text.trim(); widget.cubit.updateFields( label: name.isEmpty ? null : name, category: _nullIfBlank(_category.text), notes: _nullIfBlank(_notes.text), ); if (_pickedSpeciesId != null) { widget.cubit.linkSpecies(_pickedSpeciesId!); } Navigator.of(context).pop(); } @override Widget build(BuildContext context) { final t = context.t; return Padding( padding: EdgeInsets.only( left: 16, right: 16, top: 16, bottom: MediaQuery.of(context).viewInsets.bottom + 16, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( t.editVariety.title, style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 12), TextField( key: const Key('editVariety.name'), controller: _name, decoration: InputDecoration( labelText: t.editVariety.name, border: const OutlineInputBorder(), ), ), const SizedBox(height: 12), TextField( key: const Key('editVariety.species'), controller: _speciesField, decoration: InputDecoration( labelText: t.editVariety.species, hintText: t.editVariety.speciesHint, prefixIcon: const Icon(Icons.eco_outlined), border: const OutlineInputBorder(), ), onChanged: _onSpeciesQuery, ), for (final match in _suggestions) ListTile( dense: true, key: Key('species.option.${match.id}'), title: Text(match.displayLabel), subtitle: match.family == null ? null : Text(match.family!), onTap: () => _pick(match), ), const SizedBox(height: 12), TextField( controller: _category, decoration: InputDecoration( labelText: t.editVariety.category, border: const OutlineInputBorder(), ), ), const SizedBox(height: 12), TextField( controller: _notes, minLines: 2, maxLines: 5, decoration: InputDecoration( labelText: t.editVariety.notes, border: const OutlineInputBorder(), ), ), const SizedBox(height: 16), Row( children: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text(t.common.cancel), ), const Spacer(), FilledButton( key: const Key('editVariety.save'), onPressed: _save, child: Text(t.common.save), ), ], ), ], ), ); } } Future _showAddLotSheet(BuildContext context, VarietyDetailCubit cubit) { final t = context.t; final yearController = TextEditingController(); QuantityKind? selectedKind; return showModalBottomSheet( context: context, isScrollControlled: true, builder: (sheetContext) => StatefulBuilder( builder: (sheetContext, setState) => Padding( padding: EdgeInsets.only( left: 16, right: 16, top: 16, bottom: MediaQuery.of(sheetContext).viewInsets.bottom + 16, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( t.addLot.title, style: Theme.of(sheetContext).textTheme.titleLarge, ), const SizedBox(height: 12), TextField( key: const Key('addLot.year'), controller: yearController, keyboardType: TextInputType.number, decoration: InputDecoration( labelText: t.addLot.year, border: const OutlineInputBorder(), ), ), const SizedBox(height: 16), Text( t.addLot.quantity, style: Theme.of(sheetContext).textTheme.labelLarge, ), const SizedBox(height: 8), Wrap( spacing: 8, children: [ for (final kind in _addLotKinds) ChoiceChip( label: Text(quantityKindLabel(t, kind)), selected: selectedKind == kind, onSelected: (_) => setState(() => selectedKind = kind), ), ], ), const SizedBox(height: 16), Row( children: [ TextButton( onPressed: () => Navigator.of(sheetContext).pop(), child: Text(t.common.cancel), ), const Spacer(), FilledButton( key: const Key('addLot.save'), onPressed: () { cubit.addLot( year: int.tryParse(yearController.text.trim()), quantity: selectedKind == null ? null : Quantity(kind: selectedKind!), ); Navigator.of(sheetContext).pop(); }, child: Text(t.common.save), ), ], ), ], ), ), ), ); } const _addLotKinds = [ QuantityKind.aFew, QuantityKind.handful, QuantityKind.packet, QuantityKind.pod, QuantityKind.cob, QuantityKind.grams, ]; String? _nullIfBlank(String value) { final trimmed = value.trim(); return trimmed.isEmpty ? null : trimmed; } class _SectionTitle extends StatelessWidget { const _SectionTitle(this.text); final String text; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 8), child: Text( text, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: Theme.of(context).colorScheme.primary, ), ), ); } }