feat: barcode scanning for shelf add + scan screen barcode option

- Add ShelfBarcodeScanScreen: scans barcode via mobile_scanner, looks up
  product via GET /products/barcode/{barcode} (Open Food Facts fallback),
  returns CatalogProduct to caller; loading overlay while looking up;
  "Add manually" fallback in AppBar for unknown products
- Extract AddToShelfSheet to add_to_shelf_sheet.dart (was private in
  product_search_screen.dart) so both search and scan screens can reuse it
- Add barcode icon button to ProductSearchScreen AppBar → opens scanner
- Add "Scan barcode" card (📷) to ScanScreen alongside receipt and photo modes
- Rename ScanScreen title: addFromReceiptOrPhoto → scanScreenTitle
  ("Сканировать" / "Scan & Recognize") to reflect all three modes
- Add 2 L10n keys (scanScreenTitle, barcodeScanSubtitle) across all 12 locales

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
dbastrikin
2026-03-26 15:21:56 +02:00
parent 7b2f86c6a4
commit b2bdcbae6f
29 changed files with 537 additions and 277 deletions

View File

@@ -4,6 +4,9 @@ import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import '../../l10n/app_localizations.dart';
import '../../shared/models/product.dart';
import '../products/add_to_shelf_sheet.dart';
import '../products/shelf_barcode_scan_screen.dart';
import 'recognition_service.dart';
/// Entry screen — lets the user choose how to add products.
@@ -19,7 +22,7 @@ class _ScanScreenState extends ConsumerState<ScanScreen> {
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(l10n.addFromReceiptOrPhoto)),
appBar: AppBar(title: Text(l10n.scanScreenTitle)),
body: ListView(
padding: const EdgeInsets.all(20),
children: [
@@ -43,11 +46,43 @@ class _ScanScreenState extends ConsumerState<ScanScreen> {
subtitle: l10n.photoProductsSubtitle,
onTap: () => _pickAndRecognize(context, _Mode.products),
),
const SizedBox(height: 16),
_ModeCard(
emoji: '📷',
title: l10n.scanBarcode,
subtitle: l10n.barcodeScanSubtitle,
onTap: _openBarcode,
),
],
),
);
}
void _openBarcode() {
final messenger = ScaffoldMessenger.of(context);
final addedText = AppLocalizations.of(context)!.productAddedToShelf;
Navigator.push<CatalogProduct>(
context,
MaterialPageRoute(builder: (_) => const ShelfBarcodeScanScreen()),
).then((catalogProduct) {
if (catalogProduct != null && mounted) {
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
builder: (_) => AddToShelfSheet(
catalogProduct: catalogProduct,
onAdded: () => messenger.showSnackBar(
SnackBar(
content: Text('${catalogProduct.displayName}$addedText'),
),
),
),
);
}
});
}
Future<void> _pickAndRecognize(
BuildContext context,
_Mode mode,