feat: product search screen with catalog add and success feedback
- Add product search screen (/products/search) as primary add flow; "Add" button on products list opens search, manual entry remains as fallback - Add to shelf bottom sheet with AnimatedSwitcher success view (green checkmark) and SnackBar confirmation on the search screen via onAdded callback - Manual add (AddProductScreen) shows SnackBar on success before popping back - Extend AddProductScreen with optional nutrition fields (calories, protein, fat, carbs, fiber); auto-fills from catalog selection and auto-expands section - Auto-upsert catalog product on backend when nutrition data is provided without a primary_product_id, linking the user product to the catalog - Add fiber_per_100g field to CatalogProduct model and CreateRequest - Add 16 new L10n keys across all 12 locales (addProduct, addManually, searchProducts, quantity, storageDays, addToShelf, nutritionOptional, calories, protein, fat, carbs, fiber, productAddedToShelf, etc.) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import '../../features/profile/profile_provider.dart';
|
||||
import '../../shared/models/user.dart';
|
||||
import '../../features/products/products_screen.dart';
|
||||
import '../../features/products/add_product_screen.dart';
|
||||
import '../../features/products/product_search_screen.dart';
|
||||
import '../../features/products/product_job_history_screen.dart';
|
||||
import '../../features/scan/product_job_watch_screen.dart';
|
||||
import '../../features/scan/scan_screen.dart';
|
||||
@@ -107,6 +108,11 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
path: '/products/add',
|
||||
builder: (_, __) => const AddProductScreen(),
|
||||
),
|
||||
// Product search — search catalog before falling back to manual add.
|
||||
GoRoute(
|
||||
path: '/products/search',
|
||||
builder: (_, __) => const ProductSearchScreen(),
|
||||
),
|
||||
// Shopping list — full-screen, no bottom nav.
|
||||
GoRoute(
|
||||
path: '/menu/shopping-list',
|
||||
|
||||
@@ -4,6 +4,7 @@ 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';
|
||||
|
||||
@@ -23,17 +24,30 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
String? _category;
|
||||
String? _primaryProductId;
|
||||
bool _saving = false;
|
||||
bool _showNutrition = false;
|
||||
|
||||
// Autocomplete state
|
||||
List<CatalogProduct> _suggestions = [];
|
||||
bool _searching = false;
|
||||
Timer? _debounce;
|
||||
|
||||
// Optional nutrition fields
|
||||
final _caloriesController = TextEditingController();
|
||||
final _proteinController = TextEditingController();
|
||||
final _fatController = TextEditingController();
|
||||
final _carbsController = TextEditingController();
|
||||
final _fiberController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_qtyController.dispose();
|
||||
_daysController.dispose();
|
||||
_caloriesController.dispose();
|
||||
_proteinController.dispose();
|
||||
_fatController.dispose();
|
||||
_carbsController.dispose();
|
||||
_fiberController.dispose();
|
||||
_debounce?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -73,6 +87,36 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
if (catalogProduct.storageDays != null) {
|
||||
_daysController.text = catalogProduct.storageDays.toString();
|
||||
}
|
||||
|
||||
// Pre-fill nutrition from catalog data.
|
||||
if (catalogProduct.caloriesPer100g != null) {
|
||||
_caloriesController.text =
|
||||
catalogProduct.caloriesPer100g!.toStringAsFixed(1);
|
||||
}
|
||||
if (catalogProduct.proteinPer100g != null) {
|
||||
_proteinController.text =
|
||||
catalogProduct.proteinPer100g!.toStringAsFixed(1);
|
||||
}
|
||||
if (catalogProduct.fatPer100g != null) {
|
||||
_fatController.text = catalogProduct.fatPer100g!.toStringAsFixed(1);
|
||||
}
|
||||
if (catalogProduct.carbsPer100g != null) {
|
||||
_carbsController.text =
|
||||
catalogProduct.carbsPer100g!.toStringAsFixed(1);
|
||||
}
|
||||
if (catalogProduct.fiberPer100g != null) {
|
||||
_fiberController.text =
|
||||
catalogProduct.fiberPer100g!.toStringAsFixed(1);
|
||||
}
|
||||
|
||||
// Auto-expand nutrition section when any value is available.
|
||||
final hasNutrition = catalogProduct.caloriesPer100g != null ||
|
||||
catalogProduct.proteinPer100g != null ||
|
||||
catalogProduct.fatPer100g != null ||
|
||||
catalogProduct.carbsPer100g != null ||
|
||||
catalogProduct.fiberPer100g != null;
|
||||
if (hasNutrition) _showNutrition = true;
|
||||
|
||||
_suggestions = [];
|
||||
});
|
||||
}
|
||||
@@ -88,6 +132,8 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
|
||||
final qty = double.tryParse(_qtyController.text) ?? 1;
|
||||
final days = int.tryParse(_daysController.text) ?? 7;
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final addedText = AppLocalizations.of(context)!.productAddedToShelf;
|
||||
|
||||
setState(() => _saving = true);
|
||||
try {
|
||||
@@ -98,8 +144,18 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
category: _category,
|
||||
storageDays: days,
|
||||
primaryProductId: _primaryProductId,
|
||||
caloriesPer100g: double.tryParse(_caloriesController.text),
|
||||
proteinPer100g: double.tryParse(_proteinController.text),
|
||||
fatPer100g: double.tryParse(_fatController.text),
|
||||
carbsPer100g: double.tryParse(_carbsController.text),
|
||||
fiberPer100g: double.tryParse(_fiberController.text),
|
||||
);
|
||||
if (mounted) Navigator.pop(context);
|
||||
if (mounted) {
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text('$name — $addedText')),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -113,6 +169,7 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Добавить продукт')),
|
||||
body: GestureDetector(
|
||||
@@ -210,7 +267,80 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Nutrition section (optional, collapsed by default)
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () => setState(() => _showNutrition = !_showNutrition),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
_showNutrition
|
||||
? Icons.expand_less
|
||||
: Icons.expand_more,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.nutritionOptional,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (_showNutrition) ...[
|
||||
const SizedBox(height: 8),
|
||||
_NutritionField(
|
||||
controller: _caloriesController,
|
||||
label: l10n.calories,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _NutritionField(
|
||||
controller: _proteinController,
|
||||
label: l10n.protein,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: _NutritionField(
|
||||
controller: _fatController,
|
||||
label: l10n.fat,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _NutritionField(
|
||||
controller: _carbsController,
|
||||
label: l10n.carbs,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: _NutritionField(
|
||||
controller: _fiberController,
|
||||
label: l10n.fiber,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
FilledButton(
|
||||
onPressed: _saving ? null : _submit,
|
||||
@@ -249,3 +379,27 @@ class _AddProductScreenState extends ConsumerState<AddProductScreen> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Compact numeric text field for nutrition values
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class _NutritionField extends StatelessWidget {
|
||||
const _NutritionField({required this.controller, required this.label});
|
||||
|
||||
final TextEditingController controller;
|
||||
final String label;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextField(
|
||||
controller: controller,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
581
client/lib/features/products/product_search_screen.dart
Normal file
581
client/lib/features/products/product_search_screen.dart
Normal file
@@ -0,0 +1,581 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../core/locale/unit_provider.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../../shared/models/product.dart';
|
||||
import 'user_product_provider.dart';
|
||||
|
||||
class ProductSearchScreen extends ConsumerStatefulWidget {
|
||||
const ProductSearchScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ProductSearchScreen> createState() =>
|
||||
_ProductSearchScreenState();
|
||||
}
|
||||
|
||||
class _ProductSearchScreenState extends ConsumerState<ProductSearchScreen> {
|
||||
final _searchController = TextEditingController();
|
||||
Timer? _debounce;
|
||||
List<CatalogProduct> _results = [];
|
||||
bool _isLoading = false;
|
||||
String _query = '';
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
_debounce?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onQueryChanged(String value) {
|
||||
_debounce?.cancel();
|
||||
setState(() => _query = value.trim());
|
||||
|
||||
if (value.trim().isEmpty) {
|
||||
setState(() => _results = []);
|
||||
return;
|
||||
}
|
||||
|
||||
_debounce = Timer(const Duration(milliseconds: 300), () async {
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
final service = ref.read(userProductServiceProvider);
|
||||
final searchResults = await service.searchProducts(value.trim());
|
||||
if (mounted) setState(() => _results = searchResults);
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _openShelfSheet(CatalogProduct catalogProduct) {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final productName = catalogProduct.displayName;
|
||||
final addedText = AppLocalizations.of(context)!.productAddedToShelf;
|
||||
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (_) => _AddToShelfSheet(
|
||||
catalogProduct: catalogProduct,
|
||||
onAdded: () => messenger.showSnackBar(
|
||||
SnackBar(content: Text('$productName — $addedText')),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l10n.addProduct),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.push('/products/add'),
|
||||
child: Text(l10n.addManually),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
onChanged: _onQueryChanged,
|
||||
autofocus: true,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.searchProducts,
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: _isLoading
|
||||
? const Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
)
|
||||
: _query.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
_onQueryChanged('');
|
||||
},
|
||||
)
|
||||
: null,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildBody(l10n),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(AppLocalizations l10n) {
|
||||
if (_query.isEmpty) {
|
||||
return _EmptyQueryPrompt(onAddManually: () => context.push('/products/add'));
|
||||
}
|
||||
if (_isLoading && _results.isEmpty) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (!_isLoading && _results.isEmpty) {
|
||||
return _NoResultsView(
|
||||
query: _query,
|
||||
onAddManually: () => context.push('/products/add'),
|
||||
);
|
||||
}
|
||||
return ListView.separated(
|
||||
itemCount: _results.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) => _CatalogProductTile(
|
||||
catalogProduct: _results[index],
|
||||
onTap: () => _openShelfSheet(_results[index]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Empty query state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class _EmptyQueryPrompt extends StatelessWidget {
|
||||
const _EmptyQueryPrompt({required this.onAddManually});
|
||||
|
||||
final VoidCallback onAddManually;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final theme = Theme.of(context);
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.search,
|
||||
size: 64,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.searchProductsHint,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
OutlinedButton.icon(
|
||||
onPressed: onAddManually,
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
label: Text(l10n.addManually),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// No results state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class _NoResultsView extends StatelessWidget {
|
||||
const _NoResultsView({required this.query, required this.onAddManually});
|
||||
|
||||
final String query;
|
||||
final VoidCallback onAddManually;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final theme = Theme.of(context);
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.search_off,
|
||||
size: 48,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.noSearchResults(query),
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton.icon(
|
||||
onPressed: onAddManually,
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
label: Text(l10n.addManually),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Catalog product tile
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class _CatalogProductTile extends StatelessWidget {
|
||||
const _CatalogProductTile({
|
||||
required this.catalogProduct,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final CatalogProduct catalogProduct;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
theme.colorScheme.secondaryContainer,
|
||||
child: Text(
|
||||
_categoryEmoji(catalogProduct.category),
|
||||
style: const TextStyle(fontSize: 20),
|
||||
),
|
||||
),
|
||||
title: Text(catalogProduct.displayName),
|
||||
subtitle: catalogProduct.categoryName != null
|
||||
? Text(
|
||||
catalogProduct.categoryName!,
|
||||
style: theme.textTheme.bodySmall,
|
||||
)
|
||||
: null,
|
||||
trailing: catalogProduct.caloriesPer100g != null
|
||||
? Text(
|
||||
'${catalogProduct.caloriesPer100g!.round()} kcal',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
|
||||
String _categoryEmoji(String? category) {
|
||||
switch (category) {
|
||||
case 'meat':
|
||||
return '🥩';
|
||||
case 'dairy':
|
||||
return '🥛';
|
||||
case 'vegetable':
|
||||
return '🥕';
|
||||
case 'fruit':
|
||||
return '🍎';
|
||||
case 'grain':
|
||||
return '🌾';
|
||||
case 'seafood':
|
||||
return '🐟';
|
||||
case 'condiment':
|
||||
return '🧂';
|
||||
default:
|
||||
return '🛒';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,33 +9,28 @@ import '../scan/recognition_service.dart';
|
||||
import 'product_job_provider.dart';
|
||||
import 'user_product_provider.dart';
|
||||
|
||||
void _showAddMenu(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
Future<void> _confirmClearAll(
|
||||
BuildContext context, WidgetRef ref, AppLocalizations l10n) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.edit_outlined),
|
||||
title: const Text('Добавить вручную'),
|
||||
onTap: () {
|
||||
Navigator.pop(ctx);
|
||||
context.push('/products/add');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.document_scanner_outlined),
|
||||
title: const Text('Сканировать чек или фото'),
|
||||
onTap: () {
|
||||
Navigator.pop(ctx);
|
||||
context.push('/scan');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(l10n.clearAllConfirmTitle),
|
||||
content: Text(l10n.clearAllConfirmMessage),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: Text(l10n.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: Text(l10n.clearAllProducts),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed == true) {
|
||||
ref.read(userProductsProvider.notifier).deleteAll();
|
||||
}
|
||||
}
|
||||
|
||||
class ProductsScreen extends ConsumerWidget {
|
||||
@@ -44,22 +39,24 @@ class ProductsScreen extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final state = ref.watch(userProductsProvider);
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Мои продукты'),
|
||||
actions: [
|
||||
if (state.valueOrNull?.isNotEmpty == true)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_sweep_outlined),
|
||||
tooltip: l10n.clearAllProducts,
|
||||
onPressed: () => _confirmClearAll(context, ref, l10n),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () => ref.read(userProductsProvider.notifier).refresh(),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () => _showAddMenu(context),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Добавить'),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
const _RecentScansSection(),
|
||||
@@ -70,10 +67,11 @@ class ProductsScreen extends ConsumerWidget {
|
||||
onRetry: () => ref.read(userProductsProvider.notifier).refresh(),
|
||||
),
|
||||
data: (products) => products.isEmpty
|
||||
? _EmptyState(onAdd: () => _showAddMenu(context))
|
||||
? const _EmptyState()
|
||||
: _ProductList(products: products),
|
||||
),
|
||||
),
|
||||
const _BottomActionBar(),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -252,7 +250,7 @@ class _ProductList extends ConsumerWidget {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => ref.read(userProductsProvider.notifier).refresh(),
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.only(bottom: 80),
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
if (expiring.isNotEmpty) ...[
|
||||
_SectionHeader(
|
||||
@@ -563,9 +561,7 @@ class _EditProductSheetState extends ConsumerState<_EditProductSheet> {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class _EmptyState extends StatelessWidget {
|
||||
const _EmptyState({required this.onAdd});
|
||||
|
||||
final VoidCallback onAdd;
|
||||
const _EmptyState();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -588,17 +584,59 @@ class _EmptyState extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Добавьте продукты вручную — или в Итерации 3 сфотографируйте чек',
|
||||
'Добавьте продукты вручную или сфотографируйте чек',
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton.icon(
|
||||
onPressed: onAdd,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Добавить продукт'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bottom action toolbar
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class _BottomActionBar extends StatelessWidget {
|
||||
const _BottomActionBar();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: theme.colorScheme.outlineVariant,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(16, 10, 16, 10),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FilledButton.icon(
|
||||
onPressed: () => context.push('/products/search'),
|
||||
icon: const Icon(Icons.add, size: 18),
|
||||
label: Text(l10n.addProduct),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: FilledButton.tonalIcon(
|
||||
onPressed: () => context.push('/scan'),
|
||||
icon: const Icon(Icons.document_scanner_outlined, size: 18),
|
||||
label: Text(l10n.scan),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -46,6 +46,11 @@ class UserProductsNotifier
|
||||
String? category,
|
||||
int storageDays = 7,
|
||||
String? primaryProductId,
|
||||
double? caloriesPer100g,
|
||||
double? proteinPer100g,
|
||||
double? fatPer100g,
|
||||
double? carbsPer100g,
|
||||
double? fiberPer100g,
|
||||
}) async {
|
||||
final userProduct = await _service.createProduct(
|
||||
name: name,
|
||||
@@ -54,6 +59,11 @@ class UserProductsNotifier
|
||||
category: category,
|
||||
storageDays: storageDays,
|
||||
primaryProductId: primaryProductId,
|
||||
caloriesPer100g: caloriesPer100g,
|
||||
proteinPer100g: proteinPer100g,
|
||||
fatPer100g: fatPer100g,
|
||||
carbsPer100g: carbsPer100g,
|
||||
fiberPer100g: fiberPer100g,
|
||||
);
|
||||
state.whenData((products) {
|
||||
final updated = [...products, userProduct]
|
||||
@@ -86,6 +96,20 @@ class UserProductsNotifier
|
||||
});
|
||||
}
|
||||
|
||||
/// Optimistically clears all products, restores on error.
|
||||
Future<void> deleteAll() async {
|
||||
final previous = state;
|
||||
state.whenData((_) {
|
||||
state = const AsyncValue.data([]);
|
||||
});
|
||||
try {
|
||||
await _service.deleteAllProducts();
|
||||
} catch (_) {
|
||||
state = previous;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Optimistically removes the product, restores on error.
|
||||
Future<void> delete(String id) async {
|
||||
final previous = state;
|
||||
|
||||
@@ -21,6 +21,11 @@ class UserProductService {
|
||||
String? category,
|
||||
int storageDays = 7,
|
||||
String? primaryProductId,
|
||||
double? caloriesPer100g,
|
||||
double? proteinPer100g,
|
||||
double? fatPer100g,
|
||||
double? carbsPer100g,
|
||||
double? fiberPer100g,
|
||||
}) async {
|
||||
final data = await _client.post('/user-products', data: {
|
||||
'name': name,
|
||||
@@ -29,6 +34,11 @@ class UserProductService {
|
||||
if (category != null) 'category': category,
|
||||
'storage_days': storageDays,
|
||||
if (primaryProductId != null) 'primary_product_id': primaryProductId,
|
||||
if (caloriesPer100g != null) 'calories_per_100g': caloriesPer100g,
|
||||
if (proteinPer100g != null) 'protein_per_100g': proteinPer100g,
|
||||
if (fatPer100g != null) 'fat_per_100g': fatPer100g,
|
||||
if (carbsPer100g != null) 'carbs_per_100g': carbsPer100g,
|
||||
if (fiberPer100g != null) 'fiber_per_100g': fiberPer100g,
|
||||
});
|
||||
return UserProduct.fromJson(data);
|
||||
}
|
||||
@@ -54,6 +64,8 @@ class UserProductService {
|
||||
Future<void> deleteProduct(String id) =>
|
||||
_client.deleteVoid('/user-products/$id');
|
||||
|
||||
Future<void> deleteAllProducts() => _client.deleteVoid('/user-products');
|
||||
|
||||
Future<List<CatalogProduct>> searchProducts(String query) async {
|
||||
if (query.isEmpty) return [];
|
||||
final list = await _client.getList(
|
||||
|
||||
@@ -172,5 +172,32 @@
|
||||
"jobTypeReceipt": "إيصال",
|
||||
"jobTypeProducts": "منتجات",
|
||||
"scanSubmitting": "جارٍ الإرسال...",
|
||||
"processingProducts": "جارٍ المعالجة..."
|
||||
"processingProducts": "جارٍ المعالجة...",
|
||||
"clearAllProducts": "مسح الكل",
|
||||
"clearAllConfirmTitle": "مسح جميع المنتجات؟",
|
||||
"clearAllConfirmMessage": "سيتم حذف جميع المنتجات نهائياً.",
|
||||
"addManually": "يدويًا",
|
||||
"scan": "مسح ضوئي",
|
||||
"addProduct": "إضافة",
|
||||
"searchProducts": "البحث عن المنتجات",
|
||||
"searchProductsHint": "اكتب اسم المنتج أو أضف يدويًا",
|
||||
"noSearchResults": "لا توجد نتائج لـ\"{query}\"",
|
||||
"@noSearchResults": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"quantity": "الكمية",
|
||||
"storageDays": "أيام التخزين",
|
||||
"addToShelf": "إضافة إلى المخزن",
|
||||
"errorGeneric": "حدث خطأ ما",
|
||||
"nutritionOptional": "القيم الغذائية لكل 100 جم (اختياري)",
|
||||
"calories": "سعرات حرارية",
|
||||
"protein": "بروتين",
|
||||
"fat": "دهون",
|
||||
"carbs": "كربوهيدرات",
|
||||
"fiber": "ألياف",
|
||||
"productAddedToShelf": "تمت الإضافة إلى المخزن"
|
||||
}
|
||||
|
||||
@@ -172,5 +172,32 @@
|
||||
"jobTypeReceipt": "Kassenbon",
|
||||
"jobTypeProducts": "Produkte",
|
||||
"scanSubmitting": "Wird gesendet...",
|
||||
"processingProducts": "Verarbeitung..."
|
||||
"processingProducts": "Verarbeitung...",
|
||||
"clearAllProducts": "Alles löschen",
|
||||
"clearAllConfirmTitle": "Alle Produkte löschen?",
|
||||
"clearAllConfirmMessage": "Alle Produkte werden dauerhaft gelöscht.",
|
||||
"addManually": "Manuell",
|
||||
"scan": "Scannen",
|
||||
"addProduct": "Hinzufügen",
|
||||
"searchProducts": "Produkte suchen",
|
||||
"searchProductsHint": "Produktname eingeben oder manuell hinzufügen",
|
||||
"noSearchResults": "Keine Ergebnisse für \"{query}\"",
|
||||
"@noSearchResults": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"quantity": "Menge",
|
||||
"storageDays": "Lagertage",
|
||||
"addToShelf": "Zum Vorrat hinzufügen",
|
||||
"errorGeneric": "Etwas ist schiefgelaufen",
|
||||
"nutritionOptional": "Nährwerte pro 100g (optional)",
|
||||
"calories": "Kalorien",
|
||||
"protein": "Eiweiß",
|
||||
"fat": "Fett",
|
||||
"carbs": "Kohlenhydrate",
|
||||
"fiber": "Ballaststoffe",
|
||||
"productAddedToShelf": "Zum Vorrat hinzugefügt"
|
||||
}
|
||||
|
||||
@@ -172,5 +172,32 @@
|
||||
"jobTypeReceipt": "Receipt",
|
||||
"jobTypeProducts": "Products",
|
||||
"scanSubmitting": "Submitting...",
|
||||
"processingProducts": "Processing..."
|
||||
"processingProducts": "Processing...",
|
||||
"clearAllProducts": "Clear all",
|
||||
"clearAllConfirmTitle": "Clear all products?",
|
||||
"clearAllConfirmMessage": "All products will be permanently deleted.",
|
||||
"addManually": "Manually",
|
||||
"scan": "Scan",
|
||||
"addProduct": "Add",
|
||||
"searchProducts": "Search products",
|
||||
"searchProductsHint": "Type a product name to search or add manually",
|
||||
"noSearchResults": "No results for \"{query}\"",
|
||||
"@noSearchResults": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"quantity": "Quantity",
|
||||
"storageDays": "Storage days",
|
||||
"addToShelf": "Add to pantry",
|
||||
"errorGeneric": "Something went wrong",
|
||||
"nutritionOptional": "Nutrition per 100g (optional)",
|
||||
"calories": "Calories",
|
||||
"protein": "Protein",
|
||||
"fat": "Fat",
|
||||
"carbs": "Carbohydrates",
|
||||
"fiber": "Fiber",
|
||||
"productAddedToShelf": "Added to pantry"
|
||||
}
|
||||
|
||||
@@ -172,5 +172,32 @@
|
||||
"jobTypeReceipt": "Ticket",
|
||||
"jobTypeProducts": "Productos",
|
||||
"scanSubmitting": "Enviando...",
|
||||
"processingProducts": "Procesando..."
|
||||
"processingProducts": "Procesando...",
|
||||
"clearAllProducts": "Borrar todo",
|
||||
"clearAllConfirmTitle": "¿Borrar todos los productos?",
|
||||
"clearAllConfirmMessage": "Todos los productos serán eliminados permanentemente.",
|
||||
"addManually": "Manual",
|
||||
"scan": "Escanear",
|
||||
"addProduct": "Agregar",
|
||||
"searchProducts": "Buscar productos",
|
||||
"searchProductsHint": "Escribe el nombre del producto o agrega manualmente",
|
||||
"noSearchResults": "Sin resultados para \"{query}\"",
|
||||
"@noSearchResults": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"quantity": "Cantidad",
|
||||
"storageDays": "Días de almacenamiento",
|
||||
"addToShelf": "Agregar a despensa",
|
||||
"errorGeneric": "Algo salió mal",
|
||||
"nutritionOptional": "Nutrición por 100g (opcional)",
|
||||
"calories": "Calorías",
|
||||
"protein": "Proteína",
|
||||
"fat": "Grasas",
|
||||
"carbs": "Carbohidratos",
|
||||
"fiber": "Fibra",
|
||||
"productAddedToShelf": "Agregado a la despensa"
|
||||
}
|
||||
|
||||
@@ -172,5 +172,32 @@
|
||||
"jobTypeReceipt": "Reçu",
|
||||
"jobTypeProducts": "Produits",
|
||||
"scanSubmitting": "Envoi...",
|
||||
"processingProducts": "Traitement..."
|
||||
"processingProducts": "Traitement...",
|
||||
"clearAllProducts": "Tout effacer",
|
||||
"clearAllConfirmTitle": "Effacer tous les produits ?",
|
||||
"clearAllConfirmMessage": "Tous les produits seront définitivement supprimés.",
|
||||
"addManually": "Manuellement",
|
||||
"scan": "Scanner",
|
||||
"addProduct": "Ajouter",
|
||||
"searchProducts": "Rechercher des produits",
|
||||
"searchProductsHint": "Tapez un nom de produit ou ajoutez manuellement",
|
||||
"noSearchResults": "Aucun résultat pour \"{query}\"",
|
||||
"@noSearchResults": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"quantity": "Quantité",
|
||||
"storageDays": "Jours de conservation",
|
||||
"addToShelf": "Ajouter au garde-manger",
|
||||
"errorGeneric": "Une erreur est survenue",
|
||||
"nutritionOptional": "Nutrition pour 100g (facultatif)",
|
||||
"calories": "Calories",
|
||||
"protein": "Protéines",
|
||||
"fat": "Graisses",
|
||||
"carbs": "Glucides",
|
||||
"fiber": "Fibres",
|
||||
"productAddedToShelf": "Ajouté au garde-manger"
|
||||
}
|
||||
|
||||
@@ -172,5 +172,32 @@
|
||||
"jobTypeReceipt": "रसीद",
|
||||
"jobTypeProducts": "उत्पाद",
|
||||
"scanSubmitting": "सबमिट हो रहा है...",
|
||||
"processingProducts": "प्रोसेस हो रहा है..."
|
||||
"processingProducts": "प्रोसेस हो रहा है...",
|
||||
"clearAllProducts": "सब साफ करें",
|
||||
"clearAllConfirmTitle": "सभी उत्पाद साफ करें?",
|
||||
"clearAllConfirmMessage": "सभी उत्पाद स्थायी रूप से हटा दिए जाएंगे।",
|
||||
"addManually": "मैन्युअल",
|
||||
"scan": "स्कैन करें",
|
||||
"addProduct": "जोड़ें",
|
||||
"searchProducts": "उत्पाद खोजें",
|
||||
"searchProductsHint": "उत्पाद का नाम टाइप करें या मैन्युअल रूप से जोड़ें",
|
||||
"noSearchResults": "\"{query}\" के लिए कोई परिणाम नहीं",
|
||||
"@noSearchResults": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"quantity": "मात्रा",
|
||||
"storageDays": "भंडारण दिन",
|
||||
"addToShelf": "पेंट्री में जोड़ें",
|
||||
"errorGeneric": "कुछ गलत हो गया",
|
||||
"nutritionOptional": "पोषण मूल्य प्रति 100 ग्राम (वैकल्पिक)",
|
||||
"calories": "कैलोरी",
|
||||
"protein": "प्रोटीन",
|
||||
"fat": "वसा",
|
||||
"carbs": "कार्बोहाइड्रेट",
|
||||
"fiber": "फाइबर",
|
||||
"productAddedToShelf": "पेंट्री में जोड़ा गया"
|
||||
}
|
||||
|
||||
@@ -172,5 +172,32 @@
|
||||
"jobTypeReceipt": "Scontrino",
|
||||
"jobTypeProducts": "Prodotti",
|
||||
"scanSubmitting": "Invio...",
|
||||
"processingProducts": "Elaborazione..."
|
||||
"processingProducts": "Elaborazione...",
|
||||
"clearAllProducts": "Cancella tutto",
|
||||
"clearAllConfirmTitle": "Cancellare tutti i prodotti?",
|
||||
"clearAllConfirmMessage": "Tutti i prodotti verranno eliminati definitivamente.",
|
||||
"addManually": "Manualmente",
|
||||
"scan": "Scansiona",
|
||||
"addProduct": "Aggiungi",
|
||||
"searchProducts": "Cerca prodotti",
|
||||
"searchProductsHint": "Digita il nome del prodotto o aggiungi manualmente",
|
||||
"noSearchResults": "Nessun risultato per \"{query}\"",
|
||||
"@noSearchResults": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"quantity": "Quantità",
|
||||
"storageDays": "Giorni di conservazione",
|
||||
"addToShelf": "Aggiungi alla dispensa",
|
||||
"errorGeneric": "Qualcosa è andato storto",
|
||||
"nutritionOptional": "Valori nutrizionali per 100g (opzionale)",
|
||||
"calories": "Calorie",
|
||||
"protein": "Proteine",
|
||||
"fat": "Grassi",
|
||||
"carbs": "Carboidrati",
|
||||
"fiber": "Fibre",
|
||||
"productAddedToShelf": "Aggiunto alla dispensa"
|
||||
}
|
||||
|
||||
@@ -172,5 +172,32 @@
|
||||
"jobTypeReceipt": "レシート",
|
||||
"jobTypeProducts": "商品",
|
||||
"scanSubmitting": "送信中...",
|
||||
"processingProducts": "処理中..."
|
||||
"processingProducts": "処理中...",
|
||||
"clearAllProducts": "すべてクリア",
|
||||
"clearAllConfirmTitle": "すべての商品をクリアしますか?",
|
||||
"clearAllConfirmMessage": "すべての商品が完全に削除されます。",
|
||||
"addManually": "手動",
|
||||
"scan": "スキャン",
|
||||
"addProduct": "追加",
|
||||
"searchProducts": "製品を検索",
|
||||
"searchProductsHint": "製品名を入力するか手動で追加してください",
|
||||
"noSearchResults": "「{query}」の検索結果はありません",
|
||||
"@noSearchResults": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"quantity": "数量",
|
||||
"storageDays": "保存日数",
|
||||
"addToShelf": "パントリーに追加",
|
||||
"errorGeneric": "エラーが発生しました",
|
||||
"nutritionOptional": "100gあたりの栄養素(任意)",
|
||||
"calories": "カロリー",
|
||||
"protein": "タンパク質",
|
||||
"fat": "脂質",
|
||||
"carbs": "炭水化物",
|
||||
"fiber": "食物繊維",
|
||||
"productAddedToShelf": "パントリーに追加しました"
|
||||
}
|
||||
|
||||
@@ -172,5 +172,32 @@
|
||||
"jobTypeReceipt": "영수증",
|
||||
"jobTypeProducts": "제품",
|
||||
"scanSubmitting": "제출 중...",
|
||||
"processingProducts": "처리 중..."
|
||||
"processingProducts": "처리 중...",
|
||||
"clearAllProducts": "모두 지우기",
|
||||
"clearAllConfirmTitle": "모든 제품을 지울까요?",
|
||||
"clearAllConfirmMessage": "모든 제품이 영구적으로 삭제됩니다.",
|
||||
"addManually": "수동",
|
||||
"scan": "스캔",
|
||||
"addProduct": "추가",
|
||||
"searchProducts": "제품 검색",
|
||||
"searchProductsHint": "제품 이름을 입력하거나 수동으로 추가하세요",
|
||||
"noSearchResults": "「{query}」에 대한 결과가 없습니다",
|
||||
"@noSearchResults": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"quantity": "수량",
|
||||
"storageDays": "보관 일수",
|
||||
"addToShelf": "저장실에 추가",
|
||||
"errorGeneric": "오류가 발생했습니다",
|
||||
"nutritionOptional": "100g당 영양성분 (선택사항)",
|
||||
"calories": "칼로리",
|
||||
"protein": "단백질",
|
||||
"fat": "지방",
|
||||
"carbs": "탄수화물",
|
||||
"fiber": "식이섬유",
|
||||
"productAddedToShelf": "저장실에 추가되었습니다"
|
||||
}
|
||||
|
||||
@@ -1029,6 +1029,126 @@ abstract class AppLocalizations {
|
||||
/// In en, this message translates to:
|
||||
/// **'Processing...'**
|
||||
String get processingProducts;
|
||||
|
||||
/// No description provided for @clearAllProducts.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Clear all'**
|
||||
String get clearAllProducts;
|
||||
|
||||
/// No description provided for @clearAllConfirmTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Clear all products?'**
|
||||
String get clearAllConfirmTitle;
|
||||
|
||||
/// No description provided for @clearAllConfirmMessage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'All products will be permanently deleted.'**
|
||||
String get clearAllConfirmMessage;
|
||||
|
||||
/// No description provided for @addManually.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Manually'**
|
||||
String get addManually;
|
||||
|
||||
/// No description provided for @scan.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Scan'**
|
||||
String get scan;
|
||||
|
||||
/// No description provided for @addProduct.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add'**
|
||||
String get addProduct;
|
||||
|
||||
/// No description provided for @searchProducts.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Search products'**
|
||||
String get searchProducts;
|
||||
|
||||
/// No description provided for @searchProductsHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Type a product name to search or add manually'**
|
||||
String get searchProductsHint;
|
||||
|
||||
/// No description provided for @noSearchResults.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No results for \"{query}\"'**
|
||||
String noSearchResults(String query);
|
||||
|
||||
/// No description provided for @quantity.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Quantity'**
|
||||
String get quantity;
|
||||
|
||||
/// No description provided for @storageDays.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Storage days'**
|
||||
String get storageDays;
|
||||
|
||||
/// No description provided for @addToShelf.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add to pantry'**
|
||||
String get addToShelf;
|
||||
|
||||
/// No description provided for @errorGeneric.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Something went wrong'**
|
||||
String get errorGeneric;
|
||||
|
||||
/// No description provided for @nutritionOptional.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Nutrition per 100g (optional)'**
|
||||
String get nutritionOptional;
|
||||
|
||||
/// No description provided for @calories.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Calories'**
|
||||
String get calories;
|
||||
|
||||
/// No description provided for @protein.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Protein'**
|
||||
String get protein;
|
||||
|
||||
/// No description provided for @fat.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Fat'**
|
||||
String get fat;
|
||||
|
||||
/// No description provided for @carbs.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Carbohydrates'**
|
||||
String get carbs;
|
||||
|
||||
/// No description provided for @fiber.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Fiber'**
|
||||
String get fiber;
|
||||
|
||||
/// No description provided for @productAddedToShelf.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Added to pantry'**
|
||||
String get productAddedToShelf;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@@ -473,4 +473,66 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get processingProducts => 'جارٍ المعالجة...';
|
||||
|
||||
@override
|
||||
String get clearAllProducts => 'مسح الكل';
|
||||
|
||||
@override
|
||||
String get clearAllConfirmTitle => 'مسح جميع المنتجات؟';
|
||||
|
||||
@override
|
||||
String get clearAllConfirmMessage => 'سيتم حذف جميع المنتجات نهائياً.';
|
||||
|
||||
@override
|
||||
String get addManually => 'يدويًا';
|
||||
|
||||
@override
|
||||
String get scan => 'مسح ضوئي';
|
||||
|
||||
@override
|
||||
String get addProduct => 'إضافة';
|
||||
|
||||
@override
|
||||
String get searchProducts => 'البحث عن المنتجات';
|
||||
|
||||
@override
|
||||
String get searchProductsHint => 'اكتب اسم المنتج أو أضف يدويًا';
|
||||
|
||||
@override
|
||||
String noSearchResults(String query) {
|
||||
return 'لا توجد نتائج لـ\"$query\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String get quantity => 'الكمية';
|
||||
|
||||
@override
|
||||
String get storageDays => 'أيام التخزين';
|
||||
|
||||
@override
|
||||
String get addToShelf => 'إضافة إلى المخزن';
|
||||
|
||||
@override
|
||||
String get errorGeneric => 'حدث خطأ ما';
|
||||
|
||||
@override
|
||||
String get nutritionOptional => 'القيم الغذائية لكل 100 جم (اختياري)';
|
||||
|
||||
@override
|
||||
String get calories => 'سعرات حرارية';
|
||||
|
||||
@override
|
||||
String get protein => 'بروتين';
|
||||
|
||||
@override
|
||||
String get fat => 'دهون';
|
||||
|
||||
@override
|
||||
String get carbs => 'كربوهيدرات';
|
||||
|
||||
@override
|
||||
String get fiber => 'ألياف';
|
||||
|
||||
@override
|
||||
String get productAddedToShelf => 'تمت الإضافة إلى المخزن';
|
||||
}
|
||||
|
||||
@@ -475,4 +475,68 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get processingProducts => 'Verarbeitung...';
|
||||
|
||||
@override
|
||||
String get clearAllProducts => 'Alles löschen';
|
||||
|
||||
@override
|
||||
String get clearAllConfirmTitle => 'Alle Produkte löschen?';
|
||||
|
||||
@override
|
||||
String get clearAllConfirmMessage =>
|
||||
'Alle Produkte werden dauerhaft gelöscht.';
|
||||
|
||||
@override
|
||||
String get addManually => 'Manuell';
|
||||
|
||||
@override
|
||||
String get scan => 'Scannen';
|
||||
|
||||
@override
|
||||
String get addProduct => 'Hinzufügen';
|
||||
|
||||
@override
|
||||
String get searchProducts => 'Produkte suchen';
|
||||
|
||||
@override
|
||||
String get searchProductsHint =>
|
||||
'Produktname eingeben oder manuell hinzufügen';
|
||||
|
||||
@override
|
||||
String noSearchResults(String query) {
|
||||
return 'Keine Ergebnisse für \"$query\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String get quantity => 'Menge';
|
||||
|
||||
@override
|
||||
String get storageDays => 'Lagertage';
|
||||
|
||||
@override
|
||||
String get addToShelf => 'Zum Vorrat hinzufügen';
|
||||
|
||||
@override
|
||||
String get errorGeneric => 'Etwas ist schiefgelaufen';
|
||||
|
||||
@override
|
||||
String get nutritionOptional => 'Nährwerte pro 100g (optional)';
|
||||
|
||||
@override
|
||||
String get calories => 'Kalorien';
|
||||
|
||||
@override
|
||||
String get protein => 'Eiweiß';
|
||||
|
||||
@override
|
||||
String get fat => 'Fett';
|
||||
|
||||
@override
|
||||
String get carbs => 'Kohlenhydrate';
|
||||
|
||||
@override
|
||||
String get fiber => 'Ballaststoffe';
|
||||
|
||||
@override
|
||||
String get productAddedToShelf => 'Zum Vorrat hinzugefügt';
|
||||
}
|
||||
|
||||
@@ -473,4 +473,68 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get processingProducts => 'Processing...';
|
||||
|
||||
@override
|
||||
String get clearAllProducts => 'Clear all';
|
||||
|
||||
@override
|
||||
String get clearAllConfirmTitle => 'Clear all products?';
|
||||
|
||||
@override
|
||||
String get clearAllConfirmMessage =>
|
||||
'All products will be permanently deleted.';
|
||||
|
||||
@override
|
||||
String get addManually => 'Manually';
|
||||
|
||||
@override
|
||||
String get scan => 'Scan';
|
||||
|
||||
@override
|
||||
String get addProduct => 'Add';
|
||||
|
||||
@override
|
||||
String get searchProducts => 'Search products';
|
||||
|
||||
@override
|
||||
String get searchProductsHint =>
|
||||
'Type a product name to search or add manually';
|
||||
|
||||
@override
|
||||
String noSearchResults(String query) {
|
||||
return 'No results for \"$query\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String get quantity => 'Quantity';
|
||||
|
||||
@override
|
||||
String get storageDays => 'Storage days';
|
||||
|
||||
@override
|
||||
String get addToShelf => 'Add to pantry';
|
||||
|
||||
@override
|
||||
String get errorGeneric => 'Something went wrong';
|
||||
|
||||
@override
|
||||
String get nutritionOptional => 'Nutrition per 100g (optional)';
|
||||
|
||||
@override
|
||||
String get calories => 'Calories';
|
||||
|
||||
@override
|
||||
String get protein => 'Protein';
|
||||
|
||||
@override
|
||||
String get fat => 'Fat';
|
||||
|
||||
@override
|
||||
String get carbs => 'Carbohydrates';
|
||||
|
||||
@override
|
||||
String get fiber => 'Fiber';
|
||||
|
||||
@override
|
||||
String get productAddedToShelf => 'Added to pantry';
|
||||
}
|
||||
|
||||
@@ -475,4 +475,68 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get processingProducts => 'Procesando...';
|
||||
|
||||
@override
|
||||
String get clearAllProducts => 'Borrar todo';
|
||||
|
||||
@override
|
||||
String get clearAllConfirmTitle => '¿Borrar todos los productos?';
|
||||
|
||||
@override
|
||||
String get clearAllConfirmMessage =>
|
||||
'Todos los productos serán eliminados permanentemente.';
|
||||
|
||||
@override
|
||||
String get addManually => 'Manual';
|
||||
|
||||
@override
|
||||
String get scan => 'Escanear';
|
||||
|
||||
@override
|
||||
String get addProduct => 'Agregar';
|
||||
|
||||
@override
|
||||
String get searchProducts => 'Buscar productos';
|
||||
|
||||
@override
|
||||
String get searchProductsHint =>
|
||||
'Escribe el nombre del producto o agrega manualmente';
|
||||
|
||||
@override
|
||||
String noSearchResults(String query) {
|
||||
return 'Sin resultados para \"$query\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String get quantity => 'Cantidad';
|
||||
|
||||
@override
|
||||
String get storageDays => 'Días de almacenamiento';
|
||||
|
||||
@override
|
||||
String get addToShelf => 'Agregar a despensa';
|
||||
|
||||
@override
|
||||
String get errorGeneric => 'Algo salió mal';
|
||||
|
||||
@override
|
||||
String get nutritionOptional => 'Nutrición por 100g (opcional)';
|
||||
|
||||
@override
|
||||
String get calories => 'Calorías';
|
||||
|
||||
@override
|
||||
String get protein => 'Proteína';
|
||||
|
||||
@override
|
||||
String get fat => 'Grasas';
|
||||
|
||||
@override
|
||||
String get carbs => 'Carbohidratos';
|
||||
|
||||
@override
|
||||
String get fiber => 'Fibra';
|
||||
|
||||
@override
|
||||
String get productAddedToShelf => 'Agregado a la despensa';
|
||||
}
|
||||
|
||||
@@ -476,4 +476,68 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get processingProducts => 'Traitement...';
|
||||
|
||||
@override
|
||||
String get clearAllProducts => 'Tout effacer';
|
||||
|
||||
@override
|
||||
String get clearAllConfirmTitle => 'Effacer tous les produits ?';
|
||||
|
||||
@override
|
||||
String get clearAllConfirmMessage =>
|
||||
'Tous les produits seront définitivement supprimés.';
|
||||
|
||||
@override
|
||||
String get addManually => 'Manuellement';
|
||||
|
||||
@override
|
||||
String get scan => 'Scanner';
|
||||
|
||||
@override
|
||||
String get addProduct => 'Ajouter';
|
||||
|
||||
@override
|
||||
String get searchProducts => 'Rechercher des produits';
|
||||
|
||||
@override
|
||||
String get searchProductsHint =>
|
||||
'Tapez un nom de produit ou ajoutez manuellement';
|
||||
|
||||
@override
|
||||
String noSearchResults(String query) {
|
||||
return 'Aucun résultat pour \"$query\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String get quantity => 'Quantité';
|
||||
|
||||
@override
|
||||
String get storageDays => 'Jours de conservation';
|
||||
|
||||
@override
|
||||
String get addToShelf => 'Ajouter au garde-manger';
|
||||
|
||||
@override
|
||||
String get errorGeneric => 'Une erreur est survenue';
|
||||
|
||||
@override
|
||||
String get nutritionOptional => 'Nutrition pour 100g (facultatif)';
|
||||
|
||||
@override
|
||||
String get calories => 'Calories';
|
||||
|
||||
@override
|
||||
String get protein => 'Protéines';
|
||||
|
||||
@override
|
||||
String get fat => 'Graisses';
|
||||
|
||||
@override
|
||||
String get carbs => 'Glucides';
|
||||
|
||||
@override
|
||||
String get fiber => 'Fibres';
|
||||
|
||||
@override
|
||||
String get productAddedToShelf => 'Ajouté au garde-manger';
|
||||
}
|
||||
|
||||
@@ -474,4 +474,68 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get processingProducts => 'प्रोसेस हो रहा है...';
|
||||
|
||||
@override
|
||||
String get clearAllProducts => 'सब साफ करें';
|
||||
|
||||
@override
|
||||
String get clearAllConfirmTitle => 'सभी उत्पाद साफ करें?';
|
||||
|
||||
@override
|
||||
String get clearAllConfirmMessage =>
|
||||
'सभी उत्पाद स्थायी रूप से हटा दिए जाएंगे।';
|
||||
|
||||
@override
|
||||
String get addManually => 'मैन्युअल';
|
||||
|
||||
@override
|
||||
String get scan => 'स्कैन करें';
|
||||
|
||||
@override
|
||||
String get addProduct => 'जोड़ें';
|
||||
|
||||
@override
|
||||
String get searchProducts => 'उत्पाद खोजें';
|
||||
|
||||
@override
|
||||
String get searchProductsHint =>
|
||||
'उत्पाद का नाम टाइप करें या मैन्युअल रूप से जोड़ें';
|
||||
|
||||
@override
|
||||
String noSearchResults(String query) {
|
||||
return '\"$query\" के लिए कोई परिणाम नहीं';
|
||||
}
|
||||
|
||||
@override
|
||||
String get quantity => 'मात्रा';
|
||||
|
||||
@override
|
||||
String get storageDays => 'भंडारण दिन';
|
||||
|
||||
@override
|
||||
String get addToShelf => 'पेंट्री में जोड़ें';
|
||||
|
||||
@override
|
||||
String get errorGeneric => 'कुछ गलत हो गया';
|
||||
|
||||
@override
|
||||
String get nutritionOptional => 'पोषण मूल्य प्रति 100 ग्राम (वैकल्पिक)';
|
||||
|
||||
@override
|
||||
String get calories => 'कैलोरी';
|
||||
|
||||
@override
|
||||
String get protein => 'प्रोटीन';
|
||||
|
||||
@override
|
||||
String get fat => 'वसा';
|
||||
|
||||
@override
|
||||
String get carbs => 'कार्बोहाइड्रेट';
|
||||
|
||||
@override
|
||||
String get fiber => 'फाइबर';
|
||||
|
||||
@override
|
||||
String get productAddedToShelf => 'पेंट्री में जोड़ा गया';
|
||||
}
|
||||
|
||||
@@ -475,4 +475,68 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get processingProducts => 'Elaborazione...';
|
||||
|
||||
@override
|
||||
String get clearAllProducts => 'Cancella tutto';
|
||||
|
||||
@override
|
||||
String get clearAllConfirmTitle => 'Cancellare tutti i prodotti?';
|
||||
|
||||
@override
|
||||
String get clearAllConfirmMessage =>
|
||||
'Tutti i prodotti verranno eliminati definitivamente.';
|
||||
|
||||
@override
|
||||
String get addManually => 'Manualmente';
|
||||
|
||||
@override
|
||||
String get scan => 'Scansiona';
|
||||
|
||||
@override
|
||||
String get addProduct => 'Aggiungi';
|
||||
|
||||
@override
|
||||
String get searchProducts => 'Cerca prodotti';
|
||||
|
||||
@override
|
||||
String get searchProductsHint =>
|
||||
'Digita il nome del prodotto o aggiungi manualmente';
|
||||
|
||||
@override
|
||||
String noSearchResults(String query) {
|
||||
return 'Nessun risultato per \"$query\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String get quantity => 'Quantità';
|
||||
|
||||
@override
|
||||
String get storageDays => 'Giorni di conservazione';
|
||||
|
||||
@override
|
||||
String get addToShelf => 'Aggiungi alla dispensa';
|
||||
|
||||
@override
|
||||
String get errorGeneric => 'Qualcosa è andato storto';
|
||||
|
||||
@override
|
||||
String get nutritionOptional => 'Valori nutrizionali per 100g (opzionale)';
|
||||
|
||||
@override
|
||||
String get calories => 'Calorie';
|
||||
|
||||
@override
|
||||
String get protein => 'Proteine';
|
||||
|
||||
@override
|
||||
String get fat => 'Grassi';
|
||||
|
||||
@override
|
||||
String get carbs => 'Carboidrati';
|
||||
|
||||
@override
|
||||
String get fiber => 'Fibre';
|
||||
|
||||
@override
|
||||
String get productAddedToShelf => 'Aggiunto alla dispensa';
|
||||
}
|
||||
|
||||
@@ -470,4 +470,66 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get processingProducts => '処理中...';
|
||||
|
||||
@override
|
||||
String get clearAllProducts => 'すべてクリア';
|
||||
|
||||
@override
|
||||
String get clearAllConfirmTitle => 'すべての商品をクリアしますか?';
|
||||
|
||||
@override
|
||||
String get clearAllConfirmMessage => 'すべての商品が完全に削除されます。';
|
||||
|
||||
@override
|
||||
String get addManually => '手動';
|
||||
|
||||
@override
|
||||
String get scan => 'スキャン';
|
||||
|
||||
@override
|
||||
String get addProduct => '追加';
|
||||
|
||||
@override
|
||||
String get searchProducts => '製品を検索';
|
||||
|
||||
@override
|
||||
String get searchProductsHint => '製品名を入力するか手動で追加してください';
|
||||
|
||||
@override
|
||||
String noSearchResults(String query) {
|
||||
return '「$query」の検索結果はありません';
|
||||
}
|
||||
|
||||
@override
|
||||
String get quantity => '数量';
|
||||
|
||||
@override
|
||||
String get storageDays => '保存日数';
|
||||
|
||||
@override
|
||||
String get addToShelf => 'パントリーに追加';
|
||||
|
||||
@override
|
||||
String get errorGeneric => 'エラーが発生しました';
|
||||
|
||||
@override
|
||||
String get nutritionOptional => '100gあたりの栄養素(任意)';
|
||||
|
||||
@override
|
||||
String get calories => 'カロリー';
|
||||
|
||||
@override
|
||||
String get protein => 'タンパク質';
|
||||
|
||||
@override
|
||||
String get fat => '脂質';
|
||||
|
||||
@override
|
||||
String get carbs => '炭水化物';
|
||||
|
||||
@override
|
||||
String get fiber => '食物繊維';
|
||||
|
||||
@override
|
||||
String get productAddedToShelf => 'パントリーに追加しました';
|
||||
}
|
||||
|
||||
@@ -470,4 +470,66 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get processingProducts => '처리 중...';
|
||||
|
||||
@override
|
||||
String get clearAllProducts => '모두 지우기';
|
||||
|
||||
@override
|
||||
String get clearAllConfirmTitle => '모든 제품을 지울까요?';
|
||||
|
||||
@override
|
||||
String get clearAllConfirmMessage => '모든 제품이 영구적으로 삭제됩니다.';
|
||||
|
||||
@override
|
||||
String get addManually => '수동';
|
||||
|
||||
@override
|
||||
String get scan => '스캔';
|
||||
|
||||
@override
|
||||
String get addProduct => '추가';
|
||||
|
||||
@override
|
||||
String get searchProducts => '제품 검색';
|
||||
|
||||
@override
|
||||
String get searchProductsHint => '제품 이름을 입력하거나 수동으로 추가하세요';
|
||||
|
||||
@override
|
||||
String noSearchResults(String query) {
|
||||
return '「$query」에 대한 결과가 없습니다';
|
||||
}
|
||||
|
||||
@override
|
||||
String get quantity => '수량';
|
||||
|
||||
@override
|
||||
String get storageDays => '보관 일수';
|
||||
|
||||
@override
|
||||
String get addToShelf => '저장실에 추가';
|
||||
|
||||
@override
|
||||
String get errorGeneric => '오류가 발생했습니다';
|
||||
|
||||
@override
|
||||
String get nutritionOptional => '100g당 영양성분 (선택사항)';
|
||||
|
||||
@override
|
||||
String get calories => '칼로리';
|
||||
|
||||
@override
|
||||
String get protein => '단백질';
|
||||
|
||||
@override
|
||||
String get fat => '지방';
|
||||
|
||||
@override
|
||||
String get carbs => '탄수화물';
|
||||
|
||||
@override
|
||||
String get fiber => '식이섬유';
|
||||
|
||||
@override
|
||||
String get productAddedToShelf => '저장실에 추가되었습니다';
|
||||
}
|
||||
|
||||
@@ -475,4 +475,68 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get processingProducts => 'Processando...';
|
||||
|
||||
@override
|
||||
String get clearAllProducts => 'Limpar tudo';
|
||||
|
||||
@override
|
||||
String get clearAllConfirmTitle => 'Limpar todos os produtos?';
|
||||
|
||||
@override
|
||||
String get clearAllConfirmMessage =>
|
||||
'Todos os produtos serão excluídos permanentemente.';
|
||||
|
||||
@override
|
||||
String get addManually => 'Manualmente';
|
||||
|
||||
@override
|
||||
String get scan => 'Escanear';
|
||||
|
||||
@override
|
||||
String get addProduct => 'Adicionar';
|
||||
|
||||
@override
|
||||
String get searchProducts => 'Pesquisar produtos';
|
||||
|
||||
@override
|
||||
String get searchProductsHint =>
|
||||
'Digite o nome do produto ou adicione manualmente';
|
||||
|
||||
@override
|
||||
String noSearchResults(String query) {
|
||||
return 'Sem resultados para \"$query\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String get quantity => 'Quantidade';
|
||||
|
||||
@override
|
||||
String get storageDays => 'Dias de armazenamento';
|
||||
|
||||
@override
|
||||
String get addToShelf => 'Adicionar à despensa';
|
||||
|
||||
@override
|
||||
String get errorGeneric => 'Algo deu errado';
|
||||
|
||||
@override
|
||||
String get nutritionOptional => 'Nutrição por 100g (opcional)';
|
||||
|
||||
@override
|
||||
String get calories => 'Calorias';
|
||||
|
||||
@override
|
||||
String get protein => 'Proteína';
|
||||
|
||||
@override
|
||||
String get fat => 'Gorduras';
|
||||
|
||||
@override
|
||||
String get carbs => 'Carboidratos';
|
||||
|
||||
@override
|
||||
String get fiber => 'Fibra';
|
||||
|
||||
@override
|
||||
String get productAddedToShelf => 'Adicionado à despensa';
|
||||
}
|
||||
|
||||
@@ -473,4 +473,69 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get processingProducts => 'Обработка...';
|
||||
|
||||
@override
|
||||
String get clearAllProducts => 'Очистить список';
|
||||
|
||||
@override
|
||||
String get clearAllConfirmTitle => 'Очистить список продуктов?';
|
||||
|
||||
@override
|
||||
String get clearAllConfirmMessage =>
|
||||
'Все продукты будут удалены без возможности восстановления.';
|
||||
|
||||
@override
|
||||
String get addManually => 'Вручную';
|
||||
|
||||
@override
|
||||
String get scan => 'Сканировать';
|
||||
|
||||
@override
|
||||
String get addProduct => 'Добавить';
|
||||
|
||||
@override
|
||||
String get searchProducts => 'Поиск продуктов';
|
||||
|
||||
@override
|
||||
String get searchProductsHint =>
|
||||
'Введите название продукта или добавьте вручную';
|
||||
|
||||
@override
|
||||
String noSearchResults(String query) {
|
||||
return 'Ничего не найдено по запросу \"$query\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String get quantity => 'Количество';
|
||||
|
||||
@override
|
||||
String get storageDays => 'Дней хранения';
|
||||
|
||||
@override
|
||||
String get addToShelf => 'В холодильник';
|
||||
|
||||
@override
|
||||
String get errorGeneric => 'Что-то пошло не так';
|
||||
|
||||
@override
|
||||
String get nutritionOptional =>
|
||||
'Питательная ценность на 100г (необязательно)';
|
||||
|
||||
@override
|
||||
String get calories => 'Калории';
|
||||
|
||||
@override
|
||||
String get protein => 'Белки';
|
||||
|
||||
@override
|
||||
String get fat => 'Жиры';
|
||||
|
||||
@override
|
||||
String get carbs => 'Углеводы';
|
||||
|
||||
@override
|
||||
String get fiber => 'Клетчатка';
|
||||
|
||||
@override
|
||||
String get productAddedToShelf => 'Добавлено в холодильник';
|
||||
}
|
||||
|
||||
@@ -469,4 +469,66 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get processingProducts => '处理中...';
|
||||
|
||||
@override
|
||||
String get clearAllProducts => '清空列表';
|
||||
|
||||
@override
|
||||
String get clearAllConfirmTitle => '清空所有产品?';
|
||||
|
||||
@override
|
||||
String get clearAllConfirmMessage => '所有产品将被永久删除。';
|
||||
|
||||
@override
|
||||
String get addManually => '手动';
|
||||
|
||||
@override
|
||||
String get scan => '扫描';
|
||||
|
||||
@override
|
||||
String get addProduct => '添加';
|
||||
|
||||
@override
|
||||
String get searchProducts => '搜索产品';
|
||||
|
||||
@override
|
||||
String get searchProductsHint => '输入产品名称搜索或手动添加';
|
||||
|
||||
@override
|
||||
String noSearchResults(String query) {
|
||||
return '未找到$query的结果';
|
||||
}
|
||||
|
||||
@override
|
||||
String get quantity => '数量';
|
||||
|
||||
@override
|
||||
String get storageDays => '保存天数';
|
||||
|
||||
@override
|
||||
String get addToShelf => '添加到储藏室';
|
||||
|
||||
@override
|
||||
String get errorGeneric => '出错了';
|
||||
|
||||
@override
|
||||
String get nutritionOptional => '每100克营养成分(可选)';
|
||||
|
||||
@override
|
||||
String get calories => '卡路里';
|
||||
|
||||
@override
|
||||
String get protein => '蛋白质';
|
||||
|
||||
@override
|
||||
String get fat => '脂肪';
|
||||
|
||||
@override
|
||||
String get carbs => '碳水化合物';
|
||||
|
||||
@override
|
||||
String get fiber => '膳食纤维';
|
||||
|
||||
@override
|
||||
String get productAddedToShelf => '已添加到储藏室';
|
||||
}
|
||||
|
||||
@@ -172,5 +172,32 @@
|
||||
"jobTypeReceipt": "Recibo",
|
||||
"jobTypeProducts": "Produtos",
|
||||
"scanSubmitting": "Enviando...",
|
||||
"processingProducts": "Processando..."
|
||||
"processingProducts": "Processando...",
|
||||
"clearAllProducts": "Limpar tudo",
|
||||
"clearAllConfirmTitle": "Limpar todos os produtos?",
|
||||
"clearAllConfirmMessage": "Todos os produtos serão excluídos permanentemente.",
|
||||
"addManually": "Manualmente",
|
||||
"scan": "Escanear",
|
||||
"addProduct": "Adicionar",
|
||||
"searchProducts": "Pesquisar produtos",
|
||||
"searchProductsHint": "Digite o nome do produto ou adicione manualmente",
|
||||
"noSearchResults": "Sem resultados para \"{query}\"",
|
||||
"@noSearchResults": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"quantity": "Quantidade",
|
||||
"storageDays": "Dias de armazenamento",
|
||||
"addToShelf": "Adicionar à despensa",
|
||||
"errorGeneric": "Algo deu errado",
|
||||
"nutritionOptional": "Nutrição por 100g (opcional)",
|
||||
"calories": "Calorias",
|
||||
"protein": "Proteína",
|
||||
"fat": "Gorduras",
|
||||
"carbs": "Carboidratos",
|
||||
"fiber": "Fibra",
|
||||
"productAddedToShelf": "Adicionado à despensa"
|
||||
}
|
||||
|
||||
@@ -172,5 +172,32 @@
|
||||
"jobTypeReceipt": "Чек",
|
||||
"jobTypeProducts": "Продукты",
|
||||
"scanSubmitting": "Отправка...",
|
||||
"processingProducts": "Обработка..."
|
||||
"processingProducts": "Обработка...",
|
||||
"clearAllProducts": "Очистить список",
|
||||
"clearAllConfirmTitle": "Очистить список продуктов?",
|
||||
"clearAllConfirmMessage": "Все продукты будут удалены без возможности восстановления.",
|
||||
"addManually": "Вручную",
|
||||
"scan": "Сканировать",
|
||||
"addProduct": "Добавить",
|
||||
"searchProducts": "Поиск продуктов",
|
||||
"searchProductsHint": "Введите название продукта или добавьте вручную",
|
||||
"noSearchResults": "Ничего не найдено по запросу \"{query}\"",
|
||||
"@noSearchResults": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"quantity": "Количество",
|
||||
"storageDays": "Дней хранения",
|
||||
"addToShelf": "В холодильник",
|
||||
"errorGeneric": "Что-то пошло не так",
|
||||
"nutritionOptional": "Питательная ценность на 100г (необязательно)",
|
||||
"calories": "Калории",
|
||||
"protein": "Белки",
|
||||
"fat": "Жиры",
|
||||
"carbs": "Углеводы",
|
||||
"fiber": "Клетчатка",
|
||||
"productAddedToShelf": "Добавлено в холодильник"
|
||||
}
|
||||
|
||||
@@ -172,5 +172,32 @@
|
||||
"jobTypeReceipt": "收据",
|
||||
"jobTypeProducts": "产品",
|
||||
"scanSubmitting": "提交中...",
|
||||
"processingProducts": "处理中..."
|
||||
"processingProducts": "处理中...",
|
||||
"clearAllProducts": "清空列表",
|
||||
"clearAllConfirmTitle": "清空所有产品?",
|
||||
"clearAllConfirmMessage": "所有产品将被永久删除。",
|
||||
"addManually": "手动",
|
||||
"scan": "扫描",
|
||||
"addProduct": "添加",
|
||||
"searchProducts": "搜索产品",
|
||||
"searchProductsHint": "输入产品名称搜索或手动添加",
|
||||
"noSearchResults": "未找到{query}的结果",
|
||||
"@noSearchResults": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"quantity": "数量",
|
||||
"storageDays": "保存天数",
|
||||
"addToShelf": "添加到储藏室",
|
||||
"errorGeneric": "出错了",
|
||||
"nutritionOptional": "每100克营养成分(可选)",
|
||||
"calories": "卡路里",
|
||||
"protein": "蛋白质",
|
||||
"fat": "脂肪",
|
||||
"carbs": "碳水化合物",
|
||||
"fiber": "膳食纤维",
|
||||
"productAddedToShelf": "已添加到储藏室"
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ class CatalogProduct {
|
||||
final double? fatPer100g;
|
||||
@JsonKey(name: 'carbs_per_100g')
|
||||
final double? carbsPer100g;
|
||||
@JsonKey(name: 'fiber_per_100g')
|
||||
final double? fiberPer100g;
|
||||
|
||||
const CatalogProduct({
|
||||
required this.id,
|
||||
@@ -37,6 +39,7 @@ class CatalogProduct {
|
||||
this.proteinPer100g,
|
||||
this.fatPer100g,
|
||||
this.carbsPer100g,
|
||||
this.fiberPer100g,
|
||||
});
|
||||
|
||||
/// Display name is the server-resolved canonical name (language-aware from backend).
|
||||
|
||||
@@ -19,6 +19,7 @@ CatalogProduct _$CatalogProductFromJson(Map<String, dynamic> json) =>
|
||||
proteinPer100g: (json['protein_per_100g'] as num?)?.toDouble(),
|
||||
fatPer100g: (json['fat_per_100g'] as num?)?.toDouble(),
|
||||
carbsPer100g: (json['carbs_per_100g'] as num?)?.toDouble(),
|
||||
fiberPer100g: (json['fiber_per_100g'] as num?)?.toDouble(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$CatalogProductToJson(CatalogProduct instance) =>
|
||||
@@ -34,4 +35,5 @@ Map<String, dynamic> _$CatalogProductToJson(CatalogProduct instance) =>
|
||||
'protein_per_100g': instance.proteinPer100g,
|
||||
'fat_per_100g': instance.fatPer100g,
|
||||
'carbs_per_100g': instance.carbsPer100g,
|
||||
'fiber_per_100g': instance.fiberPer100g,
|
||||
};
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// This file is intentionally empty.
|
||||
// saved_recipe.dart now uses a manually written fromJson/toJson.
|
||||
Reference in New Issue
Block a user