diet-guard/app/lib/models/meal_item.dart
Krzysztof kuhy Rudnicki ee5a7660cb Add Flutter companion app skeleton with local meal logging
Milestone 1 of the diet-app-as-wise-balloon plan: a phone-native way to
log meals away from the PC, sharing the exact on-disk JSON shape
diet_guard already uses (same field names, no translation layer).

- lib/models/: 1:1 Dart mirrors of the Python dataclasses (Nutrition,
  FoodEntry, MealItem, FoodBankRecord, Slot), including the per-100g/
  amount-eaten portion scaling that matches _resolve.resolve_nutrition's
  semantics exactly.
- lib/services/log_storage_service.dart: plain-JSON persistence to
  food_log.json's exact shape (no sqflite -- the canonical format
  already is this JSON).
- lib/services/foodbank_service.dart: ports _foodbank.py's upsert/fuzzy
  search logic for autocomplete.
- lib/screens/: log_meal_screen.dart (single-item logging) and
  meal_builder_screen.dart (composite multi-item meals, logging full
  per-component macros via the new components field).

Verified end-to-end on a physical device (BL9000): built, installed,
logged a real meal through the UI. 77 Flutter tests passing, `flutter
analyze` clean against very_good_analysis.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU3f5KQ1GHXsbbSecfVEyF
2026-06-22 18:22:42 +02:00

62 lines
2.1 KiB
Dart

/// Composite "meal" support, mirroring diet_guard's `_meal.py`.
library;
import 'package:diet_guard_app/models/meal_component.dart';
import 'package:diet_guard_app/models/nutrition.dart';
/// Provenance stamped on a summed meal, matching `_meal.MEAL_SOURCE`.
const String mealSource = 'meal';
/// One named component of a composite meal, with its own nutrition.
///
/// Mirrors `_meal.MealItem`.
class MealItem {
/// Creates a [MealItem] from a component's name and resolved macros.
const MealItem({required this.name, required this.nutrition});
/// The component's food name (e.g. `"chicken"`).
final String name;
/// The component's resolved macros for the amount eaten.
final Nutrition nutrition;
}
/// Returns the summed nutrition of a meal's [items].
///
/// Every macro and the portion weight are added across the items and
/// rounded to 0.1, and the result is stamped `source: mealSource` so it is
/// distinguishable from a single food. Mirrors `_meal.meal_total`.
Nutrition mealTotal(List<MealItem> items) {
double sumOf(double Function(MealItem) field) {
var total = 0.0;
for (final item in items) {
total += field(item);
}
return double.parse(total.toStringAsFixed(1));
}
return Nutrition(
kcal: sumOf((item) => item.nutrition.kcal),
proteinG: sumOf((item) => item.nutrition.proteinG),
carbsG: sumOf((item) => item.nutrition.carbsG),
fatG: sumOf((item) => item.nutrition.fatG),
grams: sumOf((item) => item.nutrition.grams),
source: mealSource,
);
}
/// Returns a composite meal's per-component log record for [item].
///
/// Carries the component's full macros (not just its name) so a food bank
/// rebuilt purely by replaying the log can recover each component's
/// standalone nutrition, not just the composite's summed total. Mirrors
/// `_meal.item_to_component`.
MealComponent itemToComponent(MealItem item) => MealComponent(
name: item.name,
kcal: item.nutrition.kcal,
proteinG: item.nutrition.proteinG,
carbsG: item.nutrition.carbsG,
fatG: item.nutrition.fatG,
grams: item.nutrition.grams,
);