feat: meal tracking, dish recognition UX improvements, English AI prompts
Backend: - Translate all recognition prompts (receipt, products, dish) from Russian to English - Add lang parameter to Recognizer interface and pass locale.FromContext in handlers - DishResult type uses candidates array for multi-candidate responses Client: - Add meal tracking: diary provider, date selector, meal type model - DishResult parser: backward-compatible with legacy flat format and new candidates format - DishResultScreen: sticky bottom button, full-width portion/meal-type inputs, КБЖУ disclaimer moved under nutrition card, add date field to diary POST body - Recognition prompts now return dish/product names in user's preferred language - Onboarding, profile, home screen visual updates Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,10 +4,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../core/theme/app_colors.dart';
|
||||
import '../../shared/models/meal_type.dart';
|
||||
import '../profile/profile_provider.dart';
|
||||
import '../profile/profile_service.dart';
|
||||
|
||||
const int _totalSteps = 6;
|
||||
const int _totalSteps = 7;
|
||||
|
||||
const List<Color> _stepAccentColors = [
|
||||
Color(0xFF5856D6), // Goal — iOS purple
|
||||
@@ -15,6 +16,7 @@ const List<Color> _stepAccentColors = [
|
||||
Color(0xFFFF9500), // DOB — iOS orange
|
||||
Color(0xFF34C759), // Height + Weight — iOS green
|
||||
Color(0xFFFF2D55), // Activity — iOS pink
|
||||
Color(0xFF30B0C7), // Meal Types — teal
|
||||
Color(0xFFFF6B00), // Calories — deep orange
|
||||
];
|
||||
|
||||
@@ -24,6 +26,7 @@ const List<IconData> _stepIcons = [
|
||||
Icons.cake_outlined,
|
||||
Icons.monitor_weight_outlined,
|
||||
Icons.directions_run,
|
||||
Icons.restaurant_menu,
|
||||
Icons.local_fire_department,
|
||||
];
|
||||
|
||||
@@ -45,6 +48,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
final _heightController = TextEditingController();
|
||||
final _weightController = TextEditingController();
|
||||
String? _activity;
|
||||
List<String> _mealTypes = List<String>.from(kDefaultMealTypeIds);
|
||||
int _dailyCalories = 2000;
|
||||
|
||||
bool _saving = false;
|
||||
@@ -116,7 +120,9 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
weightValue <= 300;
|
||||
case 4: // Activity
|
||||
return _activity != null;
|
||||
case 5: // Calories
|
||||
case 5: // Meal Types — at least one must be selected
|
||||
return _mealTypes.isNotEmpty;
|
||||
case 6: // Calories
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
@@ -127,7 +133,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
if (!_canAdvance()) return;
|
||||
|
||||
// Entering calorie review — pre-calculate based on collected data
|
||||
if (_currentStep == 4) {
|
||||
if (_currentStep == 5) {
|
||||
setState(() => _dailyCalories = _calculateCalories());
|
||||
}
|
||||
|
||||
@@ -166,6 +172,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
gender: _gender,
|
||||
goal: _goal,
|
||||
activity: _activity,
|
||||
mealTypes: _mealTypes,
|
||||
dailyCalories: _dailyCalories,
|
||||
);
|
||||
|
||||
@@ -257,9 +264,19 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
_StepPage(
|
||||
accentColor: _stepAccentColors[5],
|
||||
icon: _stepIcons[5],
|
||||
child: _MealTypesStepContent(
|
||||
selected: _mealTypes,
|
||||
accentColor: _stepAccentColors[5],
|
||||
onChanged: (updated) =>
|
||||
setState(() => _mealTypes = updated),
|
||||
),
|
||||
),
|
||||
_StepPage(
|
||||
accentColor: _stepAccentColors[6],
|
||||
icon: _stepIcons[6],
|
||||
child: _CalorieStepContent(
|
||||
calories: _dailyCalories,
|
||||
accentColor: _stepAccentColors[5],
|
||||
accentColor: _stepAccentColors[6],
|
||||
onChanged: (value) =>
|
||||
setState(() => _dailyCalories = value),
|
||||
),
|
||||
@@ -1108,7 +1125,83 @@ class _ActivityCard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step 6: Calorie review ─────────────────────────────────────
|
||||
// ── Step 6: Meal types ─────────────────────────────────────────
|
||||
|
||||
class _MealTypesStepContent extends StatelessWidget {
|
||||
final List<String> selected;
|
||||
final Color accentColor;
|
||||
final ValueChanged<List<String>> onChanged;
|
||||
|
||||
const _MealTypesStepContent({
|
||||
required this.selected,
|
||||
required this.accentColor,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
void _toggle(String mealTypeId) {
|
||||
final updated = List<String>.from(selected);
|
||||
if (updated.contains(mealTypeId)) {
|
||||
// Keep at least one meal type selected.
|
||||
if (updated.length > 1) updated.remove(mealTypeId);
|
||||
} else {
|
||||
updated.add(mealTypeId);
|
||||
}
|
||||
onChanged(updated);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Ваши приёмы пищи', style: theme.textTheme.headlineSmall),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Выберите, какие приёмы пищи вы хотите отслеживать',
|
||||
style: theme.textTheme.bodyMedium
|
||||
?.copyWith(color: AppColors.textSecondary),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
...kAllMealTypes.map((mealTypeOption) {
|
||||
final isSelected = selected.contains(mealTypeOption.id);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: _SelectableCard(
|
||||
selected: isSelected,
|
||||
accentColor: accentColor,
|
||||
onTap: () => _toggle(mealTypeOption.id),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(mealTypeOption.emoji,
|
||||
style: const TextStyle(fontSize: 24)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
mealTypeOption.label,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: isSelected
|
||||
? accentColor
|
||||
: AppColors.textPrimary,
|
||||
fontWeight: isSelected
|
||||
? FontWeight.w600
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isSelected)
|
||||
Icon(Icons.check_circle, color: accentColor),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step 7: Calorie review ─────────────────────────────────────
|
||||
|
||||
class _CalorieStepContent extends StatelessWidget {
|
||||
final int calories;
|
||||
|
||||
Reference in New Issue
Block a user