From b2bdcbae6f78101eb67da62e274ea41ac96a1ac4 Mon Sep 17 00:00:00 2001 From: dbastrikin Date: Thu, 26 Mar 2026 15:21:56 +0200 Subject: [PATCH] feat: barcode scanning for shelf add + scan screen barcode option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../features/products/add_to_shelf_sheet.dart | 282 +++++++++++++++++ .../products/product_search_screen.dart | 295 ++---------------- .../products/shelf_barcode_scan_screen.dart | 90 ++++++ client/lib/features/scan/scan_screen.dart | 37 ++- client/lib/l10n/app_ar.arb | 2 + client/lib/l10n/app_de.arb | 2 + client/lib/l10n/app_en.arb | 2 + client/lib/l10n/app_es.arb | 2 + client/lib/l10n/app_fr.arb | 2 + client/lib/l10n/app_hi.arb | 2 + client/lib/l10n/app_it.arb | 2 + client/lib/l10n/app_ja.arb | 2 + client/lib/l10n/app_ko.arb | 2 + client/lib/l10n/app_localizations.dart | 12 + client/lib/l10n/app_localizations_ar.dart | 6 + client/lib/l10n/app_localizations_de.dart | 6 + client/lib/l10n/app_localizations_en.dart | 6 + client/lib/l10n/app_localizations_es.dart | 7 + client/lib/l10n/app_localizations_fr.dart | 6 + client/lib/l10n/app_localizations_hi.dart | 6 + client/lib/l10n/app_localizations_it.dart | 7 + client/lib/l10n/app_localizations_ja.dart | 6 + client/lib/l10n/app_localizations_ko.dart | 6 + client/lib/l10n/app_localizations_pt.dart | 6 + client/lib/l10n/app_localizations_ru.dart | 6 + client/lib/l10n/app_localizations_zh.dart | 6 + client/lib/l10n/app_pt.arb | 2 + client/lib/l10n/app_ru.arb | 2 + client/lib/l10n/app_zh.arb | 2 + 29 files changed, 537 insertions(+), 277 deletions(-) create mode 100644 client/lib/features/products/add_to_shelf_sheet.dart create mode 100644 client/lib/features/products/shelf_barcode_scan_screen.dart diff --git a/client/lib/features/products/add_to_shelf_sheet.dart b/client/lib/features/products/add_to_shelf_sheet.dart new file mode 100644 index 0000000..a26ea6e --- /dev/null +++ b/client/lib/features/products/add_to_shelf_sheet.dart @@ -0,0 +1,282 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../core/locale/unit_provider.dart'; +import '../../l10n/app_localizations.dart'; +import '../../shared/models/product.dart'; +import 'user_product_provider.dart'; + +// --------------------------------------------------------------------------- +// Add to shelf bottom sheet +// --------------------------------------------------------------------------- + +class AddToShelfSheet extends ConsumerStatefulWidget { + const AddToShelfSheet({ + super.key, + required this.catalogProduct, + required this.onAdded, + }); + + final CatalogProduct catalogProduct; + final VoidCallback onAdded; + + @override + ConsumerState createState() => _AddToShelfSheetState(); +} + +class _AddToShelfSheetState extends ConsumerState { + late final TextEditingController _qtyController; + late final TextEditingController _daysController; + late String _unit; + bool _saving = false; + bool _success = false; + + @override + void initState() { + super.initState(); + _qtyController = TextEditingController(text: '1'); + _daysController = TextEditingController( + text: (widget.catalogProduct.storageDays ?? 7).toString(), + ); + _unit = widget.catalogProduct.defaultUnit ?? 'pcs'; + } + + @override + void dispose() { + _qtyController.dispose(); + _daysController.dispose(); + super.dispose(); + } + + Future _confirm() async { + final quantity = double.tryParse(_qtyController.text) ?? 1; + final storageDays = int.tryParse(_daysController.text) ?? 7; + + setState(() => _saving = true); + try { + await ref.read(userProductsProvider.notifier).create( + name: widget.catalogProduct.displayName, + quantity: quantity, + unit: _unit, + category: widget.catalogProduct.category, + storageDays: storageDays, + primaryProductId: widget.catalogProduct.id, + ); + if (mounted) { + setState(() { + _saving = false; + _success = true; + }); + await Future.delayed(const Duration(milliseconds: 700)); + if (mounted) { + widget.onAdded(); + Navigator.pop(context); + } + } + } catch (_) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(AppLocalizations.of(context)!.errorGeneric)), + ); + setState(() => _saving = false); + } + } + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final insets = MediaQuery.viewInsetsOf(context); + final theme = Theme.of(context); + + return Padding( + padding: EdgeInsets.fromLTRB(16, 16, 16, 16 + insets.bottom), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!_success) ...[ + Text( + widget.catalogProduct.displayName, + style: theme.textTheme.titleMedium, + ), + if (widget.catalogProduct.categoryName != null) ...[ + const SizedBox(height: 2), + Text( + widget.catalogProduct.categoryName!, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + const SizedBox(height: 16), + ], + AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + child: _success + ? _SuccessView( + key: const ValueKey('success'), + productName: widget.catalogProduct.displayName, + ) + : _ShelfForm( + key: const ValueKey('form'), + l10n: l10n, + theme: theme, + qtyController: _qtyController, + daysController: _daysController, + unit: _unit, + saving: _saving, + onUnitChanged: (value) => setState(() => _unit = value), + onConfirm: _confirm, + ), + ), + ], + ), + ); + } +} + +// --------------------------------------------------------------------------- +// Form content extracted for AnimatedSwitcher key stability +// --------------------------------------------------------------------------- + +class _ShelfForm extends ConsumerWidget { + const _ShelfForm({ + super.key, + required this.l10n, + required this.theme, + required this.qtyController, + required this.daysController, + required this.unit, + required this.saving, + required this.onUnitChanged, + required this.onConfirm, + }); + + final AppLocalizations l10n; + final ThemeData theme; + final TextEditingController qtyController; + final TextEditingController daysController; + final String unit; + final bool saving; + final ValueChanged onUnitChanged; + final VoidCallback onConfirm; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Expanded( + child: TextField( + controller: qtyController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: InputDecoration( + labelText: l10n.quantity, + border: const OutlineInputBorder(), + ), + ), + ), + const SizedBox(width: 12), + ref.watch(unitsProvider).when( + data: (units) => DropdownButtonHideUnderline( + child: DropdownButton( + value: units.containsKey(unit) ? unit : units.keys.first, + items: units.entries + .map((entry) => DropdownMenuItem( + value: entry.key, + child: Text(entry.value), + )) + .toList(), + onChanged: (value) => onUnitChanged(value!), + ), + ), + loading: () => const SizedBox( + width: 60, + child: LinearProgressIndicator(), + ), + error: (_, __) => const Text('?'), + ), + ], + ), + const SizedBox(height: 12), + TextField( + controller: daysController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + labelText: l10n.storageDays, + border: const OutlineInputBorder(), + ), + ), + const SizedBox(height: 20), + FilledButton( + onPressed: saving ? null : onConfirm, + child: saving + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Text(l10n.addToShelf), + ), + ], + ); + } +} + +// --------------------------------------------------------------------------- +// Success view shown briefly after the product is added +// --------------------------------------------------------------------------- + +class _SuccessView extends StatelessWidget { + const _SuccessView({super.key, required this.productName}); + + final String productName; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final theme = Theme.of(context); + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 24), + Center( + child: Container( + width: 64, + height: 64, + decoration: BoxDecoration( + color: Colors.green.shade50, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.check_rounded, + color: Colors.green, + size: 36, + ), + ), + ), + const SizedBox(height: 16), + Text( + productName, + textAlign: TextAlign.center, + style: theme.textTheme.titleMedium, + ), + const SizedBox(height: 4), + Text( + l10n.productAddedToShelf, + textAlign: TextAlign.center, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 32), + ], + ); + } +} diff --git a/client/lib/features/products/product_search_screen.dart b/client/lib/features/products/product_search_screen.dart index 641f61c..48645e5 100644 --- a/client/lib/features/products/product_search_screen.dart +++ b/client/lib/features/products/product_search_screen.dart @@ -4,9 +4,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import '../../core/locale/unit_provider.dart'; import '../../l10n/app_localizations.dart'; import '../../shared/models/product.dart'; +import 'add_to_shelf_sheet.dart'; +import 'shelf_barcode_scan_screen.dart'; import 'user_product_provider.dart'; class ProductSearchScreen extends ConsumerStatefulWidget { @@ -52,6 +53,17 @@ class _ProductSearchScreenState extends ConsumerState { }); } + void _openBarcodeScanner() { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const ShelfBarcodeScanScreen()), + ).then((catalogProduct) { + if (catalogProduct != null && mounted) { + _openShelfSheet(catalogProduct); + } + }); + } + void _openShelfSheet(CatalogProduct catalogProduct) { final messenger = ScaffoldMessenger.of(context); final productName = catalogProduct.displayName; @@ -60,7 +72,7 @@ class _ProductSearchScreenState extends ConsumerState { showModalBottomSheet( context: context, isScrollControlled: true, - builder: (_) => _AddToShelfSheet( + builder: (_) => AddToShelfSheet( catalogProduct: catalogProduct, onAdded: () => messenger.showSnackBar( SnackBar(content: Text('$productName — $addedText')), @@ -77,6 +89,11 @@ class _ProductSearchScreenState extends ConsumerState { appBar: AppBar( title: Text(l10n.addProduct), actions: [ + IconButton( + icon: const Icon(Icons.qr_code_scanner), + tooltip: l10n.scanBarcode, + onPressed: _openBarcodeScanner, + ), TextButton( onPressed: () => context.push('/products/add'), child: Text(l10n.addManually), @@ -305,277 +322,3 @@ class _CatalogProductTile extends StatelessWidget { } } } - -// --------------------------------------------------------------------------- -// Add to shelf bottom sheet -// --------------------------------------------------------------------------- - -class _AddToShelfSheet extends ConsumerStatefulWidget { - const _AddToShelfSheet({ - required this.catalogProduct, - required this.onAdded, - }); - - final CatalogProduct catalogProduct; - final VoidCallback onAdded; - - @override - ConsumerState<_AddToShelfSheet> createState() => _AddToShelfSheetState(); -} - -class _AddToShelfSheetState extends ConsumerState<_AddToShelfSheet> { - late final TextEditingController _qtyController; - late final TextEditingController _daysController; - late String _unit; - bool _saving = false; - bool _success = false; - - @override - void initState() { - super.initState(); - _qtyController = TextEditingController(text: '1'); - _daysController = TextEditingController( - text: (widget.catalogProduct.storageDays ?? 7).toString(), - ); - _unit = widget.catalogProduct.defaultUnit ?? 'pcs'; - } - - @override - void dispose() { - _qtyController.dispose(); - _daysController.dispose(); - super.dispose(); - } - - Future _confirm() async { - final quantity = double.tryParse(_qtyController.text) ?? 1; - final storageDays = int.tryParse(_daysController.text) ?? 7; - - setState(() => _saving = true); - try { - await ref.read(userProductsProvider.notifier).create( - name: widget.catalogProduct.displayName, - quantity: quantity, - unit: _unit, - category: widget.catalogProduct.category, - storageDays: storageDays, - primaryProductId: widget.catalogProduct.id, - ); - if (mounted) { - setState(() { - _saving = false; - _success = true; - }); - await Future.delayed(const Duration(milliseconds: 700)); - if (mounted) { - widget.onAdded(); - Navigator.pop(context); - } - } - } catch (_) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(AppLocalizations.of(context)!.errorGeneric)), - ); - setState(() => _saving = false); - } - } - } - - @override - Widget build(BuildContext context) { - final l10n = AppLocalizations.of(context)!; - final insets = MediaQuery.viewInsetsOf(context); - final theme = Theme.of(context); - - return Padding( - padding: EdgeInsets.fromLTRB(16, 16, 16, 16 + insets.bottom), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (!_success) ...[ - Text( - widget.catalogProduct.displayName, - style: theme.textTheme.titleMedium, - ), - if (widget.catalogProduct.categoryName != null) ...[ - const SizedBox(height: 2), - Text( - widget.catalogProduct.categoryName!, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ], - const SizedBox(height: 16), - ], - AnimatedSwitcher( - duration: const Duration(milliseconds: 250), - child: _success - ? _SuccessView( - key: const ValueKey('success'), - productName: widget.catalogProduct.displayName, - ) - : _ShelfForm( - key: const ValueKey('form'), - l10n: l10n, - theme: theme, - qtyController: _qtyController, - daysController: _daysController, - unit: _unit, - saving: _saving, - onUnitChanged: (value) => setState(() => _unit = value), - onConfirm: _confirm, - ), - ), - ], - ), - ); - } -} - -// --------------------------------------------------------------------------- -// Form content extracted for AnimatedSwitcher key stability -// --------------------------------------------------------------------------- - -class _ShelfForm extends ConsumerWidget { - const _ShelfForm({ - super.key, - required this.l10n, - required this.theme, - required this.qtyController, - required this.daysController, - required this.unit, - required this.saving, - required this.onUnitChanged, - required this.onConfirm, - }); - - final AppLocalizations l10n; - final ThemeData theme; - final TextEditingController qtyController; - final TextEditingController daysController; - final String unit; - final bool saving; - final ValueChanged onUnitChanged; - final VoidCallback onConfirm; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - Expanded( - child: TextField( - controller: qtyController, - keyboardType: - const TextInputType.numberWithOptions(decimal: true), - decoration: InputDecoration( - labelText: l10n.quantity, - border: const OutlineInputBorder(), - ), - ), - ), - const SizedBox(width: 12), - ref.watch(unitsProvider).when( - data: (units) => DropdownButtonHideUnderline( - child: DropdownButton( - value: units.containsKey(unit) ? unit : units.keys.first, - items: units.entries - .map((entry) => DropdownMenuItem( - value: entry.key, - child: Text(entry.value), - )) - .toList(), - onChanged: (value) => onUnitChanged(value!), - ), - ), - loading: () => const SizedBox( - width: 60, - child: LinearProgressIndicator(), - ), - error: (_, __) => const Text('?'), - ), - ], - ), - const SizedBox(height: 12), - TextField( - controller: daysController, - keyboardType: TextInputType.number, - decoration: InputDecoration( - labelText: l10n.storageDays, - border: const OutlineInputBorder(), - ), - ), - const SizedBox(height: 20), - FilledButton( - onPressed: saving ? null : onConfirm, - child: saving - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : Text(l10n.addToShelf), - ), - ], - ); - } -} - -// --------------------------------------------------------------------------- -// Success view shown briefly after the product is added -// --------------------------------------------------------------------------- - -class _SuccessView extends StatelessWidget { - const _SuccessView({super.key, required this.productName}); - - final String productName; - - @override - Widget build(BuildContext context) { - final l10n = AppLocalizations.of(context)!; - final theme = Theme.of(context); - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox(height: 24), - Center( - child: Container( - width: 64, - height: 64, - decoration: BoxDecoration( - color: Colors.green.shade50, - shape: BoxShape.circle, - ), - child: const Icon( - Icons.check_rounded, - color: Colors.green, - size: 36, - ), - ), - ), - const SizedBox(height: 16), - Text( - productName, - textAlign: TextAlign.center, - style: theme.textTheme.titleMedium, - ), - const SizedBox(height: 4), - Text( - l10n.productAddedToShelf, - textAlign: TextAlign.center, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 32), - ], - ); - } -} diff --git a/client/lib/features/products/shelf_barcode_scan_screen.dart b/client/lib/features/products/shelf_barcode_scan_screen.dart new file mode 100644 index 0000000..c7c6b06 --- /dev/null +++ b/client/lib/features/products/shelf_barcode_scan_screen.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +import '../../l10n/app_localizations.dart'; +import '../../shared/models/product.dart'; +import 'user_product_provider.dart'; + +/// Barcode scanner for the shelf / pantry add flow. +/// +/// Returns the found [CatalogProduct] via [Navigator.pop] so the caller +/// can open [_AddToShelfSheet] for the user to review and adjust quantity. +/// On failure the user can tap "Add manually" to open [AddProductScreen]. +class ShelfBarcodeScanScreen extends ConsumerStatefulWidget { + const ShelfBarcodeScanScreen({super.key}); + + @override + ConsumerState createState() => + _ShelfBarcodeScanScreenState(); +} + +class _ShelfBarcodeScanScreenState + extends ConsumerState { + bool _scanning = true; + bool _lookingUp = false; + + Future _onBarcodeDetected(BarcodeCapture capture) async { + if (!_scanning || _lookingUp) return; + final rawValue = capture.barcodes.firstOrNull?.rawValue; + if (rawValue == null) return; + + setState(() { + _scanning = false; + _lookingUp = true; + }); + + final service = ref.read(userProductServiceProvider); + final catalogProduct = await service.getByBarcode(rawValue); + + if (!mounted) return; + + if (catalogProduct != null) { + Navigator.pop(context, catalogProduct); + return; + } + + final l10n = AppLocalizations.of(context)!; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.productNotFound)), + ); + setState(() { + _scanning = true; + _lookingUp = false; + }); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + return Scaffold( + appBar: AppBar( + title: Text(l10n.scanBarcode), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + context.push('/products/add'); + }, + child: Text(l10n.addManually), + ), + ], + ), + body: Stack( + children: [ + MobileScanner(onDetect: _onBarcodeDetected), + if (_lookingUp) + const Center( + child: Card( + child: Padding( + padding: EdgeInsets.all(20), + child: CircularProgressIndicator(), + ), + ), + ), + ], + ), + ); + } +} diff --git a/client/lib/features/scan/scan_screen.dart b/client/lib/features/scan/scan_screen.dart index abde743..008fb2b 100644 --- a/client/lib/features/scan/scan_screen.dart +++ b/client/lib/features/scan/scan_screen.dart @@ -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 { 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 { 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( + context, + MaterialPageRoute(builder: (_) => const ShelfBarcodeScanScreen()), + ).then((catalogProduct) { + if (catalogProduct != null && mounted) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (_) => AddToShelfSheet( + catalogProduct: catalogProduct, + onAdded: () => messenger.showSnackBar( + SnackBar( + content: Text('${catalogProduct.displayName} — $addedText'), + ), + ), + ), + ); + } + }); + } + Future _pickAndRecognize( BuildContext context, _Mode mode, diff --git a/client/lib/l10n/app_ar.arb b/client/lib/l10n/app_ar.arb index 669d1a8..292ee26 100644 --- a/client/lib/l10n/app_ar.arb +++ b/client/lib/l10n/app_ar.arb @@ -100,6 +100,8 @@ "navProducts": "المنتجات", "navRecipes": "الوصفات", "addFromReceiptOrPhoto": "إضافة من الإيصال أو الصورة", + "scanScreenTitle": "المسح والتعرف", + "barcodeScanSubtitle": "ابحث عن منتج بالباركود", "chooseMethod": "اختر الطريقة", "photoReceipt": "تصوير الإيصال", "photoReceiptSubtitle": "التعرف على جميع المنتجات من الإيصال", diff --git a/client/lib/l10n/app_de.arb b/client/lib/l10n/app_de.arb index c00833c..8d9473e 100644 --- a/client/lib/l10n/app_de.arb +++ b/client/lib/l10n/app_de.arb @@ -100,6 +100,8 @@ "navProducts": "Produkte", "navRecipes": "Rezepte", "addFromReceiptOrPhoto": "Aus Kassenbon oder Foto hinzufügen", + "scanScreenTitle": "Scannen & Erkennen", + "barcodeScanSubtitle": "Produkt per Barcode finden", "chooseMethod": "Methode wählen", "photoReceipt": "Kassenbon fotografieren", "photoReceiptSubtitle": "Alle Produkte vom Kassenbon erkennen", diff --git a/client/lib/l10n/app_en.arb b/client/lib/l10n/app_en.arb index b794ff3..5f9bac7 100644 --- a/client/lib/l10n/app_en.arb +++ b/client/lib/l10n/app_en.arb @@ -100,6 +100,8 @@ "navProducts": "Products", "navRecipes": "Recipes", "addFromReceiptOrPhoto": "Add from receipt or photo", + "scanScreenTitle": "Scan & Recognize", + "barcodeScanSubtitle": "Find a product by its barcode", "chooseMethod": "Choose method", "photoReceipt": "Photo of receipt", "photoReceiptSubtitle": "Recognize all items from a receipt", diff --git a/client/lib/l10n/app_es.arb b/client/lib/l10n/app_es.arb index eb08c03..7e52553 100644 --- a/client/lib/l10n/app_es.arb +++ b/client/lib/l10n/app_es.arb @@ -100,6 +100,8 @@ "navProducts": "Productos", "navRecipes": "Recetas", "addFromReceiptOrPhoto": "Añadir desde recibo o foto", + "scanScreenTitle": "Escanear y Reconocer", + "barcodeScanSubtitle": "Encontrar un producto por su código de barras", "chooseMethod": "Elegir método", "photoReceipt": "Fotografiar recibo", "photoReceiptSubtitle": "Reconocemos todos los productos del recibo", diff --git a/client/lib/l10n/app_fr.arb b/client/lib/l10n/app_fr.arb index c47cf59..e64db68 100644 --- a/client/lib/l10n/app_fr.arb +++ b/client/lib/l10n/app_fr.arb @@ -100,6 +100,8 @@ "navProducts": "Produits", "navRecipes": "Recettes", "addFromReceiptOrPhoto": "Ajouter depuis ticket ou photo", + "scanScreenTitle": "Scanner & Reconnaître", + "barcodeScanSubtitle": "Trouver un produit par son code-barres", "chooseMethod": "Choisir la méthode", "photoReceipt": "Photographier le ticket", "photoReceiptSubtitle": "Reconnaissance de tous les produits du ticket", diff --git a/client/lib/l10n/app_hi.arb b/client/lib/l10n/app_hi.arb index 80c7534..9d91705 100644 --- a/client/lib/l10n/app_hi.arb +++ b/client/lib/l10n/app_hi.arb @@ -100,6 +100,8 @@ "navProducts": "उत्पाद", "navRecipes": "रेसिपी", "addFromReceiptOrPhoto": "रसीद या फ़ोटो से जोड़ें", + "scanScreenTitle": "स्कैन और पहचानें", + "barcodeScanSubtitle": "बारकोड से उत्पाद खोजें", "chooseMethod": "तरीका चुनें", "photoReceipt": "रसीद की फ़ोटो", "photoReceiptSubtitle": "रसीद से सभी उत्पाद पहचानें", diff --git a/client/lib/l10n/app_it.arb b/client/lib/l10n/app_it.arb index 0819e71..0b4c236 100644 --- a/client/lib/l10n/app_it.arb +++ b/client/lib/l10n/app_it.arb @@ -100,6 +100,8 @@ "navProducts": "Prodotti", "navRecipes": "Ricette", "addFromReceiptOrPhoto": "Aggiungi da scontrino o foto", + "scanScreenTitle": "Scansiona & Riconosci", + "barcodeScanSubtitle": "Trova un prodotto tramite il codice a barre", "chooseMethod": "Scegli il metodo", "photoReceipt": "Fotografa scontrino", "photoReceiptSubtitle": "Riconosciamo tutti i prodotti dallo scontrino", diff --git a/client/lib/l10n/app_ja.arb b/client/lib/l10n/app_ja.arb index 83c7cb4..8b274a1 100644 --- a/client/lib/l10n/app_ja.arb +++ b/client/lib/l10n/app_ja.arb @@ -100,6 +100,8 @@ "navProducts": "食品", "navRecipes": "レシピ", "addFromReceiptOrPhoto": "レシートや写真から追加", + "scanScreenTitle": "スキャン&認識", + "barcodeScanSubtitle": "バーコードで商品を探す", "chooseMethod": "方法を選択", "photoReceipt": "レシートを撮影", "photoReceiptSubtitle": "レシートから全商品を認識", diff --git a/client/lib/l10n/app_ko.arb b/client/lib/l10n/app_ko.arb index a7cc3fa..7efc4a6 100644 --- a/client/lib/l10n/app_ko.arb +++ b/client/lib/l10n/app_ko.arb @@ -100,6 +100,8 @@ "navProducts": "식품", "navRecipes": "레시피", "addFromReceiptOrPhoto": "영수증 또는 사진으로 추가", + "scanScreenTitle": "스캔 및 인식", + "barcodeScanSubtitle": "바코드로 제품 찾기", "chooseMethod": "방법 선택", "photoReceipt": "영수증 촬영", "photoReceiptSubtitle": "영수증의 모든 상품 인식", diff --git a/client/lib/l10n/app_localizations.dart b/client/lib/l10n/app_localizations.dart index 3f89a47..770f413 100644 --- a/client/lib/l10n/app_localizations.dart +++ b/client/lib/l10n/app_localizations.dart @@ -676,6 +676,18 @@ abstract class AppLocalizations { /// **'Add from receipt or photo'** String get addFromReceiptOrPhoto; + /// No description provided for @scanScreenTitle. + /// + /// In en, this message translates to: + /// **'Scan & Recognize'** + String get scanScreenTitle; + + /// No description provided for @barcodeScanSubtitle. + /// + /// In en, this message translates to: + /// **'Find a product by its barcode'** + String get barcodeScanSubtitle; + /// No description provided for @chooseMethod. /// /// In en, this message translates to: diff --git a/client/lib/l10n/app_localizations_ar.dart b/client/lib/l10n/app_localizations_ar.dart index a964383..6b4a953 100644 --- a/client/lib/l10n/app_localizations_ar.dart +++ b/client/lib/l10n/app_localizations_ar.dart @@ -290,6 +290,12 @@ class AppLocalizationsAr extends AppLocalizations { @override String get addFromReceiptOrPhoto => 'إضافة من الإيصال أو الصورة'; + @override + String get scanScreenTitle => 'المسح والتعرف'; + + @override + String get barcodeScanSubtitle => 'ابحث عن منتج بالباركود'; + @override String get chooseMethod => 'اختر الطريقة'; diff --git a/client/lib/l10n/app_localizations_de.dart b/client/lib/l10n/app_localizations_de.dart index f74c4cb..201b72e 100644 --- a/client/lib/l10n/app_localizations_de.dart +++ b/client/lib/l10n/app_localizations_de.dart @@ -291,6 +291,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String get addFromReceiptOrPhoto => 'Aus Kassenbon oder Foto hinzufügen'; + @override + String get scanScreenTitle => 'Scannen & Erkennen'; + + @override + String get barcodeScanSubtitle => 'Produkt per Barcode finden'; + @override String get chooseMethod => 'Methode wählen'; diff --git a/client/lib/l10n/app_localizations_en.dart b/client/lib/l10n/app_localizations_en.dart index 1804d72..61db5cc 100644 --- a/client/lib/l10n/app_localizations_en.dart +++ b/client/lib/l10n/app_localizations_en.dart @@ -290,6 +290,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get addFromReceiptOrPhoto => 'Add from receipt or photo'; + @override + String get scanScreenTitle => 'Scan & Recognize'; + + @override + String get barcodeScanSubtitle => 'Find a product by its barcode'; + @override String get chooseMethod => 'Choose method'; diff --git a/client/lib/l10n/app_localizations_es.dart b/client/lib/l10n/app_localizations_es.dart index 918cd46..9e26227 100644 --- a/client/lib/l10n/app_localizations_es.dart +++ b/client/lib/l10n/app_localizations_es.dart @@ -291,6 +291,13 @@ class AppLocalizationsEs extends AppLocalizations { @override String get addFromReceiptOrPhoto => 'Añadir desde recibo o foto'; + @override + String get scanScreenTitle => 'Escanear y Reconocer'; + + @override + String get barcodeScanSubtitle => + 'Encontrar un producto por su código de barras'; + @override String get chooseMethod => 'Elegir método'; diff --git a/client/lib/l10n/app_localizations_fr.dart b/client/lib/l10n/app_localizations_fr.dart index 81a7923..4ee3e8f 100644 --- a/client/lib/l10n/app_localizations_fr.dart +++ b/client/lib/l10n/app_localizations_fr.dart @@ -291,6 +291,12 @@ class AppLocalizationsFr extends AppLocalizations { @override String get addFromReceiptOrPhoto => 'Ajouter depuis ticket ou photo'; + @override + String get scanScreenTitle => 'Scanner & Reconnaître'; + + @override + String get barcodeScanSubtitle => 'Trouver un produit par son code-barres'; + @override String get chooseMethod => 'Choisir la méthode'; diff --git a/client/lib/l10n/app_localizations_hi.dart b/client/lib/l10n/app_localizations_hi.dart index 4e69f02..6add642 100644 --- a/client/lib/l10n/app_localizations_hi.dart +++ b/client/lib/l10n/app_localizations_hi.dart @@ -291,6 +291,12 @@ class AppLocalizationsHi extends AppLocalizations { @override String get addFromReceiptOrPhoto => 'रसीद या फ़ोटो से जोड़ें'; + @override + String get scanScreenTitle => 'स्कैन और पहचानें'; + + @override + String get barcodeScanSubtitle => 'बारकोड से उत्पाद खोजें'; + @override String get chooseMethod => 'तरीका चुनें'; diff --git a/client/lib/l10n/app_localizations_it.dart b/client/lib/l10n/app_localizations_it.dart index cc3b6cf..fed53ee 100644 --- a/client/lib/l10n/app_localizations_it.dart +++ b/client/lib/l10n/app_localizations_it.dart @@ -291,6 +291,13 @@ class AppLocalizationsIt extends AppLocalizations { @override String get addFromReceiptOrPhoto => 'Aggiungi da scontrino o foto'; + @override + String get scanScreenTitle => 'Scansiona & Riconosci'; + + @override + String get barcodeScanSubtitle => + 'Trova un prodotto tramite il codice a barre'; + @override String get chooseMethod => 'Scegli il metodo'; diff --git a/client/lib/l10n/app_localizations_ja.dart b/client/lib/l10n/app_localizations_ja.dart index 488eb2f..e5b730d 100644 --- a/client/lib/l10n/app_localizations_ja.dart +++ b/client/lib/l10n/app_localizations_ja.dart @@ -289,6 +289,12 @@ class AppLocalizationsJa extends AppLocalizations { @override String get addFromReceiptOrPhoto => 'レシートや写真から追加'; + @override + String get scanScreenTitle => 'スキャン&認識'; + + @override + String get barcodeScanSubtitle => 'バーコードで商品を探す'; + @override String get chooseMethod => '方法を選択'; diff --git a/client/lib/l10n/app_localizations_ko.dart b/client/lib/l10n/app_localizations_ko.dart index 4546fc9..766de8f 100644 --- a/client/lib/l10n/app_localizations_ko.dart +++ b/client/lib/l10n/app_localizations_ko.dart @@ -289,6 +289,12 @@ class AppLocalizationsKo extends AppLocalizations { @override String get addFromReceiptOrPhoto => '영수증 또는 사진으로 추가'; + @override + String get scanScreenTitle => '스캔 및 인식'; + + @override + String get barcodeScanSubtitle => '바코드로 제품 찾기'; + @override String get chooseMethod => '방법 선택'; diff --git a/client/lib/l10n/app_localizations_pt.dart b/client/lib/l10n/app_localizations_pt.dart index 5d154f5..093f386 100644 --- a/client/lib/l10n/app_localizations_pt.dart +++ b/client/lib/l10n/app_localizations_pt.dart @@ -291,6 +291,12 @@ class AppLocalizationsPt extends AppLocalizations { @override String get addFromReceiptOrPhoto => 'Adicionar de recibo ou foto'; + @override + String get scanScreenTitle => 'Escanear & Reconhecer'; + + @override + String get barcodeScanSubtitle => 'Encontrar produto pelo código de barras'; + @override String get chooseMethod => 'Escolher método'; diff --git a/client/lib/l10n/app_localizations_ru.dart b/client/lib/l10n/app_localizations_ru.dart index 875c8a7..a2e99b1 100644 --- a/client/lib/l10n/app_localizations_ru.dart +++ b/client/lib/l10n/app_localizations_ru.dart @@ -290,6 +290,12 @@ class AppLocalizationsRu extends AppLocalizations { @override String get addFromReceiptOrPhoto => 'Добавить из чека или фото'; + @override + String get scanScreenTitle => 'Сканировать'; + + @override + String get barcodeScanSubtitle => 'Найти продукт по штрихкоду'; + @override String get chooseMethod => 'Выберите способ'; diff --git a/client/lib/l10n/app_localizations_zh.dart b/client/lib/l10n/app_localizations_zh.dart index d3b9e14..7326855 100644 --- a/client/lib/l10n/app_localizations_zh.dart +++ b/client/lib/l10n/app_localizations_zh.dart @@ -289,6 +289,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get addFromReceiptOrPhoto => '从收据或照片添加'; + @override + String get scanScreenTitle => '扫描与识别'; + + @override + String get barcodeScanSubtitle => '通过条形码查找产品'; + @override String get chooseMethod => '选择方式'; diff --git a/client/lib/l10n/app_pt.arb b/client/lib/l10n/app_pt.arb index fccc907..e871b54 100644 --- a/client/lib/l10n/app_pt.arb +++ b/client/lib/l10n/app_pt.arb @@ -100,6 +100,8 @@ "navProducts": "Produtos", "navRecipes": "Receitas", "addFromReceiptOrPhoto": "Adicionar de recibo ou foto", + "scanScreenTitle": "Escanear & Reconhecer", + "barcodeScanSubtitle": "Encontrar produto pelo código de barras", "chooseMethod": "Escolher método", "photoReceipt": "Fotografar recibo", "photoReceiptSubtitle": "Reconhecemos todos os produtos do recibo", diff --git a/client/lib/l10n/app_ru.arb b/client/lib/l10n/app_ru.arb index f0f32fe..40c594e 100644 --- a/client/lib/l10n/app_ru.arb +++ b/client/lib/l10n/app_ru.arb @@ -100,6 +100,8 @@ "navProducts": "Продукты", "navRecipes": "Рецепты", "addFromReceiptOrPhoto": "Добавить из чека или фото", + "scanScreenTitle": "Сканировать", + "barcodeScanSubtitle": "Найти продукт по штрихкоду", "chooseMethod": "Выберите способ", "photoReceipt": "Сфотографировать чек", "photoReceiptSubtitle": "Распознаем все продукты из чека", diff --git a/client/lib/l10n/app_zh.arb b/client/lib/l10n/app_zh.arb index e7dda6e..9d6d1e2 100644 --- a/client/lib/l10n/app_zh.arb +++ b/client/lib/l10n/app_zh.arb @@ -100,6 +100,8 @@ "navProducts": "食品", "navRecipes": "食谱", "addFromReceiptOrPhoto": "从收据或照片添加", + "scanScreenTitle": "扫描与识别", + "barcodeScanSubtitle": "通过条形码查找产品", "chooseMethod": "选择方式", "photoReceipt": "拍摄收据", "photoReceiptSubtitle": "识别收据中的所有商品",