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:
282
client/lib/features/products/add_to_shelf_sheet.dart
Normal file
282
client/lib/features/products/add_to_shelf_sheet.dart
Normal 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),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,9 +4,10 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../../core/locale/unit_provider.dart';
|
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
import '../../shared/models/product.dart';
|
import '../../shared/models/product.dart';
|
||||||
|
import 'add_to_shelf_sheet.dart';
|
||||||
|
import 'shelf_barcode_scan_screen.dart';
|
||||||
import 'user_product_provider.dart';
|
import 'user_product_provider.dart';
|
||||||
|
|
||||||
class ProductSearchScreen extends ConsumerStatefulWidget {
|
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) {
|
void _openShelfSheet(CatalogProduct catalogProduct) {
|
||||||
final messenger = ScaffoldMessenger.of(context);
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
final productName = catalogProduct.displayName;
|
final productName = catalogProduct.displayName;
|
||||||
@@ -60,7 +72,7 @@ class _ProductSearchScreenState extends ConsumerState<ProductSearchScreen> {
|
|||||||
showModalBottomSheet<void>(
|
showModalBottomSheet<void>(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
builder: (_) => _AddToShelfSheet(
|
builder: (_) => AddToShelfSheet(
|
||||||
catalogProduct: catalogProduct,
|
catalogProduct: catalogProduct,
|
||||||
onAdded: () => messenger.showSnackBar(
|
onAdded: () => messenger.showSnackBar(
|
||||||
SnackBar(content: Text('$productName — $addedText')),
|
SnackBar(content: Text('$productName — $addedText')),
|
||||||
@@ -77,6 +89,11 @@ class _ProductSearchScreenState extends ConsumerState<ProductSearchScreen> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(l10n.addProduct),
|
title: Text(l10n.addProduct),
|
||||||
actions: [
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.qr_code_scanner),
|
||||||
|
tooltip: l10n.scanBarcode,
|
||||||
|
onPressed: _openBarcodeScanner,
|
||||||
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => context.push('/products/add'),
|
onPressed: () => context.push('/products/add'),
|
||||||
child: Text(l10n.addManually),
|
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),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
90
client/lib/features/products/shelf_barcode_scan_screen.dart
Normal file
90
client/lib/features/products/shelf_barcode_scan_screen.dart
Normal 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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,9 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|
||||||
import '../../l10n/app_localizations.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';
|
import 'recognition_service.dart';
|
||||||
|
|
||||||
/// Entry screen — lets the user choose how to add products.
|
/// Entry screen — lets the user choose how to add products.
|
||||||
@@ -19,7 +22,7 @@ class _ScanScreenState extends ConsumerState<ScanScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: Text(l10n.addFromReceiptOrPhoto)),
|
appBar: AppBar(title: Text(l10n.scanScreenTitle)),
|
||||||
body: ListView(
|
body: ListView(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
children: [
|
children: [
|
||||||
@@ -43,11 +46,43 @@ class _ScanScreenState extends ConsumerState<ScanScreen> {
|
|||||||
subtitle: l10n.photoProductsSubtitle,
|
subtitle: l10n.photoProductsSubtitle,
|
||||||
onTap: () => _pickAndRecognize(context, _Mode.products),
|
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(
|
Future<void> _pickAndRecognize(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
_Mode mode,
|
_Mode mode,
|
||||||
|
|||||||
@@ -100,6 +100,8 @@
|
|||||||
"navProducts": "المنتجات",
|
"navProducts": "المنتجات",
|
||||||
"navRecipes": "الوصفات",
|
"navRecipes": "الوصفات",
|
||||||
"addFromReceiptOrPhoto": "إضافة من الإيصال أو الصورة",
|
"addFromReceiptOrPhoto": "إضافة من الإيصال أو الصورة",
|
||||||
|
"scanScreenTitle": "المسح والتعرف",
|
||||||
|
"barcodeScanSubtitle": "ابحث عن منتج بالباركود",
|
||||||
"chooseMethod": "اختر الطريقة",
|
"chooseMethod": "اختر الطريقة",
|
||||||
"photoReceipt": "تصوير الإيصال",
|
"photoReceipt": "تصوير الإيصال",
|
||||||
"photoReceiptSubtitle": "التعرف على جميع المنتجات من الإيصال",
|
"photoReceiptSubtitle": "التعرف على جميع المنتجات من الإيصال",
|
||||||
|
|||||||
@@ -100,6 +100,8 @@
|
|||||||
"navProducts": "Produkte",
|
"navProducts": "Produkte",
|
||||||
"navRecipes": "Rezepte",
|
"navRecipes": "Rezepte",
|
||||||
"addFromReceiptOrPhoto": "Aus Kassenbon oder Foto hinzufügen",
|
"addFromReceiptOrPhoto": "Aus Kassenbon oder Foto hinzufügen",
|
||||||
|
"scanScreenTitle": "Scannen & Erkennen",
|
||||||
|
"barcodeScanSubtitle": "Produkt per Barcode finden",
|
||||||
"chooseMethod": "Methode wählen",
|
"chooseMethod": "Methode wählen",
|
||||||
"photoReceipt": "Kassenbon fotografieren",
|
"photoReceipt": "Kassenbon fotografieren",
|
||||||
"photoReceiptSubtitle": "Alle Produkte vom Kassenbon erkennen",
|
"photoReceiptSubtitle": "Alle Produkte vom Kassenbon erkennen",
|
||||||
|
|||||||
@@ -100,6 +100,8 @@
|
|||||||
"navProducts": "Products",
|
"navProducts": "Products",
|
||||||
"navRecipes": "Recipes",
|
"navRecipes": "Recipes",
|
||||||
"addFromReceiptOrPhoto": "Add from receipt or photo",
|
"addFromReceiptOrPhoto": "Add from receipt or photo",
|
||||||
|
"scanScreenTitle": "Scan & Recognize",
|
||||||
|
"barcodeScanSubtitle": "Find a product by its barcode",
|
||||||
"chooseMethod": "Choose method",
|
"chooseMethod": "Choose method",
|
||||||
"photoReceipt": "Photo of receipt",
|
"photoReceipt": "Photo of receipt",
|
||||||
"photoReceiptSubtitle": "Recognize all items from a receipt",
|
"photoReceiptSubtitle": "Recognize all items from a receipt",
|
||||||
|
|||||||
@@ -100,6 +100,8 @@
|
|||||||
"navProducts": "Productos",
|
"navProducts": "Productos",
|
||||||
"navRecipes": "Recetas",
|
"navRecipes": "Recetas",
|
||||||
"addFromReceiptOrPhoto": "Añadir desde recibo o foto",
|
"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",
|
"chooseMethod": "Elegir método",
|
||||||
"photoReceipt": "Fotografiar recibo",
|
"photoReceipt": "Fotografiar recibo",
|
||||||
"photoReceiptSubtitle": "Reconocemos todos los productos del recibo",
|
"photoReceiptSubtitle": "Reconocemos todos los productos del recibo",
|
||||||
|
|||||||
@@ -100,6 +100,8 @@
|
|||||||
"navProducts": "Produits",
|
"navProducts": "Produits",
|
||||||
"navRecipes": "Recettes",
|
"navRecipes": "Recettes",
|
||||||
"addFromReceiptOrPhoto": "Ajouter depuis ticket ou photo",
|
"addFromReceiptOrPhoto": "Ajouter depuis ticket ou photo",
|
||||||
|
"scanScreenTitle": "Scanner & Reconnaître",
|
||||||
|
"barcodeScanSubtitle": "Trouver un produit par son code-barres",
|
||||||
"chooseMethod": "Choisir la méthode",
|
"chooseMethod": "Choisir la méthode",
|
||||||
"photoReceipt": "Photographier le ticket",
|
"photoReceipt": "Photographier le ticket",
|
||||||
"photoReceiptSubtitle": "Reconnaissance de tous les produits du ticket",
|
"photoReceiptSubtitle": "Reconnaissance de tous les produits du ticket",
|
||||||
|
|||||||
@@ -100,6 +100,8 @@
|
|||||||
"navProducts": "उत्पाद",
|
"navProducts": "उत्पाद",
|
||||||
"navRecipes": "रेसिपी",
|
"navRecipes": "रेसिपी",
|
||||||
"addFromReceiptOrPhoto": "रसीद या फ़ोटो से जोड़ें",
|
"addFromReceiptOrPhoto": "रसीद या फ़ोटो से जोड़ें",
|
||||||
|
"scanScreenTitle": "स्कैन और पहचानें",
|
||||||
|
"barcodeScanSubtitle": "बारकोड से उत्पाद खोजें",
|
||||||
"chooseMethod": "तरीका चुनें",
|
"chooseMethod": "तरीका चुनें",
|
||||||
"photoReceipt": "रसीद की फ़ोटो",
|
"photoReceipt": "रसीद की फ़ोटो",
|
||||||
"photoReceiptSubtitle": "रसीद से सभी उत्पाद पहचानें",
|
"photoReceiptSubtitle": "रसीद से सभी उत्पाद पहचानें",
|
||||||
|
|||||||
@@ -100,6 +100,8 @@
|
|||||||
"navProducts": "Prodotti",
|
"navProducts": "Prodotti",
|
||||||
"navRecipes": "Ricette",
|
"navRecipes": "Ricette",
|
||||||
"addFromReceiptOrPhoto": "Aggiungi da scontrino o foto",
|
"addFromReceiptOrPhoto": "Aggiungi da scontrino o foto",
|
||||||
|
"scanScreenTitle": "Scansiona & Riconosci",
|
||||||
|
"barcodeScanSubtitle": "Trova un prodotto tramite il codice a barre",
|
||||||
"chooseMethod": "Scegli il metodo",
|
"chooseMethod": "Scegli il metodo",
|
||||||
"photoReceipt": "Fotografa scontrino",
|
"photoReceipt": "Fotografa scontrino",
|
||||||
"photoReceiptSubtitle": "Riconosciamo tutti i prodotti dallo scontrino",
|
"photoReceiptSubtitle": "Riconosciamo tutti i prodotti dallo scontrino",
|
||||||
|
|||||||
@@ -100,6 +100,8 @@
|
|||||||
"navProducts": "食品",
|
"navProducts": "食品",
|
||||||
"navRecipes": "レシピ",
|
"navRecipes": "レシピ",
|
||||||
"addFromReceiptOrPhoto": "レシートや写真から追加",
|
"addFromReceiptOrPhoto": "レシートや写真から追加",
|
||||||
|
"scanScreenTitle": "スキャン&認識",
|
||||||
|
"barcodeScanSubtitle": "バーコードで商品を探す",
|
||||||
"chooseMethod": "方法を選択",
|
"chooseMethod": "方法を選択",
|
||||||
"photoReceipt": "レシートを撮影",
|
"photoReceipt": "レシートを撮影",
|
||||||
"photoReceiptSubtitle": "レシートから全商品を認識",
|
"photoReceiptSubtitle": "レシートから全商品を認識",
|
||||||
|
|||||||
@@ -100,6 +100,8 @@
|
|||||||
"navProducts": "식품",
|
"navProducts": "식품",
|
||||||
"navRecipes": "레시피",
|
"navRecipes": "레시피",
|
||||||
"addFromReceiptOrPhoto": "영수증 또는 사진으로 추가",
|
"addFromReceiptOrPhoto": "영수증 또는 사진으로 추가",
|
||||||
|
"scanScreenTitle": "스캔 및 인식",
|
||||||
|
"barcodeScanSubtitle": "바코드로 제품 찾기",
|
||||||
"chooseMethod": "방법 선택",
|
"chooseMethod": "방법 선택",
|
||||||
"photoReceipt": "영수증 촬영",
|
"photoReceipt": "영수증 촬영",
|
||||||
"photoReceiptSubtitle": "영수증의 모든 상품 인식",
|
"photoReceiptSubtitle": "영수증의 모든 상품 인식",
|
||||||
|
|||||||
@@ -676,6 +676,18 @@ abstract class AppLocalizations {
|
|||||||
/// **'Add from receipt or photo'**
|
/// **'Add from receipt or photo'**
|
||||||
String get addFromReceiptOrPhoto;
|
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.
|
/// No description provided for @chooseMethod.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|||||||
@@ -290,6 +290,12 @@ class AppLocalizationsAr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get addFromReceiptOrPhoto => 'إضافة من الإيصال أو الصورة';
|
String get addFromReceiptOrPhoto => 'إضافة من الإيصال أو الصورة';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanScreenTitle => 'المسح والتعرف';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get barcodeScanSubtitle => 'ابحث عن منتج بالباركود';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chooseMethod => 'اختر الطريقة';
|
String get chooseMethod => 'اختر الطريقة';
|
||||||
|
|
||||||
|
|||||||
@@ -291,6 +291,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get addFromReceiptOrPhoto => 'Aus Kassenbon oder Foto hinzufügen';
|
String get addFromReceiptOrPhoto => 'Aus Kassenbon oder Foto hinzufügen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanScreenTitle => 'Scannen & Erkennen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get barcodeScanSubtitle => 'Produkt per Barcode finden';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chooseMethod => 'Methode wählen';
|
String get chooseMethod => 'Methode wählen';
|
||||||
|
|
||||||
|
|||||||
@@ -290,6 +290,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get addFromReceiptOrPhoto => 'Add from receipt or photo';
|
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
|
@override
|
||||||
String get chooseMethod => 'Choose method';
|
String get chooseMethod => 'Choose method';
|
||||||
|
|
||||||
|
|||||||
@@ -291,6 +291,13 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get addFromReceiptOrPhoto => 'Añadir desde recibo o foto';
|
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
|
@override
|
||||||
String get chooseMethod => 'Elegir método';
|
String get chooseMethod => 'Elegir método';
|
||||||
|
|
||||||
|
|||||||
@@ -291,6 +291,12 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get addFromReceiptOrPhoto => 'Ajouter depuis ticket ou photo';
|
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
|
@override
|
||||||
String get chooseMethod => 'Choisir la méthode';
|
String get chooseMethod => 'Choisir la méthode';
|
||||||
|
|
||||||
|
|||||||
@@ -291,6 +291,12 @@ class AppLocalizationsHi extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get addFromReceiptOrPhoto => 'रसीद या फ़ोटो से जोड़ें';
|
String get addFromReceiptOrPhoto => 'रसीद या फ़ोटो से जोड़ें';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanScreenTitle => 'स्कैन और पहचानें';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get barcodeScanSubtitle => 'बारकोड से उत्पाद खोजें';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chooseMethod => 'तरीका चुनें';
|
String get chooseMethod => 'तरीका चुनें';
|
||||||
|
|
||||||
|
|||||||
@@ -291,6 +291,13 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get addFromReceiptOrPhoto => 'Aggiungi da scontrino o foto';
|
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
|
@override
|
||||||
String get chooseMethod => 'Scegli il metodo';
|
String get chooseMethod => 'Scegli il metodo';
|
||||||
|
|
||||||
|
|||||||
@@ -289,6 +289,12 @@ class AppLocalizationsJa extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get addFromReceiptOrPhoto => 'レシートや写真から追加';
|
String get addFromReceiptOrPhoto => 'レシートや写真から追加';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanScreenTitle => 'スキャン&認識';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get barcodeScanSubtitle => 'バーコードで商品を探す';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chooseMethod => '方法を選択';
|
String get chooseMethod => '方法を選択';
|
||||||
|
|
||||||
|
|||||||
@@ -289,6 +289,12 @@ class AppLocalizationsKo extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get addFromReceiptOrPhoto => '영수증 또는 사진으로 추가';
|
String get addFromReceiptOrPhoto => '영수증 또는 사진으로 추가';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanScreenTitle => '스캔 및 인식';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get barcodeScanSubtitle => '바코드로 제품 찾기';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chooseMethod => '방법 선택';
|
String get chooseMethod => '방법 선택';
|
||||||
|
|
||||||
|
|||||||
@@ -291,6 +291,12 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get addFromReceiptOrPhoto => 'Adicionar de recibo ou foto';
|
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
|
@override
|
||||||
String get chooseMethod => 'Escolher método';
|
String get chooseMethod => 'Escolher método';
|
||||||
|
|
||||||
|
|||||||
@@ -290,6 +290,12 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get addFromReceiptOrPhoto => 'Добавить из чека или фото';
|
String get addFromReceiptOrPhoto => 'Добавить из чека или фото';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanScreenTitle => 'Сканировать';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get barcodeScanSubtitle => 'Найти продукт по штрихкоду';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chooseMethod => 'Выберите способ';
|
String get chooseMethod => 'Выберите способ';
|
||||||
|
|
||||||
|
|||||||
@@ -289,6 +289,12 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get addFromReceiptOrPhoto => '从收据或照片添加';
|
String get addFromReceiptOrPhoto => '从收据或照片添加';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanScreenTitle => '扫描与识别';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get barcodeScanSubtitle => '通过条形码查找产品';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chooseMethod => '选择方式';
|
String get chooseMethod => '选择方式';
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,8 @@
|
|||||||
"navProducts": "Produtos",
|
"navProducts": "Produtos",
|
||||||
"navRecipes": "Receitas",
|
"navRecipes": "Receitas",
|
||||||
"addFromReceiptOrPhoto": "Adicionar de recibo ou foto",
|
"addFromReceiptOrPhoto": "Adicionar de recibo ou foto",
|
||||||
|
"scanScreenTitle": "Escanear & Reconhecer",
|
||||||
|
"barcodeScanSubtitle": "Encontrar produto pelo código de barras",
|
||||||
"chooseMethod": "Escolher método",
|
"chooseMethod": "Escolher método",
|
||||||
"photoReceipt": "Fotografar recibo",
|
"photoReceipt": "Fotografar recibo",
|
||||||
"photoReceiptSubtitle": "Reconhecemos todos os produtos do recibo",
|
"photoReceiptSubtitle": "Reconhecemos todos os produtos do recibo",
|
||||||
|
|||||||
@@ -100,6 +100,8 @@
|
|||||||
"navProducts": "Продукты",
|
"navProducts": "Продукты",
|
||||||
"navRecipes": "Рецепты",
|
"navRecipes": "Рецепты",
|
||||||
"addFromReceiptOrPhoto": "Добавить из чека или фото",
|
"addFromReceiptOrPhoto": "Добавить из чека или фото",
|
||||||
|
"scanScreenTitle": "Сканировать",
|
||||||
|
"barcodeScanSubtitle": "Найти продукт по штрихкоду",
|
||||||
"chooseMethod": "Выберите способ",
|
"chooseMethod": "Выберите способ",
|
||||||
"photoReceipt": "Сфотографировать чек",
|
"photoReceipt": "Сфотографировать чек",
|
||||||
"photoReceiptSubtitle": "Распознаем все продукты из чека",
|
"photoReceiptSubtitle": "Распознаем все продукты из чека",
|
||||||
|
|||||||
@@ -100,6 +100,8 @@
|
|||||||
"navProducts": "食品",
|
"navProducts": "食品",
|
||||||
"navRecipes": "食谱",
|
"navRecipes": "食谱",
|
||||||
"addFromReceiptOrPhoto": "从收据或照片添加",
|
"addFromReceiptOrPhoto": "从收据或照片添加",
|
||||||
|
"scanScreenTitle": "扫描与识别",
|
||||||
|
"barcodeScanSubtitle": "通过条形码查找产品",
|
||||||
"chooseMethod": "选择方式",
|
"chooseMethod": "选择方式",
|
||||||
"photoReceipt": "拍摄收据",
|
"photoReceipt": "拍摄收据",
|
||||||
"photoReceiptSubtitle": "识别收据中的所有商品",
|
"photoReceiptSubtitle": "识别收据中的所有商品",
|
||||||
|
|||||||
Reference in New Issue
Block a user