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

@@ -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<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<void> _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<String> 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<String>(
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),
],
);
}
}

View File

@@ -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<ProductSearchScreen> {
});
}
void _openBarcodeScanner() {
Navigator.push<CatalogProduct>(
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<ProductSearchScreen> {
showModalBottomSheet<void>(
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<ProductSearchScreen> {
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<void> _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<String> 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<String>(
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),
],
);
}
}

View File

@@ -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<ShelfBarcodeScanScreen> createState() =>
_ShelfBarcodeScanScreenState();
}
class _ShelfBarcodeScanScreenState
extends ConsumerState<ShelfBarcodeScanScreen> {
bool _scanning = true;
bool _lookingUp = false;
Future<void> _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(),
),
),
),
],
),
);
}
}

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,

View File

@@ -100,6 +100,8 @@
"navProducts": "المنتجات",
"navRecipes": "الوصفات",
"addFromReceiptOrPhoto": "إضافة من الإيصال أو الصورة",
"scanScreenTitle": "المسح والتعرف",
"barcodeScanSubtitle": "ابحث عن منتج بالباركود",
"chooseMethod": "اختر الطريقة",
"photoReceipt": "تصوير الإيصال",
"photoReceiptSubtitle": "التعرف على جميع المنتجات من الإيصال",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -100,6 +100,8 @@
"navProducts": "उत्पाद",
"navRecipes": "रेसिपी",
"addFromReceiptOrPhoto": "रसीद या फ़ोटो से जोड़ें",
"scanScreenTitle": "स्कैन और पहचानें",
"barcodeScanSubtitle": "बारकोड से उत्पाद खोजें",
"chooseMethod": "तरीका चुनें",
"photoReceipt": "रसीद की फ़ोटो",
"photoReceiptSubtitle": "रसीद से सभी उत्पाद पहचानें",

View File

@@ -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",

View File

@@ -100,6 +100,8 @@
"navProducts": "食品",
"navRecipes": "レシピ",
"addFromReceiptOrPhoto": "レシートや写真から追加",
"scanScreenTitle": "スキャン&認識",
"barcodeScanSubtitle": "バーコードで商品を探す",
"chooseMethod": "方法を選択",
"photoReceipt": "レシートを撮影",
"photoReceiptSubtitle": "レシートから全商品を認識",

View File

@@ -100,6 +100,8 @@
"navProducts": "식품",
"navRecipes": "레시피",
"addFromReceiptOrPhoto": "영수증 또는 사진으로 추가",
"scanScreenTitle": "스캔 및 인식",
"barcodeScanSubtitle": "바코드로 제품 찾기",
"chooseMethod": "방법 선택",
"photoReceipt": "영수증 촬영",
"photoReceiptSubtitle": "영수증의 모든 상품 인식",

View File

@@ -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:

View File

@@ -290,6 +290,12 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get addFromReceiptOrPhoto => 'إضافة من الإيصال أو الصورة';
@override
String get scanScreenTitle => 'المسح والتعرف';
@override
String get barcodeScanSubtitle => 'ابحث عن منتج بالباركود';
@override
String get chooseMethod => 'اختر الطريقة';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -291,6 +291,12 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get addFromReceiptOrPhoto => 'रसीद या फ़ोटो से जोड़ें';
@override
String get scanScreenTitle => 'स्कैन और पहचानें';
@override
String get barcodeScanSubtitle => 'बारकोड से उत्पाद खोजें';
@override
String get chooseMethod => 'तरीका चुनें';

View File

@@ -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';

View File

@@ -289,6 +289,12 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get addFromReceiptOrPhoto => 'レシートや写真から追加';
@override
String get scanScreenTitle => 'スキャン&認識';
@override
String get barcodeScanSubtitle => 'バーコードで商品を探す';
@override
String get chooseMethod => '方法を選択';

View File

@@ -289,6 +289,12 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get addFromReceiptOrPhoto => '영수증 또는 사진으로 추가';
@override
String get scanScreenTitle => '스캔 및 인식';
@override
String get barcodeScanSubtitle => '바코드로 제품 찾기';
@override
String get chooseMethod => '방법 선택';

View File

@@ -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';

View File

@@ -290,6 +290,12 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get addFromReceiptOrPhoto => 'Добавить из чека или фото';
@override
String get scanScreenTitle => 'Сканировать';
@override
String get barcodeScanSubtitle => 'Найти продукт по штрихкоду';
@override
String get chooseMethod => 'Выберите способ';

View File

@@ -289,6 +289,12 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get addFromReceiptOrPhoto => '从收据或照片添加';
@override
String get scanScreenTitle => '扫描与识别';
@override
String get barcodeScanSubtitle => '通过条形码查找产品';
@override
String get chooseMethod => '选择方式';

View File

@@ -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",

View File

@@ -100,6 +100,8 @@
"navProducts": "Продукты",
"navRecipes": "Рецепты",
"addFromReceiptOrPhoto": "Добавить из чека или фото",
"scanScreenTitle": "Сканировать",
"barcodeScanSubtitle": "Найти продукт по штрихкоду",
"chooseMethod": "Выберите способ",
"photoReceipt": "Сфотографировать чек",
"photoReceiptSubtitle": "Распознаем все продукты из чека",

View File

@@ -100,6 +100,8 @@
"navProducts": "食品",
"navRecipes": "食谱",
"addFromReceiptOrPhoto": "从收据或照片添加",
"scanScreenTitle": "扫描与识别",
"barcodeScanSubtitle": "通过条形码查找产品",
"chooseMethod": "选择方式",
"photoReceipt": "拍摄收据",
"photoReceiptSubtitle": "识别收据中的所有商品",