mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-11 21:13:53 +01:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWYndZhdUrRGFEjBazQ
178 lines
5.6 KiB
Dart
178 lines
5.6 KiB
Dart
/// A row of macro entry fields (kcal/protein/carbs/fat/grams), with an
|
|
/// optional reference weight so a label's per-100g macros can be typed
|
|
/// directly and scaled to the amount actually eaten.
|
|
library;
|
|
|
|
import 'package:diet_guard_app/models/nutrition.dart';
|
|
import 'package:diet_guard_app/ui/cues.dart';
|
|
import 'package:diet_guard_app/ui/macro_kind.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
/// Text controllers for one macro-entry row, owned by the calling screen so
|
|
/// it can read/clear/prefill values around the row's lifecycle.
|
|
class MacroControllers {
|
|
/// Creates a fresh set of empty macro controllers.
|
|
MacroControllers()
|
|
: kcal = TextEditingController(),
|
|
protein = TextEditingController(),
|
|
carbs = TextEditingController(),
|
|
fat = TextEditingController(),
|
|
perGrams = TextEditingController(),
|
|
grams = TextEditingController();
|
|
|
|
/// Calories controller.
|
|
final TextEditingController kcal;
|
|
|
|
/// Protein (g) controller.
|
|
final TextEditingController protein;
|
|
|
|
/// Carbohydrate (g) controller.
|
|
final TextEditingController carbs;
|
|
|
|
/// Fat (g) controller.
|
|
final TextEditingController fat;
|
|
|
|
/// Reference weight (g) the typed macros are stated for, e.g. `100` for
|
|
/// a per-100g label. Blank means the macros already describe the full
|
|
/// eaten portion.
|
|
final TextEditingController perGrams;
|
|
|
|
/// Portion weight actually eaten (g). Blank assumes the eaten amount
|
|
/// equals [perGrams].
|
|
final TextEditingController grams;
|
|
|
|
/// Fills every macro field from [nutrition], as a food-bank pick does.
|
|
///
|
|
/// Both [perGrams] and [grams] are set to the suggestion's own weight, so
|
|
/// the row starts out describing exactly the portion that was banked.
|
|
///
|
|
/// Clears first so no field can survive from a previous pick. This one
|
|
/// assignment set happens to cover all six today, but the clear is what
|
|
/// makes that non-load-bearing -- see [clear].
|
|
void fillFrom(Nutrition nutrition) {
|
|
clear();
|
|
kcal.text = nutrition.kcal.toStringAsFixed(0);
|
|
protein.text = nutrition.proteinG.toStringAsFixed(0);
|
|
carbs.text = nutrition.carbsG.toStringAsFixed(0);
|
|
fat.text = nutrition.fatG.toStringAsFixed(0);
|
|
perGrams.text = nutrition.grams.toStringAsFixed(0);
|
|
grams.text = nutrition.grams.toStringAsFixed(0);
|
|
}
|
|
|
|
/// Clears every field's text.
|
|
void clear() {
|
|
kcal.clear();
|
|
protein.clear();
|
|
carbs.clear();
|
|
fat.clear();
|
|
perGrams.clear();
|
|
grams.clear();
|
|
}
|
|
|
|
/// Adds [listener] to every field's controller.
|
|
///
|
|
/// Split out for `log_meal_screen.dart`'s 250-line cap, mirroring [clear]
|
|
/// and [dispose]'s own per-field enumeration.
|
|
void addListenerToAll(VoidCallback listener) {
|
|
kcal.addListener(listener);
|
|
protein.addListener(listener);
|
|
carbs.addListener(listener);
|
|
fat.addListener(listener);
|
|
perGrams.addListener(listener);
|
|
grams.addListener(listener);
|
|
}
|
|
|
|
/// Disposes every controller.
|
|
void dispose() {
|
|
kcal.dispose();
|
|
protein.dispose();
|
|
carbs.dispose();
|
|
fat.dispose();
|
|
perGrams.dispose();
|
|
grams.dispose();
|
|
}
|
|
}
|
|
|
|
/// A row of number-entry fields, each captioned by an icon: calories,
|
|
/// macros, and the
|
|
/// optional reference-weight-vs-eaten-weight split.
|
|
///
|
|
/// Layout mirrors the Python gate's macro section: the reference weight
|
|
/// (`per (g)`) sits on the same line as `kcal` so the user can see at a
|
|
/// glance which portion size the calories describe.
|
|
class MacroInputRow extends StatelessWidget {
|
|
/// Creates a [MacroInputRow] bound to [controllers].
|
|
///
|
|
/// When [compact] is true, all six fields render in a single row with
|
|
/// abbreviated labels instead of the default three stacked rows.
|
|
const MacroInputRow({
|
|
required this.controllers,
|
|
this.compact = false,
|
|
super.key,
|
|
});
|
|
|
|
/// The text controllers this row reads from and writes to.
|
|
final MacroControllers controllers;
|
|
|
|
/// Whether to render all fields in one row with abbreviated labels.
|
|
final bool compact;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final fields = [
|
|
_macroField(MacroKind.perGrams, controllers.perGrams),
|
|
_macroField(MacroKind.kcal, controllers.kcal),
|
|
_macroField(MacroKind.protein, controllers.protein),
|
|
_macroField(MacroKind.carbs, controllers.carbs),
|
|
_macroField(MacroKind.fat, controllers.fat),
|
|
_macroField(MacroKind.eaten, controllers.grams),
|
|
];
|
|
if (compact) {
|
|
return Row(
|
|
children: [
|
|
for (final (i, field) in fields.indexed) ...[
|
|
if (i > 0) const SizedBox(width: 4),
|
|
Expanded(child: field),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
// Reference weight and kcal share a line, then the three macros, then the
|
|
// eaten weight: the same order the Python gate shows them in.
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
SizedBox(width: 96, child: fields[0]),
|
|
const SizedBox(width: 8),
|
|
Expanded(child: fields[1]),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
children: [
|
|
for (final (i, field) in fields.sublist(2, 5).indexed) ...[
|
|
if (i > 0) const SizedBox(width: 8),
|
|
Expanded(child: field),
|
|
],
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
fields[5],
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _macroField(MacroKind kind, TextEditingController controller) {
|
|
return IconField(
|
|
icon: kind.icon,
|
|
iconColor: kind.color,
|
|
semanticLabel: kind.semanticLabel(compact: compact),
|
|
controller: controller,
|
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
dense: true,
|
|
);
|
|
}
|
|
}
|