Skip to content

Commit

Permalink
Merge pull request #136 from simonoppowa/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
simonoppowa authored Jan 14, 2025
2 parents a1d5a16 + dc57091 commit 8cd04dc
Show file tree
Hide file tree
Showing 17 changed files with 176 additions and 56 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ See [Data Protection](https://www.iubenda.com/privacy-policy/53501884)

## TODOs
- Add serving sizes to meals
~~- Add Imperial unit support~~
- ~~Add Imperial unit support~~
- Add support for Material You themes

## Contribution
Expand Down
57 changes: 41 additions & 16 deletions lib/core/presentation/widgets/meal_value_unit_text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,63 @@ import 'package:opennutritracker/generated/l10n.dart';
class MealValueUnitText extends StatelessWidget {
final double value;
final MealEntity meal;
final String? displayUnit;
final bool usesImperialUnits;
final TextStyle? textStyle;
final String? prefix;

const MealValueUnitText(
{super.key,
required this.value,
required this.meal,
required this.usesImperialUnits,
this.textStyle,
this.prefix = ''});
const MealValueUnitText({
super.key,
required this.value,
required this.meal,
this.displayUnit,
required this.usesImperialUnits,
this.textStyle,
this.prefix = '',
});

@override
Widget build(BuildContext context) {
final mealUnit = meal.mealUnit ?? S.of(context).gramMilliliterUnit;
final displayUnit = _convertUnit(context, mealUnit);
final displayValue = _convertValue(value, mealUnit);
final unitToDisplay = displayUnit ?? _convertUnit(context, mealUnit);
final convertedValue = _convertValue(value, mealUnit, unitToDisplay);

return Text(
'$prefix${_formatValue(displayValue)} $displayUnit',
'$prefix${_formatValue(convertedValue)} $unitToDisplay',
style: textStyle,
overflow: TextOverflow.ellipsis,
maxLines: 1,
);
}

double _convertValue(double value, String unit) {
switch (unit) {
case 'g':
return usesImperialUnits ? UnitCalc.gToOz(value) : value;
case 'ml':
return usesImperialUnits ? UnitCalc.mlToFlOz(value) : value;
double _convertValue(double value, String fromUnit, String toUnit) {
// If units are the same, no conversion needed
if (fromUnit == toUnit) return value;

// Convert to base unit first (g or ml)
double baseValue = _convertToBaseUnit(value, fromUnit);

// Then convert from base unit to target unit
return _convertFromBaseUnit(baseValue, toUnit);
}

double _convertToBaseUnit(double value, String fromUnit) {
switch (fromUnit) {
case 'oz':
return UnitCalc.ozToG(value);
case 'fl oz' || 'fl.oz':
return UnitCalc.flOzToMl(value);
default:
return value;
}
}

double _convertFromBaseUnit(double value, String toUnit) {
switch (toUnit) {
case 'oz':
return UnitCalc.gToOz(value);
case 'fl oz' || 'fl.oz':
return UnitCalc.mlToFlOz(value);
default:
return value;
}
Expand Down
46 changes: 46 additions & 0 deletions lib/core/utils/calc/unit_calc.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:opennutritracker/core/utils/extensions.dart';
import 'package:opennutritracker/generated/l10n.dart';

class UnitCalc {
static double cmToInches(double cm) {
Expand Down Expand Up @@ -44,4 +46,48 @@ class UnitCalc {
static double flOzToMl(double flOz) {
return flOz * 29.5735;
}

static double metricToImperialValue(double metricValue, String unit) {
switch (unit) {
case 'g':
return gToOz(metricValue);
case 'ml':
return mlToFlOz(metricValue);
default:
return metricValue;
}
}

static double imperialToMetricValue(double imperialValue, String unit) {
switch (unit) {
case 'oz':
return ozToG(imperialValue);
case 'fl oz' || 'fl.oz':
return flOzToMl(imperialValue);
default:
return imperialValue;
}
}

static String metricToImperialUnit(BuildContext context, String unit) {
switch (unit) {
case 'g':
return S.of(context).ozUnit;
case 'ml':
return S.of(context).flOzUnit;
default:
return unit;
}
}

static String imperialToMetricUnit(BuildContext context, String unit) {
switch (unit) {
case 'oz':
return S.of(context).gramUnit;
case 'fl oz' || 'fl.oz':
return S.of(context).milliliterUnit;
default:
return unit;
}
}
}
2 changes: 1 addition & 1 deletion lib/features/edit_meal/presentation/edit_meal_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ class _EditMealScreenState extends State<EditMealScreen> {
decoration: InputDecoration(
labelText: _usesImperialUnits
? S.of(context).servingSizeLabelImperial
: S.of(context).servingSizeLabel,
: S.of(context).servingSizeLabelMetric,
border: const OutlineInputBorder()),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
),
Expand Down
7 changes: 6 additions & 1 deletion lib/features/meal_detail/meal_detail_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ class _MealDetailScreenState extends State<MealDetailScreen> {
MealValueUnitText(
value: double.parse(totalQuantity),
meal: meal,
displayUnit: selectedUnit,
usesImperialUnits: _usesImperialUnits,
textStyle: Theme.of(context).textTheme.bodyMedium,
prefix: ' / ',
Expand All @@ -246,7 +247,11 @@ class _MealDetailScreenState extends State<MealDetailScreen> {
),
const Divider(),
const SizedBox(height: 16.0),
MealDetailNutrimentsTable(product: meal),
MealDetailNutrimentsTable(
product: meal,
usesImperialUnits: _usesImperialUnits,
servingQuantity: meal.servingQuantity,
servingUnit: meal.servingUnit),
const SizedBox(height: 32.0),
MealInfoButton(url: meal.url, source: meal.source),
meal.source == MealSourceEntity.off
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ class MealDetailBloc extends Bloc<MealDetailEvent, MealDetailState> {
final fatPerUnit = (event.meal.nutriments.fatPerUnit ?? 0);
final proteinPerUnit = (event.meal.nutriments.proteinsPerUnit ?? 0);

final quantity = double.parse(selectedTotalQuantity);
final quantity =
double.parse(selectedTotalQuantity.replaceAll(',', '.'));

// Convert imperial quantity to metric
double convertedQuantity = quantity;
Expand All @@ -70,7 +71,7 @@ class MealDetailBloc extends Bloc<MealDetailEvent, MealDetailState> {

void addIntake(BuildContext context, String unit, String amountText,
IntakeTypeEntity type, MealEntity meal, DateTime day) async {
final quantity = double.parse(amountText);
final quantity = double.parse(amountText.replaceAll(',', '.'));

final intakeEntity = IntakeEntity(
id: IdGenerator.getUniqueID(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:opennutritracker/core/domain/entity/intake_type_entity.dart';
import 'package:opennutritracker/core/utils/custom_text_input_formatter.dart';
import 'package:opennutritracker/core/utils/locator.dart';
import 'package:opennutritracker/core/utils/navigation_options.dart';
import 'package:opennutritracker/features/add_meal/domain/entity/meal_entity.dart';
Expand Down Expand Up @@ -68,9 +68,12 @@ class MealDetailBottomSheet extends StatelessWidget {
quantityTextController.text,
selectedUnit);
}),
keyboardType: TextInputType.number,
inputFormatters:
CustomTextInputFormatter.doubleOnly(),
keyboardType: TextInputType.numberWithOptions(
decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'^\d+([.,]\d{0,2})?$'))
],
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: S.of(context).quantityLabel,
Expand All @@ -85,10 +88,11 @@ class MealDetailBottomSheet extends StatelessWidget {
border: const OutlineInputBorder(),
labelText: S.of(context).unitLabel),
items: <DropdownMenuItem<String>>[
if (product.isSolid && !product.isLiquid)
..._getSolidUnitDropdownItems(context)
else if (product.isLiquid &&
!product.isSolid)
if (product.isSolid ||
!product.isLiquid && !product.isSolid)
..._getSolidUnitDropdownItems(context),
if (product.isLiquid ||
!product.isLiquid && !product.isSolid)
..._getLiquidUnitDropdownItems(context),
..._getOtherDropdownItems(context)
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,16 @@ import 'package:opennutritracker/generated/l10n.dart';

class MealDetailNutrimentsTable extends StatelessWidget {
final MealEntity product;
final bool usesImperialUnits;
final double? servingQuantity;
final String? servingUnit;

const MealDetailNutrimentsTable({super.key, required this.product});
const MealDetailNutrimentsTable(
{super.key,
required this.product,
required this.usesImperialUnits,
this.servingQuantity,
this.servingUnit});

@override
Widget build(BuildContext context) {
Expand All @@ -18,6 +26,10 @@ class MealDetailNutrimentsTable extends StatelessWidget {
?.copyWith(fontWeight: FontWeight.bold) ??
const TextStyle();

final headerText = usesImperialUnits && servingQuantity != null
? "${S.of(context).perServingLabel} (${servingQuantity!.roundToPrecision(1)}${servingUnit ?? 'g/ml'})"
: S.of(context).per100gmlLabel;

return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expand All @@ -27,45 +39,54 @@ class MealDetailNutrimentsTable extends StatelessWidget {
Table(
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
border: TableBorder.all(
color:
Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5)),
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.5)),
children: <TableRow>[
_getNutrimentsTableRow(
"", S.of(context).per100gmlLabel, textStyleBold),
_getNutrimentsTableRow("", headerText, textStyleBold),
_getNutrimentsTableRow(
S.of(context).energyLabel,
"${product.nutriments.energyKcal100?.toInt() ?? "?"} ${S.of(context).kcalLabel}",
"${_adjustValueForServing(product.nutriments.energyKcal100?.toDouble() ?? 0).toInt()} ${S.of(context).kcalLabel}",
textStyleNormal),
_getNutrimentsTableRow(
S.of(context).fatLabel,
"${product.nutriments.fat100?.roundToPrecision(2) ?? "?"}g",
"${_adjustValueForServing(product.nutriments.fat100 ?? 0).roundToPrecision(2)}g",
textStyleNormal),
_getNutrimentsTableRow(
' ${S.of(context).saturatedFatLabel}',
"${product.nutriments.saturatedFat100?.roundToPrecision(2) ?? "?"}g",
"${_adjustValueForServing(product.nutriments.saturatedFat100 ?? 0).roundToPrecision(2)}g",
textStyleNormal),
_getNutrimentsTableRow(
S.of(context).carbohydrateLabel,
"${product.nutriments.carbohydrates100?.roundToPrecision(2) ?? "?"}g",
"${_adjustValueForServing(product.nutriments.carbohydrates100 ?? 0).roundToPrecision(2)}g",
textStyleNormal),
_getNutrimentsTableRow(
' ${S.of(context).sugarLabel}',
"${product.nutriments.sugars100?.roundToPrecision(2) ?? "?"}g",
"${_adjustValueForServing(product.nutriments.sugars100 ?? 0).roundToPrecision(2)}g",
textStyleNormal),
_getNutrimentsTableRow(
S.of(context).fiberLabel,
"${product.nutriments.fiber100?.roundToPrecision(2) ?? "?"}g",
"${_adjustValueForServing(product.nutriments.fiber100 ?? 0).roundToPrecision(2)}g",
textStyleNormal),
_getNutrimentsTableRow(
S.of(context).proteinLabel,
"${product.nutriments.proteins100?.roundToPrecision(2) ?? "?"}g",
"${_adjustValueForServing(product.nutriments.proteins100 ?? 0).roundToPrecision(2)}g",
textStyleNormal)
],
)
],
);
}

double _adjustValueForServing(double value) {
if (!usesImperialUnits || servingQuantity == null) {
return value;
}
// Calculate per serving value based on 100g reference
return (value * servingQuantity!) / 100;
}

TableRow _getNutrimentsTableRow(
String label, String quantityString, TextStyle textStyle) {
return TableRow(children: <Widget>[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class _OnboardingSecondPageBodyState extends State<OnboardingSecondPageBody> {
child: TextFormField(
onChanged: (text) {
if (_heightFormKey.currentState!.validate()) {
_parsedHeight = double.tryParse(text);
_parsedHeight = double.tryParse(text.replaceAll(',', '.'));
checkCorrectInput();
} else {
_parsedHeight = null;
Expand All @@ -59,11 +59,12 @@ class _OnboardingSecondPageBodyState extends State<OnboardingSecondPageBody> {
borderRadius: BorderRadius.circular(10),
),
),
keyboardType: TextInputType.number,
keyboardType: TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
!_isImperialSelected
? FilteringTextInputFormatter.digitsOnly
: FilteringTextInputFormatter.allow(RegExp(r'[0-9.]'))
: FilteringTextInputFormatter.allow(
RegExp(r'^\d+([.,]\d{0,1})?$'))
]),
),
Padding(
Expand Down Expand Up @@ -163,7 +164,7 @@ class _OnboardingSecondPageBodyState extends State<OnboardingSecondPageBody> {

if (_isImperialSelected) {
// Regex for feet and inches
if (value.isEmpty || !RegExp(r'^[0-9]+(\.[0-9]+)?$').hasMatch(value)) {
if (value.isEmpty || !RegExp(r'^[0-9]+([.,][0-9])?$').hasMatch(value)) {
return S.of(context).onboardingWrongHeightLabel;
} else {
return null;
Expand Down
5 changes: 4 additions & 1 deletion lib/generated/intl/messages_de.dart
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,7 @@ class MessageLookup extends MessageLookupByLibrary {
"palVeryActiveLabel":
MessageLookupByLibrary.simpleMessage("Sehr aktiv"),
"per100gmlLabel": MessageLookupByLibrary.simpleMessage("Pro 100 g/ml"),
"perServingLabel": MessageLookupByLibrary.simpleMessage("Pro Portion"),
"privacyPolicyLabel":
MessageLookupByLibrary.simpleMessage("Datenschutzrichtlinie"),
"profileLabel": MessageLookupByLibrary.simpleMessage("Profil"),
Expand Down Expand Up @@ -611,7 +612,9 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Gewicht auswählen"),
"sendAnonymousUserData": MessageLookupByLibrary.simpleMessage(
"Anonyme Nutzungsdaten senden?"),
"servingSizeLabel":
"servingSizeLabelImperial":
MessageLookupByLibrary.simpleMessage("Portionsgröße (oz/fl oz)"),
"servingSizeLabelMetric":
MessageLookupByLibrary.simpleMessage("Portionsgröße (g/ml)"),
"settingAboutLabel": MessageLookupByLibrary.simpleMessage("Über"),
"settingFeedbackLabel":
Expand Down
Loading

0 comments on commit 8cd04dc

Please sign in to comment.