Skip to content

Commit

Permalink
Improve the Nutrition Facts screen
Browse files Browse the repository at this point in the history
  • Loading branch information
g123k committed Jul 7, 2023
1 parent 505c0d1 commit b687853
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,13 @@ class _KnowledgePanelTableCardState extends State<KnowledgePanelTableCard> {
return Column(
children: <Widget>[
for (List<Widget> row in rowsWidgets)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: row,
Semantics(
excludeSemantics: true,
value: _buildSemanticsValue(row),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: row,
),
),
if (withPortionCalculator) const Divider(),
if (withPortionCalculator) PortionCalculator(widget.product)
Expand Down Expand Up @@ -253,6 +257,21 @@ class _KnowledgePanelTableCardState extends State<KnowledgePanelTableCard> {
}
}
}

String _buildSemanticsValue(List<Widget> row) {
final StringBuffer buffer = StringBuffer();

for (final Widget widget in row) {
if (widget is TableCellWidget && widget.cell.text.isNotEmpty) {
if (buffer.isNotEmpty) {
buffer.write(' - ');
}
buffer.write(widget.cell.text);
}
}

return buffer.toString();
}
}

class TableCellWidget extends StatefulWidget {
Expand Down
22 changes: 21 additions & 1 deletion packages/smooth_app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1927,10 +1927,30 @@
"@scan_header_compare_button_valid_state_tooltip": {
"description": "Tooltip (message visible with a long-press) on the Compare button on top of the scanner, when there is at least two prodiucts"
},
"portion_calculator_description": "Calculate nutrition facts for a specific quantity",
"portion_calculator_description": "Calculate nutrition facts for a specific quantity:",
"@portion_calculator_description": {
"description": "Sort of title that describes the portion calculator."
},
"portion_calculator_hint": "Quantity in",
"@portion_calculator_hint": {
"description": "Hint to show when a quantity is empty in the portion calculator."
},
"portion_calculator_accessibility": "Input a quantity to calculate nutrition facts",
"@portion_calculator_accessibility": {
"description": "Hint for the acessibility to explain to enter a quantity."
},
"portion_calculator_error": "Please enter a quantity between {min} and {max} g",
"@portion_calculator_error": {
"description": "Error message to explain that the quantity is invalid.",
"placeholders": {
"min": {
"type": "int"
},
"max": {
"type": "int"
}
}
},
"portion_calculator_result_title": "Nutrition facts for {grams} g (or ml)",
"@portion_calculator_result_title": {
"description": "Title of the results of the portion calculator.",
Expand Down
159 changes: 112 additions & 47 deletions packages/smooth_app/lib/pages/product/portion_calculator.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:smooth_app/generic_lib/design_constants.dart';
import 'package:smooth_app/generic_lib/dialogs/smooth_alert_dialog.dart';
import 'package:smooth_app/generic_lib/duration_constants.dart';
import 'package:smooth_app/pages/product/ordered_nutrients_cache.dart';
import 'package:smooth_app/pages/product/portion_helper.dart';

Expand All @@ -18,73 +19,129 @@ class PortionCalculator extends StatefulWidget {
}

class _PortionCalculatorState extends State<PortionCalculator> {
/// Typical size needed for [CupertinoPicker].
static const double _kItemExtent = DEFAULT_ICON_SIZE;

/// Max value for the picker.
static const int _maxGrams = 1000;
static const int _minGrams = 10;

/// Value for the picker, with an initial value.
int _grams = 100;

late final FixedExtentScrollController _controllerUnit;
final TextEditingController _quantityController = TextEditingController(
text: '100',
);

@override
void initState() {
super.initState();
_controllerUnit = FixedExtentScrollController(
initialItem: _fromGramsToIndex(_grams),
);
_quantityController.addListener(_onInputChanged);
}

void _onInputChanged() => setState(() {});

@override
Widget build(BuildContext context) {
final MediaQueryData data = MediaQuery.of(context);
final AppLocalizations appLocalizations = AppLocalizations.of(context);
final bool isQuantityValid = _isInputValid();

return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
appLocalizations.portion_calculator_description,
textAlign: TextAlign.center,
// We have to manually add a Semantic node here, otherwise the text is
// not read
Semantics(
value: appLocalizations.portion_calculator_description,
excludeSemantics: true,
child: Text(
appLocalizations.portion_calculator_description,
style: Theme.of(context).textTheme.headlineMedium,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
SizedBox(
width: _kItemExtent * 2,
height: _kItemExtent * 5,
child: CupertinoPicker.builder(
scrollController: _controllerUnit,
itemExtent: _kItemExtent,
onSelectedItemChanged: (final int index) =>
_grams = _fromIndexToGrams(index),
childCount: _fromGramsToIndex(_maxGrams) + 1,
itemBuilder: (final BuildContext context, final int index) =>
Text(
'${_fromIndexToGrams(index)}',
style: Theme.of(context).textTheme.bodyMedium,
const SizedBox(height: MEDIUM_SPACE),
Container(
height: (data.textScaleFactor * (SMALL_SPACE * 2 + 15.0)) * 1.2,
padding: const EdgeInsets.symmetric(horizontal: MEDIUM_SPACE),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: <Widget>[
SizedBox(
width: data.size.width * 0.3,
child: Semantics(
value:
'${_quantityController.text} ${UnitHelper.unitToString(Unit.G)}',
hint: appLocalizations.portion_calculator_accessibility,
textField: true,
excludeSemantics: true,
child: TextField(
controller: _quantityController,
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.allow(RegExp('[0-9]*')),
],
enableSuggestions: false,
style: const TextStyle(letterSpacing: 5.0),
textAlign: TextAlign.center,
decoration: InputDecoration(
suffixText: UnitHelper.unitToString(Unit.G),
filled: true,
border: const OutlineInputBorder(
borderRadius: ANGULAR_BORDER_RADIUS,
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: SMALL_SPACE,
vertical: SMALL_SPACE,
),
hintText: appLocalizations.portion_calculator_hint,
hintStyle: const TextStyle(letterSpacing: 1.0),
),
textInputAction: TextInputAction.search,
onSubmitted: (_) {
if (_isInputValid()) {
_computeAndShow();
}
},
autofocus: false,
),
),
),
),
Text(UnitHelper.unitToString(Unit.G)!),
Padding(
padding: const EdgeInsets.only(left: SMALL_SPACE),
child: ElevatedButton(
onPressed: () async => _computeAndShow(),
child: Text(appLocalizations.calculate),
const SizedBox(width: MEDIUM_SPACE),
AnimatedOpacity(
opacity: isQuantityValid ? 1.0 : 0.5,
duration: SmoothAnimationsDuration.brief,
child: Tooltip(
message: !isQuantityValid
? appLocalizations.portion_calculator_error(
_minGrams,
_maxGrams,
)
: '',
excludeFromSemantics: isQuantityValid,
child: SizedBox(
height: double.infinity,
child: ElevatedButton(
onPressed: isQuantityValid
? () async => _computeAndShow()
: null,
child: Text(appLocalizations.calculate),
),
),
),
),
),
],
],
),
),
],
);
}

int _fromIndexToGrams(final int index) => (index + 1) * 10;

int _fromGramsToIndex(final int grams) => (grams ~/ 10) - 1;
bool _isInputValid() {
try {
final int value = int.parse(_quantityController.text);
return value >= _minGrams && value <= _maxGrams;
} on FormatException catch (_) {
return false;
}
}

/// Computes all the nutrients with a portion factor, and displays a dialog.
Future<void> _computeAndShow() async {
Expand All @@ -100,18 +157,20 @@ class _PortionCalculatorState extends State<PortionCalculator> {
if (!mounted) {
return;
}

final int quantity = int.parse(_quantityController.text);
final PortionHelper helper = PortionHelper(
cache.orderedNutrients.nutrients,
widget.product.nutriments!,
_grams,
quantity,
);
if (helper.isEmpty) {
return;
}
await showDialog<void>(
context: context,
builder: (final BuildContext context) => SmoothAlertDialog(
title: appLocalizations.portion_calculator_result_title(_grams),
title: appLocalizations.portion_calculator_result_title(quantity),
body: Column(
children: List<Widget>.generate(
helper.length,
Expand All @@ -132,4 +191,10 @@ class _PortionCalculatorState extends State<PortionCalculator> {
),
);
}

@override
void dispose() {
_quantityController.addListener(_onInputChanged);
super.dispose();
}
}

0 comments on commit b687853

Please sign in to comment.