mirror of
https://github.com/openfoodfacts/smooth-app.git
synced 2025-08-26 11:16:45 +08:00
Impacted files: * `app_en.arb`: removed 2 labels not used anymore * `nutrition_container.dart`: added "no nutrition data" * `nutrition_page_loaded.dart`: refactored the "save" action as in `SimpleInputPage`; added "no nutrition data"; just trying - remove the status bar recoloring; added a save/cancel bottom bar; fixed a bug in `_isEdited` * `product_query.dart`: added field "no nutrition data"
This commit is contained in:
@ -99,6 +99,7 @@ abstract class ProductQuery {
|
||||
ProductField.SERVING_SIZE,
|
||||
ProductField.STORES,
|
||||
ProductField.PACKAGING_QUANTITY,
|
||||
ProductField.NO_NUTRITION_DATA,
|
||||
ProductField.NUTRIMENTS,
|
||||
ProductField.NUTRIENT_LEVELS,
|
||||
ProductField.NUTRIMENT_ENERGY_UNIT,
|
||||
|
@ -44,7 +44,6 @@
|
||||
"@label_web": {},
|
||||
"learnMore": "Learn more",
|
||||
"@learnMore": {},
|
||||
"general_confirmation": "Are you sure?",
|
||||
"unknown": "Unknown",
|
||||
"@unknown": {
|
||||
"description": "Short label for product list view: the compatibility of that product with your preferences is unknown"
|
||||
@ -423,7 +422,6 @@
|
||||
},
|
||||
"nutrition": "Nutrition",
|
||||
"@nutrition": {},
|
||||
"nutrition_page_close_confirmation": "Are you sure you want to close without saving?",
|
||||
"nutrition_facts_photo": "Nutrition facts photo",
|
||||
"@nutrition_facts_photo": {
|
||||
"description": "Button label: For adding a picture of the nutrition facts of a product"
|
||||
|
@ -19,6 +19,7 @@ class NutritionContainer {
|
||||
}
|
||||
_servingSize = product.servingSize;
|
||||
_barcode = product.barcode!;
|
||||
noNutritionData = product.noNutritionData ?? false;
|
||||
}
|
||||
|
||||
static const String _energyId = 'energy';
|
||||
@ -66,6 +67,8 @@ class NutritionContainer {
|
||||
|
||||
late final String _barcode;
|
||||
|
||||
late bool noNutritionData;
|
||||
|
||||
/// Returns the not interesting nutrients, for a "Please add me!" list.
|
||||
Iterable<OrderedNutrient> getLeftoverNutrients() => _nutrients.where(
|
||||
(final OrderedNutrient element) => _isNotRelevant(element),
|
||||
@ -90,6 +93,7 @@ class NutritionContainer {
|
||||
/// Returns a [Product] with only nutrients data.
|
||||
Product getProduct() => Product(
|
||||
barcode: _barcode,
|
||||
noNutritionData: noNutritionData,
|
||||
nutriments: _getNutriments(),
|
||||
servingSize: _servingSize,
|
||||
);
|
||||
|
@ -39,7 +39,7 @@ class _NutritionPageLoadedState extends State<NutritionPageLoaded> {
|
||||
late final NumberFormat _numberFormat;
|
||||
late final NutritionContainer _nutritionContainer;
|
||||
|
||||
bool _unspecified = false; // TODO(monsieurtanuki): fetch that data from API?
|
||||
late bool _noNutritionData;
|
||||
// If true then serving, if false then 100g.
|
||||
bool _servingOr100g = false;
|
||||
|
||||
@ -60,11 +60,9 @@ class _NutritionPageLoadedState extends State<NutritionPageLoaded> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_product = widget.product;
|
||||
_nutritionContainer = NutritionContainer(
|
||||
orderedNutrients: widget.orderedNutrients,
|
||||
product: _product,
|
||||
);
|
||||
_nutritionContainer = _getFreshContainer();
|
||||
_numberFormat = NumberFormat('####0.#####', ProductQuery.getLocaleString());
|
||||
_noNutritionData = _product.noNutritionData ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -77,52 +75,63 @@ class _NutritionPageLoadedState extends State<NutritionPageLoaded> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final AppLocalizations localizations = AppLocalizations.of(context);
|
||||
final LocalDatabase localDatabase = context.read<LocalDatabase>();
|
||||
final AppLocalizations appLocalizations = AppLocalizations.of(context);
|
||||
final List<Widget> children = <Widget>[];
|
||||
children.add(_switchNoNutrition(localizations));
|
||||
if (!_unspecified) {
|
||||
children.add(_getServingField(localizations));
|
||||
children.add(_getServingSwitch(localizations));
|
||||
children.add(_switchNoNutrition(appLocalizations));
|
||||
if (!_noNutritionData) {
|
||||
children.add(_getServingField(appLocalizations));
|
||||
children.add(_getServingSwitch(appLocalizations));
|
||||
for (final OrderedNutrient orderedNutrient
|
||||
in _nutritionContainer.getDisplayableNutrients()) {
|
||||
children.add(
|
||||
_getNutrientRow(localizations, orderedNutrient),
|
||||
_getNutrientRow(appLocalizations, orderedNutrient),
|
||||
);
|
||||
}
|
||||
children.add(_addNutrientButton(localizations));
|
||||
children.add(_addNutrientButton(appLocalizations));
|
||||
}
|
||||
|
||||
return WillPopScope(
|
||||
//return a boolean to decide whether to return to previous page or not
|
||||
onWillPop: () => _showCancelPopup(localizations),
|
||||
onWillPop: () async => _mayExitPage(saving: false),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: AutoSizeText(
|
||||
localizations.nutrition_page_title,
|
||||
appLocalizations.nutrition_page_title,
|
||||
maxLines: 2,
|
||||
),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
onPressed: () => _validateAndSave(localizations, localDatabase),
|
||||
icon: const Icon(Icons.check),
|
||||
)
|
||||
],
|
||||
systemOverlayStyle: const SystemUiOverlayStyle(
|
||||
statusBarIconBrightness: Brightness.light,
|
||||
statusBarBrightness: Brightness.dark,
|
||||
),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: LARGE_SPACE,
|
||||
vertical: SMALL_SPACE,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: ListView(children: children),
|
||||
),
|
||||
),
|
||||
SmoothActionButtonsBar(
|
||||
positiveAction: SmoothActionButton(
|
||||
text: appLocalizations.save,
|
||||
onPressed: () async => _exitPage(
|
||||
await _mayExitPage(saving: true),
|
||||
),
|
||||
),
|
||||
negativeAction: SmoothActionButton(
|
||||
text: appLocalizations.cancel,
|
||||
onPressed: () async => _exitPage(
|
||||
await _mayExitPage(saving: false),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -268,9 +277,9 @@ class _NutritionPageLoadedState extends State<NutritionPageLoaded> {
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Switch(
|
||||
value: _unspecified,
|
||||
value: _noNutritionData,
|
||||
onChanged: (final bool value) =>
|
||||
setState(() => _unspecified = !_unspecified),
|
||||
setState(() => _noNutritionData = !_noNutritionData),
|
||||
trackColor: MaterialStateProperty.all(
|
||||
Theme.of(context).colorScheme.onPrimary),
|
||||
),
|
||||
@ -367,98 +376,104 @@ class _NutritionPageLoadedState extends State<NutritionPageLoaded> {
|
||||
label: Text(appLocalizations.nutrition_page_add_nutrient),
|
||||
);
|
||||
|
||||
Future<bool> _showCancelPopup(final AppLocalizations appLocalizations) async {
|
||||
//if no changes made then returns true to the onWillPop
|
||||
// allowing it to let the user return back to previous screen
|
||||
if (!_isEdited()) {
|
||||
return true;
|
||||
}
|
||||
return await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => SmoothAlertDialog(
|
||||
title: appLocalizations.general_confirmation,
|
||||
body: Text(appLocalizations.nutrition_page_close_confirmation),
|
||||
negativeAction: SmoothActionButton(
|
||||
text: appLocalizations.cancel,
|
||||
// returns false to onWillPop after the alert dialog is closed with cancel button
|
||||
//blocking return to the previous screen
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
positiveAction: SmoothActionButton(
|
||||
text: appLocalizations.okay,
|
||||
// returns true to onWillPop after the alert dialog is closed with close button
|
||||
//letting return to the previous screen
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
),
|
||||
),
|
||||
) ??
|
||||
// in case alert dialog is closed, a false is return
|
||||
// blocking the return to the previous screen
|
||||
false;
|
||||
}
|
||||
|
||||
Future<void> _validateAndSave(final AppLocalizations localizations,
|
||||
final LocalDatabase localDatabase) async {
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _showSavePopup(localizations, localDatabase);
|
||||
}
|
||||
|
||||
Future<void> _showSavePopup(
|
||||
final AppLocalizations appLocalizations,
|
||||
final LocalDatabase localDatabase,
|
||||
) async {
|
||||
final bool shouldSave = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => SmoothAlertDialog(
|
||||
title: appLocalizations.general_confirmation,
|
||||
body: Text(appLocalizations.save_confirmation),
|
||||
negativeAction: SmoothActionButton(
|
||||
text: appLocalizations.cancel,
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
),
|
||||
positiveAction: SmoothActionButton(
|
||||
text: appLocalizations.save.toUpperCase(),
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
),
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
|
||||
if (shouldSave) {
|
||||
await _save(localDatabase);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _save(final LocalDatabase localDatabase) async {
|
||||
for (final String key in _controllers.keys) {
|
||||
final TextEditingController controller = _controllers[key]!;
|
||||
_nutritionContainer.setControllerText(key, controller.text);
|
||||
}
|
||||
// minimal product: we only want to save the nutrients
|
||||
final Product inputProduct = _nutritionContainer.getProduct();
|
||||
await ProductRefresher().saveAndRefresh(
|
||||
context: context,
|
||||
localDatabase: localDatabase,
|
||||
product: inputProduct,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns `true` if any value differs between form and container.
|
||||
bool _isEdited() {
|
||||
for (final String key in _controllers.keys) {
|
||||
final TextEditingController controller = _controllers[key]!;
|
||||
if (_nutritionContainer.getValue(key) != null) {
|
||||
if (_numberFormat.format(_nutritionContainer.getValue(key)) !=
|
||||
if (_nutritionContainer.getValue(key) == null) {
|
||||
if (controller.value.text != '') {
|
||||
return true;
|
||||
}
|
||||
} else if (_numberFormat.format(_nutritionContainer.getValue(key)) !=
|
||||
controller.value.text) {
|
||||
//if any controller is not equal to the value in the container
|
||||
// then the form is edited, return true
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (_nutritionContainer.noNutritionData != _noNutritionData) {
|
||||
return true;
|
||||
}
|
||||
//else form is not edited just return false
|
||||
return false;
|
||||
}
|
||||
|
||||
Product? _getChangedProduct() {
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return null;
|
||||
}
|
||||
// We use a separate fresh container here.
|
||||
// If something breaks while saving, we won't get a half written object.
|
||||
final NutritionContainer output = _getFreshContainer();
|
||||
for (final String key in _controllers.keys) {
|
||||
final TextEditingController controller = _controllers[key]!;
|
||||
output.setControllerText(key, controller.text);
|
||||
}
|
||||
output.noNutritionData = _noNutritionData;
|
||||
return output.getProduct();
|
||||
}
|
||||
|
||||
NutritionContainer _getFreshContainer() => NutritionContainer(
|
||||
orderedNutrients: widget.orderedNutrients,
|
||||
product: _product,
|
||||
);
|
||||
|
||||
/// Exits the page if the [flag] is `true`.
|
||||
void _exitPage(final bool flag) {
|
||||
if (flag) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if we should really exit the page.
|
||||
///
|
||||
/// Parameter [saving] tells about the context: are we leaving the page,
|
||||
/// or have we clicked on the "save" button?
|
||||
Future<bool> _mayExitPage({required final bool saving}) async {
|
||||
if (!_isEdited()) {
|
||||
return true;
|
||||
}
|
||||
final AppLocalizations appLocalizations = AppLocalizations.of(context);
|
||||
final LocalDatabase localDatabase = context.read<LocalDatabase>();
|
||||
if (!saving) {
|
||||
final bool? pleaseSave = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (final BuildContext context) => SmoothAlertDialog(
|
||||
close: true,
|
||||
body: Text(appLocalizations.edit_product_form_item_exit_confirmation),
|
||||
title: appLocalizations.nutrition_page_title,
|
||||
negativeAction: SmoothActionButton(
|
||||
text: appLocalizations.ignore,
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
),
|
||||
positiveAction: SmoothActionButton(
|
||||
text: appLocalizations.save,
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (pleaseSave == null) {
|
||||
return false;
|
||||
}
|
||||
if (pleaseSave == false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
final Product? changedProduct = _getChangedProduct();
|
||||
if (changedProduct == null) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
// here I cheat and I reuse the only invalid case.
|
||||
content: Text(appLocalizations.nutrition_page_invalid_number),
|
||||
),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// if it fails, we stay on the same page
|
||||
return ProductRefresher().saveAndRefresh(
|
||||
context: context,
|
||||
localDatabase: localDatabase,
|
||||
product: changedProduct,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user