import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import '../data/variety_repository.dart'; import '../i18n/strings.g.dart'; import '../services/ocr/label_text_extractor.dart'; import 'photo_pick.dart'; /// Rapid "capture now, catalogue later": shoot/pick a burst of photos and turn /// each into a draft variety. Returns how many were captured. Future captureDraftBurst( BuildContext context, VarietyRepository repository, ) async { final photos = await pickPhotos(context); for (final bytes in photos) { await repository.addDraftVariety(bytes); } return photos.length; } /// The "to catalogue" tray: lists photo-first drafts (live from the DB); tapping /// one prompts for a name and promotes it into the inventory. Future showTriageSheet( BuildContext context, VarietyRepository repository, ) { return showModalBottomSheet( context: context, isScrollControlled: true, builder: (_) => _TriageSheet(repository: repository), ); } class _TriageSheet extends StatelessWidget { const _TriageSheet({required this.repository}); final VarietyRepository repository; @override Widget build(BuildContext context) { final t = context.t; return StreamBuilder>( stream: repository.watchDrafts(), builder: (context, snapshot) { final drafts = snapshot.data ?? const []; 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.draft.triageTitle, style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 12), if (drafts.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 24), child: Text( t.inventory.empty, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium, ), ) else Flexible( child: ListView.builder( shrinkWrap: true, itemCount: drafts.length, itemBuilder: (context, i) => _DraftTile( draft: drafts[i], repository: repository, ), ), ), ], ), ); }, ); } } class _DraftTile extends StatelessWidget { const _DraftTile({required this.draft, required this.repository}); final VarietyListItem draft; final VarietyRepository repository; @override Widget build(BuildContext context) { final t = context.t; final photo = draft.photo; return ListTile( key: Key('draft.tile.${draft.id}'), leading: photo == null ? const Icon(Icons.image_outlined) : ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.memory( photo, width: 48, height: 48, fit: BoxFit.cover, ), ), title: Text(t.draft.untitled), trailing: IconButton( icon: const Icon(Icons.delete_outline), tooltip: t.draft.discard, onPressed: () => repository.softDeleteVariety(draft.id), ), onTap: () async { final name = await showDialog( context: context, builder: (_) => NameDraftDialog(photo: photo), ); if (name != null && name.trim().isNotEmpty) { await repository.nameDraft(draft.id, name.trim()); } }, ); } } /// Prompts for a draft's name, showing its photo. Where an on-device OCR engine /// exists, a "Suggest name from photo" button pre-fills the field (always /// editable, never auto-confirmed). class NameDraftDialog extends StatefulWidget { const NameDraftDialog({required this.photo, this.extractor, super.key}); final Uint8List? photo; /// Injectable for tests; otherwise resolved from the service locator (or a /// no-op when none is registered). final LabelTextExtractor? extractor; @override State createState() => _NameDraftDialogState(); } class _NameDraftDialogState extends State { final _controller = TextEditingController(); bool _suggesting = false; LabelTextExtractor get _extractor => widget.extractor ?? (GetIt.instance.isRegistered() ? GetIt.instance() : const NoOpLabelTextExtractor()); @override void dispose() { _controller.dispose(); super.dispose(); } Future _suggest() async { final photo = widget.photo; if (photo == null) return; setState(() => _suggesting = true); final suggestion = await _extractor.suggestLabel(photo); if (!mounted) return; setState(() { _suggesting = false; if (suggestion != null && suggestion.trim().isNotEmpty) { _controller.text = suggestion.trim(); } }); } @override Widget build(BuildContext context) { final t = context.t; final canSuggest = widget.photo != null && _extractor.isSupported; return AlertDialog( title: Text(t.draft.nameField), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (widget.photo != null) ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.memory( widget.photo!, height: 140, fit: BoxFit.cover, ), ), const SizedBox(height: 12), TextField( key: const Key('draft.nameField'), controller: _controller, autofocus: true, textCapitalization: TextCapitalization.sentences, decoration: InputDecoration( hintText: t.draft.nameHint, border: const OutlineInputBorder(), ), onSubmitted: (value) => Navigator.of(context).pop(value), ), if (canSuggest) ...[ const SizedBox(height: 8), Align( alignment: AlignmentDirectional.centerStart, child: TextButton.icon( key: const Key('draft.suggestFromPhoto'), onPressed: _suggesting ? null : _suggest, icon: _suggesting ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.auto_fix_high_outlined), label: Text(t.draft.suggestFromPhoto), ), ), ], ], ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text(t.common.cancel), ), FilledButton( key: const Key('draft.nameSave'), onPressed: () => Navigator.of(context).pop(_controller.text), child: Text(t.common.save), ), ], ); } }