feat: improved receipt recognition, batch product add, and scan UX
- Rewrite receipt OCR prompt: completes truncated names, preserves fat% and flavour attributes, extracts weight/volume from line, infers typical package sizes for solid goods with quantity_confidence field - Add quantity_confidence to RecognizedItem, EnrichedItem, and ProductJobResultItem; propagate through item enricher and worker - Replace per-item create loop with single POST /user-products/batch call from RecognitionConfirmScreen - Rebuild RecognitionConfirmScreen: amber qty border for low quantity_confidence, tappable product name → catalog picker, sort items by confidence, full L10n (no hardcoded strings) - Add timestamps (HH:mm / d MMM HH:mm) to recent scan chips - Show close-app hint on ProductJobWatchScreen (queued + processing) - Refresh recentProductJobsProvider on watch screen init so new job appears without a manual pull-to-refresh - App-level WidgetsBindingObserver refreshes product and dish job lists on resume, fixing stale lists after background/foreground transitions - Add 9 new L10n keys across all 12 locales Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,12 +5,39 @@ import 'package:food_ai/l10n/app_localizations.dart';
|
||||
import 'core/locale/language_provider.dart';
|
||||
import 'core/router/app_router.dart';
|
||||
import 'core/theme/app_theme.dart';
|
||||
import 'features/home/home_provider.dart';
|
||||
import 'features/products/product_job_provider.dart';
|
||||
|
||||
class App extends ConsumerWidget {
|
||||
class App extends ConsumerStatefulWidget {
|
||||
const App({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ConsumerState<App> createState() => _AppState();
|
||||
}
|
||||
|
||||
class _AppState extends ConsumerState<App> with WidgetsBindingObserver {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState lifecycleState) {
|
||||
if (lifecycleState == AppLifecycleState.resumed) {
|
||||
ref.read(recentProductJobsProvider.notifier).refresh();
|
||||
ref.read(todayJobsProvider.notifier).load();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final router = ref.watch(routerProvider);
|
||||
final languageCode = ref.watch(languageProvider);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../core/locale/unit_provider.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
@@ -120,7 +121,7 @@ class _RecentScansSection extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 72,
|
||||
height: 84,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
@@ -187,6 +188,12 @@ class _ScanJobChip extends ConsumerWidget {
|
||||
isFailed: isFailed,
|
||||
isActive: isActive,
|
||||
),
|
||||
Text(
|
||||
_formatChipDate(job.createdAt),
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -197,6 +204,17 @@ class _ScanJobChip extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
String _formatChipDate(DateTime dateTime) {
|
||||
final local = dateTime.toLocal();
|
||||
final now = DateTime.now();
|
||||
final isToday = local.year == now.year &&
|
||||
local.month == now.month &&
|
||||
local.day == now.day;
|
||||
return isToday
|
||||
? DateFormat.Hm().format(local)
|
||||
: DateFormat('d MMM HH:mm').format(local);
|
||||
}
|
||||
|
||||
class _StatusBadge extends StatelessWidget {
|
||||
const _StatusBadge({
|
||||
required this.status,
|
||||
|
||||
@@ -72,6 +72,17 @@ class UserProductsNotifier
|
||||
});
|
||||
}
|
||||
|
||||
/// Adds multiple products in a single request and appends them to the list.
|
||||
Future<void> batchCreate(List<Map<String, dynamic>> payloads) async {
|
||||
final created = await _service.batchCreateProducts(payloads);
|
||||
state.whenData((products) {
|
||||
final updated = [...products, ...created]
|
||||
..sort((firstProduct, secondProduct) =>
|
||||
firstProduct.expiresAt.compareTo(secondProduct.expiresAt));
|
||||
state = AsyncValue.data(updated);
|
||||
});
|
||||
}
|
||||
|
||||
/// Updates a product in-place, keeping list sort order.
|
||||
Future<void> update(
|
||||
String id, {
|
||||
|
||||
@@ -61,6 +61,14 @@ class UserProductService {
|
||||
return UserProduct.fromJson(data);
|
||||
}
|
||||
|
||||
Future<List<UserProduct>> batchCreateProducts(
|
||||
List<Map<String, dynamic>> payloads) async {
|
||||
final list = await _client.postList('/user-products/batch', data: payloads);
|
||||
return list
|
||||
.map((element) => UserProduct.fromJson(element as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<void> deleteProduct(String id) =>
|
||||
_client.deleteVoid('/user-products/$id');
|
||||
|
||||
|
||||
@@ -23,6 +23,15 @@ class _ProductJobWatchScreenState
|
||||
String? _errorMessage;
|
||||
bool _navigated = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Refresh so the new job appears in recent scans immediately.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(recentProductJobsProvider.notifier).refresh();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
@@ -112,6 +121,14 @@ class _QueuedBody extends StatelessWidget {
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
l10n.scanJobCloseHint,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -126,14 +143,26 @@ class _ProcessingBody extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CircularProgressIndicator(),
|
||||
const SizedBox(height: 24),
|
||||
Text(label, style: Theme.of(context).textTheme.titleMedium),
|
||||
],
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CircularProgressIndicator(),
|
||||
const SizedBox(height: 24),
|
||||
Text(label, style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
l10n.scanJobCloseHint,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ 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 '../products/user_product_provider.dart';
|
||||
import 'recognition_service.dart';
|
||||
|
||||
@@ -34,20 +36,25 @@ class _RecognitionConfirmScreenState
|
||||
primaryProductId: item.primaryProductId,
|
||||
storageDays: item.storageDays,
|
||||
confidence: item.confidence,
|
||||
quantityConfidence: item.quantityConfidence,
|
||||
))
|
||||
.toList();
|
||||
// Sort: low-confidence items first so the user notices them.
|
||||
_items.sort((firstItem, secondItem) =>
|
||||
firstItem.confidence.compareTo(secondItem.confidence));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Найдено ${_items.length} продуктов'),
|
||||
title: Text(l10n.recognitionFoundProducts(_items.length)),
|
||||
actions: [
|
||||
if (_items.isNotEmpty)
|
||||
TextButton(
|
||||
onPressed: _saving ? null : _addAll,
|
||||
child: const Text('Добавить всё'),
|
||||
child: Text(l10n.recognitionAddAll),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -56,10 +63,10 @@ class _RecognitionConfirmScreenState
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.only(bottom: 80),
|
||||
itemCount: _items.length,
|
||||
itemBuilder: (_, i) => _ItemTile(
|
||||
item: _items[i],
|
||||
itemBuilder: (_, index) => _ItemTile(
|
||||
item: _items[index],
|
||||
units: ref.watch(unitsProvider).valueOrNull ?? {},
|
||||
onDelete: () => setState(() => _items.removeAt(i)),
|
||||
onDelete: () => setState(() => _items.removeAt(index)),
|
||||
onChanged: () => setState(() {}),
|
||||
),
|
||||
),
|
||||
@@ -74,29 +81,31 @@ class _RecognitionConfirmScreenState
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.add_shopping_cart),
|
||||
label: const Text('В запасы'),
|
||||
label: Text(l10n.recognitionAddToStock),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _addAll() async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
setState(() => _saving = true);
|
||||
try {
|
||||
for (final item in _items) {
|
||||
await ref.read(userProductsProvider.notifier).create(
|
||||
name: item.name,
|
||||
quantity: item.quantity,
|
||||
unit: item.unit,
|
||||
category: item.category,
|
||||
storageDays: item.storageDays,
|
||||
primaryProductId: item.primaryProductId,
|
||||
);
|
||||
}
|
||||
final payloads = _items
|
||||
.map((item) => <String, dynamic>{
|
||||
'name': item.name,
|
||||
'quantity': item.quantity,
|
||||
'unit': item.unit,
|
||||
'category': item.category,
|
||||
'storage_days': item.storageDays,
|
||||
if (item.primaryProductId != null)
|
||||
'primary_product_id': item.primaryProductId,
|
||||
})
|
||||
.toList();
|
||||
await ref.read(userProductsProvider.notifier).batchCreate(payloads);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Добавлено ${_items.length} продуктов'),
|
||||
),
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.recognitionAdded(_items.length))),
|
||||
);
|
||||
// Pop back to products screen.
|
||||
int count = 0;
|
||||
@@ -104,8 +113,8 @@ class _RecognitionConfirmScreenState
|
||||
}
|
||||
} catch (_) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Не удалось добавить продукты')),
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.recognitionProductsFailed)),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
@@ -123,9 +132,10 @@ class _EditableItem {
|
||||
double quantity;
|
||||
String unit;
|
||||
final String category;
|
||||
final String? primaryProductId;
|
||||
String? primaryProductId;
|
||||
final int storageDays;
|
||||
final double confidence;
|
||||
final double quantityConfidence;
|
||||
|
||||
_EditableItem({
|
||||
required this.name,
|
||||
@@ -135,6 +145,7 @@ class _EditableItem {
|
||||
this.primaryProductId,
|
||||
required this.storageDays,
|
||||
required this.confidence,
|
||||
required this.quantityConfidence,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -142,7 +153,7 @@ class _EditableItem {
|
||||
// Item tile with inline editing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class _ItemTile extends StatefulWidget {
|
||||
class _ItemTile extends ConsumerStatefulWidget {
|
||||
const _ItemTile({
|
||||
required this.item,
|
||||
required this.units,
|
||||
@@ -156,10 +167,10 @@ class _ItemTile extends StatefulWidget {
|
||||
final VoidCallback onChanged;
|
||||
|
||||
@override
|
||||
State<_ItemTile> createState() => _ItemTileState();
|
||||
ConsumerState<_ItemTile> createState() => _ItemTileState();
|
||||
}
|
||||
|
||||
class _ItemTileState extends State<_ItemTile> {
|
||||
class _ItemTileState extends ConsumerState<_ItemTile> {
|
||||
late final _qtyController =
|
||||
TextEditingController(text: _formatQty(widget.item.quantity));
|
||||
|
||||
@@ -169,18 +180,43 @@ class _ItemTileState extends State<_ItemTile> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String _formatQty(double v) =>
|
||||
v == v.roundToDouble() ? v.toInt().toString() : v.toStringAsFixed(1);
|
||||
String _formatQty(double value) =>
|
||||
value == value.roundToDouble()
|
||||
? value.toInt().toString()
|
||||
: value.toStringAsFixed(1);
|
||||
|
||||
void _openProductPicker() async {
|
||||
final picked = await showModalBottomSheet<CatalogProduct>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (_) => _ProductPickerSheet(currentName: widget.item.name),
|
||||
);
|
||||
if (picked != null && mounted) {
|
||||
setState(() {
|
||||
widget.item.name = picked.displayName;
|
||||
widget.item.primaryProductId = picked.id;
|
||||
if (picked.defaultUnit != null) {
|
||||
widget.item.unit = picked.defaultUnit!;
|
||||
}
|
||||
});
|
||||
widget.onChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final conf = widget.item.confidence;
|
||||
final confColor = conf >= 0.8
|
||||
? Colors.green
|
||||
: conf >= 0.5
|
||||
? Colors.orange
|
||||
: Colors.red;
|
||||
final qtyUncertain = widget.item.quantityConfidence < 0.7;
|
||||
final tileColor = conf < 0.7
|
||||
? theme.colorScheme.errorContainer.withValues(alpha: 0.15)
|
||||
: null;
|
||||
|
||||
return Dismissible(
|
||||
key: ValueKey(widget.item.name),
|
||||
@@ -192,7 +228,8 @@ class _ItemTileState extends State<_ItemTile> {
|
||||
child: Icon(Icons.delete_outline, color: theme.colorScheme.onError),
|
||||
),
|
||||
onDismissed: (_) => widget.onDelete(),
|
||||
child: Padding(
|
||||
child: Container(
|
||||
color: tileColor,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -200,8 +237,22 @@ class _ItemTileState extends State<_ItemTile> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(widget.item.name,
|
||||
style: theme.textTheme.bodyLarge),
|
||||
GestureDetector(
|
||||
onTap: _openProductPicker,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(widget.item.name,
|
||||
style: theme.textTheme.bodyLarge),
|
||||
),
|
||||
Icon(
|
||||
Icons.edit_outlined,
|
||||
size: 16,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
@@ -214,7 +265,7 @@ class _ItemTileState extends State<_ItemTile> {
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${(conf * 100).toInt()}% уверенность',
|
||||
l10n.recognitionConfidence((conf * 100).toInt()),
|
||||
style: theme.textTheme.labelSmall
|
||||
?.copyWith(color: confColor),
|
||||
),
|
||||
@@ -231,13 +282,18 @@ class _ItemTileState extends State<_ItemTile> {
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(decimal: true),
|
||||
textAlign: TextAlign.center,
|
||||
decoration: const InputDecoration(
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 8),
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 8),
|
||||
border: const OutlineInputBorder(),
|
||||
enabledBorder: qtyUncertain
|
||||
? const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.orange, width: 2),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
onChanged: (v) {
|
||||
final parsed = double.tryParse(v);
|
||||
onChanged: (value) {
|
||||
final parsed = double.tryParse(value);
|
||||
if (parsed != null) {
|
||||
widget.item.quantity = parsed;
|
||||
widget.onChanged();
|
||||
@@ -249,24 +305,23 @@ class _ItemTileState extends State<_ItemTile> {
|
||||
widget.units.isEmpty
|
||||
? const SizedBox(width: 48)
|
||||
: Builder(builder: (builderContext) {
|
||||
// Reconcile item.unit with valid server codes so that the
|
||||
// submitted value matches what the dropdown displays.
|
||||
if (!widget.units.containsKey(widget.item.unit)) {
|
||||
widget.item.unit = widget.units.keys.first;
|
||||
}
|
||||
return DropdownButton<String>(
|
||||
value: widget.item.unit,
|
||||
underline: const SizedBox(),
|
||||
items: widget.units.entries
|
||||
.map((e) => DropdownMenuItem(value: e.key, child: Text(e.value)))
|
||||
.toList(),
|
||||
onChanged: (v) {
|
||||
if (v != null) {
|
||||
setState(() => widget.item.unit = v);
|
||||
widget.onChanged();
|
||||
}
|
||||
},
|
||||
);
|
||||
value: widget.item.unit,
|
||||
underline: const SizedBox(),
|
||||
items: widget.units.entries
|
||||
.map((entry) => DropdownMenuItem(
|
||||
value: entry.key, child: Text(entry.value)))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() => widget.item.unit = value);
|
||||
widget.onChanged();
|
||||
}
|
||||
},
|
||||
);
|
||||
}),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
@@ -279,6 +334,116 @@ class _ItemTileState extends State<_ItemTile> {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Product picker bottom sheet
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class _ProductPickerSheet extends ConsumerStatefulWidget {
|
||||
const _ProductPickerSheet({required this.currentName});
|
||||
|
||||
final String currentName;
|
||||
|
||||
@override
|
||||
ConsumerState<_ProductPickerSheet> createState() =>
|
||||
_ProductPickerSheetState();
|
||||
}
|
||||
|
||||
class _ProductPickerSheetState extends ConsumerState<_ProductPickerSheet> {
|
||||
late final _searchController =
|
||||
TextEditingController(text: widget.currentName);
|
||||
List<CatalogProduct> _results = [];
|
||||
bool _loading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_search(widget.currentName);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _search(String query) async {
|
||||
if (query.isEmpty) {
|
||||
setState(() => _results = []);
|
||||
return;
|
||||
}
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
final service = ref.read(userProductServiceProvider);
|
||||
final searchResults = await service.searchProducts(query);
|
||||
if (mounted) setState(() => _results = searchResults);
|
||||
} catch (_) {
|
||||
// Silently ignore search errors.
|
||||
} finally {
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
l10n.recognitionReplaceProduct,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.searchProductsHint,
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
onChanged: _search,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (_loading)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
else
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 300),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: _results.length,
|
||||
itemBuilder: (_, index) {
|
||||
final product = _results[index];
|
||||
return ListTile(
|
||||
title: Text(product.displayName),
|
||||
onTap: () => Navigator.pop(context, product),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Empty state
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -290,15 +455,16 @@ class _EmptyState extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.search_off, size: 64),
|
||||
const SizedBox(height: 12),
|
||||
const Text('Продукты не найдены'),
|
||||
Text(l10n.recognitionEmpty),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton(onPressed: onBack, child: const Text('Назад')),
|
||||
FilledButton(onPressed: onBack, child: Text(l10n.cancel)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -21,6 +21,7 @@ class RecognizedItem {
|
||||
String unit;
|
||||
final String category;
|
||||
final double confidence;
|
||||
final double quantityConfidence;
|
||||
final String? primaryProductId;
|
||||
final int storageDays;
|
||||
|
||||
@@ -30,6 +31,7 @@ class RecognizedItem {
|
||||
required this.unit,
|
||||
required this.category,
|
||||
required this.confidence,
|
||||
this.quantityConfidence = 1.0,
|
||||
this.primaryProductId,
|
||||
required this.storageDays,
|
||||
});
|
||||
@@ -41,6 +43,7 @@ class RecognizedItem {
|
||||
unit: json['unit'] as String? ?? 'pcs',
|
||||
category: json['category'] as String? ?? 'other',
|
||||
confidence: (json['confidence'] as num?)?.toDouble() ?? 0.0,
|
||||
quantityConfidence: (json['quantity_confidence'] as num?)?.toDouble() ?? 1.0,
|
||||
primaryProductId: json['mapping_id'] as String?,
|
||||
storageDays: json['storage_days'] as int? ?? 7,
|
||||
);
|
||||
|
||||
@@ -201,5 +201,35 @@
|
||||
"fat": "دهون",
|
||||
"carbs": "كربوهيدرات",
|
||||
"fiber": "ألياف",
|
||||
"productAddedToShelf": "تمت الإضافة إلى المخزن"
|
||||
"productAddedToShelf": "تمت الإضافة إلى المخزن",
|
||||
"recognitionFoundProducts": "تم العثور على {count} منتجات",
|
||||
"@recognitionFoundProducts": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionAddAll": "إضافة الكل",
|
||||
"recognitionAddToStock": "إضافة إلى المخزن",
|
||||
"recognitionAdded": "تمت إضافة {count} منتجات",
|
||||
"@recognitionAdded": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionProductsFailed": "فشل إضافة المنتجات",
|
||||
"recognitionEmpty": "لم يتم العثور على منتجات",
|
||||
"recognitionConfidence": "{percent}% ثقة",
|
||||
"@recognitionConfidence": {
|
||||
"placeholders": {
|
||||
"percent": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionReplaceProduct": "استبدال المنتج",
|
||||
"scanJobCloseHint": "يمكنك إغلاق التطبيق — سيظهر هذا المسح في عمليات المسح الأخيرة في شاشة المنتجات"
|
||||
}
|
||||
|
||||
@@ -201,5 +201,35 @@
|
||||
"fat": "Fett",
|
||||
"carbs": "Kohlenhydrate",
|
||||
"fiber": "Ballaststoffe",
|
||||
"productAddedToShelf": "Zum Vorrat hinzugefügt"
|
||||
"productAddedToShelf": "Zum Vorrat hinzugefügt",
|
||||
"recognitionFoundProducts": "{count} Produkte gefunden",
|
||||
"@recognitionFoundProducts": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionAddAll": "Alle hinzufügen",
|
||||
"recognitionAddToStock": "In Vorrat",
|
||||
"recognitionAdded": "{count} Produkte hinzugefügt",
|
||||
"@recognitionAdded": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionProductsFailed": "Hinzufügen fehlgeschlagen",
|
||||
"recognitionEmpty": "Keine Produkte gefunden",
|
||||
"recognitionConfidence": "{percent}% Sicherheit",
|
||||
"@recognitionConfidence": {
|
||||
"placeholders": {
|
||||
"percent": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionReplaceProduct": "Produkt ersetzen",
|
||||
"scanJobCloseHint": "Du kannst die App schließen — dieser Scan erscheint in Letzte Scans auf dem Produktbildschirm"
|
||||
}
|
||||
|
||||
@@ -201,5 +201,35 @@
|
||||
"fat": "Fat",
|
||||
"carbs": "Carbohydrates",
|
||||
"fiber": "Fiber",
|
||||
"productAddedToShelf": "Added to pantry"
|
||||
"productAddedToShelf": "Added to pantry",
|
||||
"recognitionFoundProducts": "Found {count} products",
|
||||
"@recognitionFoundProducts": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionAddAll": "Add all",
|
||||
"recognitionAddToStock": "Add to pantry",
|
||||
"recognitionAdded": "Added {count} products",
|
||||
"@recognitionAdded": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionProductsFailed": "Failed to add products",
|
||||
"recognitionEmpty": "No products found",
|
||||
"recognitionConfidence": "{percent}% confidence",
|
||||
"@recognitionConfidence": {
|
||||
"placeholders": {
|
||||
"percent": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionReplaceProduct": "Replace product",
|
||||
"scanJobCloseHint": "You can close the app — this scan will appear in Recent Scans on the Products screen"
|
||||
}
|
||||
|
||||
@@ -201,5 +201,35 @@
|
||||
"fat": "Grasas",
|
||||
"carbs": "Carbohidratos",
|
||||
"fiber": "Fibra",
|
||||
"productAddedToShelf": "Agregado a la despensa"
|
||||
"productAddedToShelf": "Agregado a la despensa",
|
||||
"recognitionFoundProducts": "Se encontraron {count} productos",
|
||||
"@recognitionFoundProducts": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionAddAll": "Agregar todo",
|
||||
"recognitionAddToStock": "Agregar al almacén",
|
||||
"recognitionAdded": "Se agregaron {count} productos",
|
||||
"@recognitionAdded": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionProductsFailed": "Error al agregar productos",
|
||||
"recognitionEmpty": "No se encontraron productos",
|
||||
"recognitionConfidence": "{percent}% de confianza",
|
||||
"@recognitionConfidence": {
|
||||
"placeholders": {
|
||||
"percent": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionReplaceProduct": "Reemplazar producto",
|
||||
"scanJobCloseHint": "Puedes cerrar la app — este escaneo aparecerá en Escaneos recientes en la pantalla de Productos"
|
||||
}
|
||||
|
||||
@@ -201,5 +201,35 @@
|
||||
"fat": "Graisses",
|
||||
"carbs": "Glucides",
|
||||
"fiber": "Fibres",
|
||||
"productAddedToShelf": "Ajouté au garde-manger"
|
||||
"productAddedToShelf": "Ajouté au garde-manger",
|
||||
"recognitionFoundProducts": "{count} produits trouvés",
|
||||
"@recognitionFoundProducts": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionAddAll": "Tout ajouter",
|
||||
"recognitionAddToStock": "Ajouter au garde-manger",
|
||||
"recognitionAdded": "{count} produits ajoutés",
|
||||
"@recognitionAdded": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionProductsFailed": "Échec de l'ajout des produits",
|
||||
"recognitionEmpty": "Aucun produit trouvé",
|
||||
"recognitionConfidence": "{percent}% de confiance",
|
||||
"@recognitionConfidence": {
|
||||
"placeholders": {
|
||||
"percent": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionReplaceProduct": "Remplacer le produit",
|
||||
"scanJobCloseHint": "Vous pouvez fermer l'app — ce scan apparaîtra dans Scans récents sur l'écran Produits"
|
||||
}
|
||||
|
||||
@@ -201,5 +201,35 @@
|
||||
"fat": "वसा",
|
||||
"carbs": "कार्बोहाइड्रेट",
|
||||
"fiber": "फाइबर",
|
||||
"productAddedToShelf": "पेंट्री में जोड़ा गया"
|
||||
"productAddedToShelf": "पेंट्री में जोड़ा गया",
|
||||
"recognitionFoundProducts": "{count} उत्पाद मिले",
|
||||
"@recognitionFoundProducts": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionAddAll": "सब जोड़ें",
|
||||
"recognitionAddToStock": "पेंट्री में जोड़ें",
|
||||
"recognitionAdded": "{count} उत्पाद जोड़े गए",
|
||||
"@recognitionAdded": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionProductsFailed": "उत्पाद जोड़ने में विफल",
|
||||
"recognitionEmpty": "कोई उत्पाद नहीं मिला",
|
||||
"recognitionConfidence": "{percent}% विश्वसनीयता",
|
||||
"@recognitionConfidence": {
|
||||
"placeholders": {
|
||||
"percent": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionReplaceProduct": "उत्पाद बदलें",
|
||||
"scanJobCloseHint": "आप ऐप बंद कर सकते हैं — यह स्कैन उत्पाद स्क्रीन पर हाल के स्कैन में दिखाई देगा"
|
||||
}
|
||||
|
||||
@@ -201,5 +201,35 @@
|
||||
"fat": "Grassi",
|
||||
"carbs": "Carboidrati",
|
||||
"fiber": "Fibre",
|
||||
"productAddedToShelf": "Aggiunto alla dispensa"
|
||||
"productAddedToShelf": "Aggiunto alla dispensa",
|
||||
"recognitionFoundProducts": "Trovati {count} prodotti",
|
||||
"@recognitionFoundProducts": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionAddAll": "Aggiungi tutto",
|
||||
"recognitionAddToStock": "Aggiungi alla dispensa",
|
||||
"recognitionAdded": "Aggiunti {count} prodotti",
|
||||
"@recognitionAdded": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionProductsFailed": "Impossibile aggiungere i prodotti",
|
||||
"recognitionEmpty": "Nessun prodotto trovato",
|
||||
"recognitionConfidence": "{percent}% di certezza",
|
||||
"@recognitionConfidence": {
|
||||
"placeholders": {
|
||||
"percent": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionReplaceProduct": "Sostituisci prodotto",
|
||||
"scanJobCloseHint": "Puoi chiudere l'app — questa scansione apparirà in Scansioni recenti nella schermata Prodotti"
|
||||
}
|
||||
|
||||
@@ -201,5 +201,35 @@
|
||||
"fat": "脂質",
|
||||
"carbs": "炭水化物",
|
||||
"fiber": "食物繊維",
|
||||
"productAddedToShelf": "パントリーに追加しました"
|
||||
"productAddedToShelf": "パントリーに追加しました",
|
||||
"recognitionFoundProducts": "{count}個の商品が見つかりました",
|
||||
"@recognitionFoundProducts": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionAddAll": "すべて追加",
|
||||
"recognitionAddToStock": "パントリーに追加",
|
||||
"recognitionAdded": "{count}個の商品を追加しました",
|
||||
"@recognitionAdded": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionProductsFailed": "商品の追加に失敗しました",
|
||||
"recognitionEmpty": "商品が見つかりません",
|
||||
"recognitionConfidence": "{percent}%の信頼度",
|
||||
"@recognitionConfidence": {
|
||||
"placeholders": {
|
||||
"percent": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionReplaceProduct": "商品を置き換える",
|
||||
"scanJobCloseHint": "アプリを閉じても大丈夫です — このスキャンは製品画面の最近のスキャンに表示されます"
|
||||
}
|
||||
|
||||
@@ -201,5 +201,35 @@
|
||||
"fat": "지방",
|
||||
"carbs": "탄수화물",
|
||||
"fiber": "식이섬유",
|
||||
"productAddedToShelf": "저장실에 추가되었습니다"
|
||||
"productAddedToShelf": "저장실에 추가되었습니다",
|
||||
"recognitionFoundProducts": "{count}개 제품 찾음",
|
||||
"@recognitionFoundProducts": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionAddAll": "모두 추가",
|
||||
"recognitionAddToStock": "저장실에 추가",
|
||||
"recognitionAdded": "{count}개 제품 추가됨",
|
||||
"@recognitionAdded": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionProductsFailed": "제품 추가 실패",
|
||||
"recognitionEmpty": "제품을 찾을 수 없습니다",
|
||||
"recognitionConfidence": "{percent}% 신뢰도",
|
||||
"@recognitionConfidence": {
|
||||
"placeholders": {
|
||||
"percent": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionReplaceProduct": "제품 교체",
|
||||
"scanJobCloseHint": "앱을 닫아도 됩니다 — 이 스캔은 제품 화면의 최근 스캔에 표시됩니다"
|
||||
}
|
||||
|
||||
@@ -1161,6 +1161,60 @@ abstract class AppLocalizations {
|
||||
/// In en, this message translates to:
|
||||
/// **'Added to pantry'**
|
||||
String get productAddedToShelf;
|
||||
|
||||
/// No description provided for @recognitionFoundProducts.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Found {count} products'**
|
||||
String recognitionFoundProducts(int count);
|
||||
|
||||
/// No description provided for @recognitionAddAll.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add all'**
|
||||
String get recognitionAddAll;
|
||||
|
||||
/// No description provided for @recognitionAddToStock.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add to pantry'**
|
||||
String get recognitionAddToStock;
|
||||
|
||||
/// No description provided for @recognitionAdded.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Added {count} products'**
|
||||
String recognitionAdded(int count);
|
||||
|
||||
/// No description provided for @recognitionProductsFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Failed to add products'**
|
||||
String get recognitionProductsFailed;
|
||||
|
||||
/// No description provided for @recognitionEmpty.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No products found'**
|
||||
String get recognitionEmpty;
|
||||
|
||||
/// No description provided for @recognitionConfidence.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{percent}% confidence'**
|
||||
String recognitionConfidence(int percent);
|
||||
|
||||
/// No description provided for @recognitionReplaceProduct.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Replace product'**
|
||||
String get recognitionReplaceProduct;
|
||||
|
||||
/// No description provided for @scanJobCloseHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'You can close the app — this scan will appear in Recent Scans on the Products screen'**
|
||||
String get scanJobCloseHint;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@@ -541,4 +541,38 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get productAddedToShelf => 'تمت الإضافة إلى المخزن';
|
||||
|
||||
@override
|
||||
String recognitionFoundProducts(int count) {
|
||||
return 'تم العثور على $count منتجات';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionAddAll => 'إضافة الكل';
|
||||
|
||||
@override
|
||||
String get recognitionAddToStock => 'إضافة إلى المخزن';
|
||||
|
||||
@override
|
||||
String recognitionAdded(int count) {
|
||||
return 'تمت إضافة $count منتجات';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionProductsFailed => 'فشل إضافة المنتجات';
|
||||
|
||||
@override
|
||||
String get recognitionEmpty => 'لم يتم العثور على منتجات';
|
||||
|
||||
@override
|
||||
String recognitionConfidence(int percent) {
|
||||
return '$percent% ثقة';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionReplaceProduct => 'استبدال المنتج';
|
||||
|
||||
@override
|
||||
String get scanJobCloseHint =>
|
||||
'يمكنك إغلاق التطبيق — سيظهر هذا المسح في عمليات المسح الأخيرة في شاشة المنتجات';
|
||||
}
|
||||
|
||||
@@ -545,4 +545,38 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get productAddedToShelf => 'Zum Vorrat hinzugefügt';
|
||||
|
||||
@override
|
||||
String recognitionFoundProducts(int count) {
|
||||
return '$count Produkte gefunden';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionAddAll => 'Alle hinzufügen';
|
||||
|
||||
@override
|
||||
String get recognitionAddToStock => 'In Vorrat';
|
||||
|
||||
@override
|
||||
String recognitionAdded(int count) {
|
||||
return '$count Produkte hinzugefügt';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionProductsFailed => 'Hinzufügen fehlgeschlagen';
|
||||
|
||||
@override
|
||||
String get recognitionEmpty => 'Keine Produkte gefunden';
|
||||
|
||||
@override
|
||||
String recognitionConfidence(int percent) {
|
||||
return '$percent% Sicherheit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionReplaceProduct => 'Produkt ersetzen';
|
||||
|
||||
@override
|
||||
String get scanJobCloseHint =>
|
||||
'Du kannst die App schließen — dieser Scan erscheint in Letzte Scans auf dem Produktbildschirm';
|
||||
}
|
||||
|
||||
@@ -543,4 +543,38 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get productAddedToShelf => 'Added to pantry';
|
||||
|
||||
@override
|
||||
String recognitionFoundProducts(int count) {
|
||||
return 'Found $count products';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionAddAll => 'Add all';
|
||||
|
||||
@override
|
||||
String get recognitionAddToStock => 'Add to pantry';
|
||||
|
||||
@override
|
||||
String recognitionAdded(int count) {
|
||||
return 'Added $count products';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionProductsFailed => 'Failed to add products';
|
||||
|
||||
@override
|
||||
String get recognitionEmpty => 'No products found';
|
||||
|
||||
@override
|
||||
String recognitionConfidence(int percent) {
|
||||
return '$percent% confidence';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionReplaceProduct => 'Replace product';
|
||||
|
||||
@override
|
||||
String get scanJobCloseHint =>
|
||||
'You can close the app — this scan will appear in Recent Scans on the Products screen';
|
||||
}
|
||||
|
||||
@@ -546,4 +546,38 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get productAddedToShelf => 'Agregado a la despensa';
|
||||
|
||||
@override
|
||||
String recognitionFoundProducts(int count) {
|
||||
return 'Se encontraron $count productos';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionAddAll => 'Agregar todo';
|
||||
|
||||
@override
|
||||
String get recognitionAddToStock => 'Agregar al almacén';
|
||||
|
||||
@override
|
||||
String recognitionAdded(int count) {
|
||||
return 'Se agregaron $count productos';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionProductsFailed => 'Error al agregar productos';
|
||||
|
||||
@override
|
||||
String get recognitionEmpty => 'No se encontraron productos';
|
||||
|
||||
@override
|
||||
String recognitionConfidence(int percent) {
|
||||
return '$percent% de confianza';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionReplaceProduct => 'Reemplazar producto';
|
||||
|
||||
@override
|
||||
String get scanJobCloseHint =>
|
||||
'Puedes cerrar la app — este escaneo aparecerá en Escaneos recientes en la pantalla de Productos';
|
||||
}
|
||||
|
||||
@@ -546,4 +546,38 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get productAddedToShelf => 'Ajouté au garde-manger';
|
||||
|
||||
@override
|
||||
String recognitionFoundProducts(int count) {
|
||||
return '$count produits trouvés';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionAddAll => 'Tout ajouter';
|
||||
|
||||
@override
|
||||
String get recognitionAddToStock => 'Ajouter au garde-manger';
|
||||
|
||||
@override
|
||||
String recognitionAdded(int count) {
|
||||
return '$count produits ajoutés';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionProductsFailed => 'Échec de l\'ajout des produits';
|
||||
|
||||
@override
|
||||
String get recognitionEmpty => 'Aucun produit trouvé';
|
||||
|
||||
@override
|
||||
String recognitionConfidence(int percent) {
|
||||
return '$percent% de confiance';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionReplaceProduct => 'Remplacer le produit';
|
||||
|
||||
@override
|
||||
String get scanJobCloseHint =>
|
||||
'Vous pouvez fermer l\'app — ce scan apparaîtra dans Scans récents sur l\'écran Produits';
|
||||
}
|
||||
|
||||
@@ -544,4 +544,38 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get productAddedToShelf => 'पेंट्री में जोड़ा गया';
|
||||
|
||||
@override
|
||||
String recognitionFoundProducts(int count) {
|
||||
return '$count उत्पाद मिले';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionAddAll => 'सब जोड़ें';
|
||||
|
||||
@override
|
||||
String get recognitionAddToStock => 'पेंट्री में जोड़ें';
|
||||
|
||||
@override
|
||||
String recognitionAdded(int count) {
|
||||
return '$count उत्पाद जोड़े गए';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionProductsFailed => 'उत्पाद जोड़ने में विफल';
|
||||
|
||||
@override
|
||||
String get recognitionEmpty => 'कोई उत्पाद नहीं मिला';
|
||||
|
||||
@override
|
||||
String recognitionConfidence(int percent) {
|
||||
return '$percent% विश्वसनीयता';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionReplaceProduct => 'उत्पाद बदलें';
|
||||
|
||||
@override
|
||||
String get scanJobCloseHint =>
|
||||
'आप ऐप बंद कर सकते हैं — यह स्कैन उत्पाद स्क्रीन पर हाल के स्कैन में दिखाई देगा';
|
||||
}
|
||||
|
||||
@@ -546,4 +546,38 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get productAddedToShelf => 'Aggiunto alla dispensa';
|
||||
|
||||
@override
|
||||
String recognitionFoundProducts(int count) {
|
||||
return 'Trovati $count prodotti';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionAddAll => 'Aggiungi tutto';
|
||||
|
||||
@override
|
||||
String get recognitionAddToStock => 'Aggiungi alla dispensa';
|
||||
|
||||
@override
|
||||
String recognitionAdded(int count) {
|
||||
return 'Aggiunti $count prodotti';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionProductsFailed => 'Impossibile aggiungere i prodotti';
|
||||
|
||||
@override
|
||||
String get recognitionEmpty => 'Nessun prodotto trovato';
|
||||
|
||||
@override
|
||||
String recognitionConfidence(int percent) {
|
||||
return '$percent% di certezza';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionReplaceProduct => 'Sostituisci prodotto';
|
||||
|
||||
@override
|
||||
String get scanJobCloseHint =>
|
||||
'Puoi chiudere l\'app — questa scansione apparirà in Scansioni recenti nella schermata Prodotti';
|
||||
}
|
||||
|
||||
@@ -538,4 +538,37 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get productAddedToShelf => 'パントリーに追加しました';
|
||||
|
||||
@override
|
||||
String recognitionFoundProducts(int count) {
|
||||
return '$count個の商品が見つかりました';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionAddAll => 'すべて追加';
|
||||
|
||||
@override
|
||||
String get recognitionAddToStock => 'パントリーに追加';
|
||||
|
||||
@override
|
||||
String recognitionAdded(int count) {
|
||||
return '$count個の商品を追加しました';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionProductsFailed => '商品の追加に失敗しました';
|
||||
|
||||
@override
|
||||
String get recognitionEmpty => '商品が見つかりません';
|
||||
|
||||
@override
|
||||
String recognitionConfidence(int percent) {
|
||||
return '$percent%の信頼度';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionReplaceProduct => '商品を置き換える';
|
||||
|
||||
@override
|
||||
String get scanJobCloseHint => 'アプリを閉じても大丈夫です — このスキャンは製品画面の最近のスキャンに表示されます';
|
||||
}
|
||||
|
||||
@@ -538,4 +538,37 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get productAddedToShelf => '저장실에 추가되었습니다';
|
||||
|
||||
@override
|
||||
String recognitionFoundProducts(int count) {
|
||||
return '$count개 제품 찾음';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionAddAll => '모두 추가';
|
||||
|
||||
@override
|
||||
String get recognitionAddToStock => '저장실에 추가';
|
||||
|
||||
@override
|
||||
String recognitionAdded(int count) {
|
||||
return '$count개 제품 추가됨';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionProductsFailed => '제품 추가 실패';
|
||||
|
||||
@override
|
||||
String get recognitionEmpty => '제품을 찾을 수 없습니다';
|
||||
|
||||
@override
|
||||
String recognitionConfidence(int percent) {
|
||||
return '$percent% 신뢰도';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionReplaceProduct => '제품 교체';
|
||||
|
||||
@override
|
||||
String get scanJobCloseHint => '앱을 닫아도 됩니다 — 이 스캔은 제품 화면의 최근 스캔에 표시됩니다';
|
||||
}
|
||||
|
||||
@@ -545,4 +545,38 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get productAddedToShelf => 'Adicionado à despensa';
|
||||
|
||||
@override
|
||||
String recognitionFoundProducts(int count) {
|
||||
return 'Encontrado $count produtos';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionAddAll => 'Adicionar tudo';
|
||||
|
||||
@override
|
||||
String get recognitionAddToStock => 'Adicionar ao estoque';
|
||||
|
||||
@override
|
||||
String recognitionAdded(int count) {
|
||||
return 'Adicionado $count produtos';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionProductsFailed => 'Falha ao adicionar produtos';
|
||||
|
||||
@override
|
||||
String get recognitionEmpty => 'Nenhum produto encontrado';
|
||||
|
||||
@override
|
||||
String recognitionConfidence(int percent) {
|
||||
return '$percent% de confiança';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionReplaceProduct => 'Substituir produto';
|
||||
|
||||
@override
|
||||
String get scanJobCloseHint =>
|
||||
'Você pode fechar o app — este scan aparecerá em Scans recentes na tela de Produtos';
|
||||
}
|
||||
|
||||
@@ -544,4 +544,38 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get productAddedToShelf => 'Добавлено в холодильник';
|
||||
|
||||
@override
|
||||
String recognitionFoundProducts(int count) {
|
||||
return 'Найдено $count продуктов';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionAddAll => 'Добавить всё';
|
||||
|
||||
@override
|
||||
String get recognitionAddToStock => 'В запасы';
|
||||
|
||||
@override
|
||||
String recognitionAdded(int count) {
|
||||
return 'Добавлено $count продуктов';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionProductsFailed => 'Не удалось добавить продукты';
|
||||
|
||||
@override
|
||||
String get recognitionEmpty => 'Продукты не найдены';
|
||||
|
||||
@override
|
||||
String recognitionConfidence(int percent) {
|
||||
return '$percent% уверенность';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionReplaceProduct => 'Заменить продукт';
|
||||
|
||||
@override
|
||||
String get scanJobCloseHint =>
|
||||
'Можно закрыть приложение — скан появится в последних сканированиях на экране продуктов';
|
||||
}
|
||||
|
||||
@@ -537,4 +537,37 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get productAddedToShelf => '已添加到储藏室';
|
||||
|
||||
@override
|
||||
String recognitionFoundProducts(int count) {
|
||||
return '找到 $count 个产品';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionAddAll => '全部添加';
|
||||
|
||||
@override
|
||||
String get recognitionAddToStock => '加入储藏室';
|
||||
|
||||
@override
|
||||
String recognitionAdded(int count) {
|
||||
return '已添加 $count 个产品';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionProductsFailed => '添加产品失败';
|
||||
|
||||
@override
|
||||
String get recognitionEmpty => '未找到产品';
|
||||
|
||||
@override
|
||||
String recognitionConfidence(int percent) {
|
||||
return '$percent% 置信度';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recognitionReplaceProduct => '替换产品';
|
||||
|
||||
@override
|
||||
String get scanJobCloseHint => '您可以关闭应用 — 此扫描将显示在产品页面的最近扫描中';
|
||||
}
|
||||
|
||||
@@ -201,5 +201,35 @@
|
||||
"fat": "Gorduras",
|
||||
"carbs": "Carboidratos",
|
||||
"fiber": "Fibra",
|
||||
"productAddedToShelf": "Adicionado à despensa"
|
||||
"productAddedToShelf": "Adicionado à despensa",
|
||||
"recognitionFoundProducts": "Encontrado {count} produtos",
|
||||
"@recognitionFoundProducts": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionAddAll": "Adicionar tudo",
|
||||
"recognitionAddToStock": "Adicionar ao estoque",
|
||||
"recognitionAdded": "Adicionado {count} produtos",
|
||||
"@recognitionAdded": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionProductsFailed": "Falha ao adicionar produtos",
|
||||
"recognitionEmpty": "Nenhum produto encontrado",
|
||||
"recognitionConfidence": "{percent}% de confiança",
|
||||
"@recognitionConfidence": {
|
||||
"placeholders": {
|
||||
"percent": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionReplaceProduct": "Substituir produto",
|
||||
"scanJobCloseHint": "Você pode fechar o app — este scan aparecerá em Scans recentes na tela de Produtos"
|
||||
}
|
||||
|
||||
@@ -201,5 +201,35 @@
|
||||
"fat": "Жиры",
|
||||
"carbs": "Углеводы",
|
||||
"fiber": "Клетчатка",
|
||||
"productAddedToShelf": "Добавлено в холодильник"
|
||||
"productAddedToShelf": "Добавлено в холодильник",
|
||||
"recognitionFoundProducts": "Найдено {count} продуктов",
|
||||
"@recognitionFoundProducts": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionAddAll": "Добавить всё",
|
||||
"recognitionAddToStock": "В запасы",
|
||||
"recognitionAdded": "Добавлено {count} продуктов",
|
||||
"@recognitionAdded": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionProductsFailed": "Не удалось добавить продукты",
|
||||
"recognitionEmpty": "Продукты не найдены",
|
||||
"recognitionConfidence": "{percent}% уверенность",
|
||||
"@recognitionConfidence": {
|
||||
"placeholders": {
|
||||
"percent": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionReplaceProduct": "Заменить продукт",
|
||||
"scanJobCloseHint": "Можно закрыть приложение — скан появится в последних сканированиях на экране продуктов"
|
||||
}
|
||||
|
||||
@@ -201,5 +201,35 @@
|
||||
"fat": "脂肪",
|
||||
"carbs": "碳水化合物",
|
||||
"fiber": "膳食纤维",
|
||||
"productAddedToShelf": "已添加到储藏室"
|
||||
"productAddedToShelf": "已添加到储藏室",
|
||||
"recognitionFoundProducts": "找到 {count} 个产品",
|
||||
"@recognitionFoundProducts": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionAddAll": "全部添加",
|
||||
"recognitionAddToStock": "加入储藏室",
|
||||
"recognitionAdded": "已添加 {count} 个产品",
|
||||
"@recognitionAdded": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionProductsFailed": "添加产品失败",
|
||||
"recognitionEmpty": "未找到产品",
|
||||
"recognitionConfidence": "{percent}% 置信度",
|
||||
"@recognitionConfidence": {
|
||||
"placeholders": {
|
||||
"percent": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recognitionReplaceProduct": "替换产品",
|
||||
"scanJobCloseHint": "您可以关闭应用 — 此扫描将显示在产品页面的最近扫描中"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user