mirror of
https://github.com/openfoodfacts/smooth-app.git
synced 2025-08-26 11:16:45 +08:00
* feat: #1377 - display and re-extract ingredients
This commit is contained in:
@ -17,11 +17,17 @@ import 'package:smooth_app/pages/user_preferences_dev_mode.dart';
|
||||
///
|
||||
/// Panels display large data like all health data or environment data.
|
||||
class KnowledgePanelsBuilder {
|
||||
const KnowledgePanelsBuilder({this.setState});
|
||||
const KnowledgePanelsBuilder({
|
||||
this.setState,
|
||||
this.refreshProductCallback,
|
||||
});
|
||||
|
||||
/// Would for instance refresh the product page.
|
||||
final VoidCallback? setState;
|
||||
|
||||
/// Callback to refresh the product when necessary.
|
||||
final Function(BuildContext)? refreshProductCallback;
|
||||
|
||||
/// Builds all panels.
|
||||
///
|
||||
/// Typical use case: product page.
|
||||
@ -140,12 +146,12 @@ class KnowledgePanelsBuilder {
|
||||
knowledgePanelElementWidgets.add(
|
||||
addPanelButton(
|
||||
appLocalizations.score_add_missing_ingredients,
|
||||
onPressed: () async => Navigator.push<Widget>(
|
||||
onPressed: () async => Navigator.push<bool>(
|
||||
context,
|
||||
MaterialPageRoute<Widget>(
|
||||
MaterialPageRoute<bool>(
|
||||
builder: (BuildContext context) => EditIngredientsPage(
|
||||
product: product,
|
||||
imageIngredientsUrl: product.imageIngredientsUrl,
|
||||
refreshProductCallback: refreshProductCallback,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -100,6 +100,7 @@ abstract class ProductQuery {
|
||||
ProductField.NUTRIMENT_ENERGY_UNIT,
|
||||
ProductField.ADDITIVES,
|
||||
ProductField.INGREDIENTS_ANALYSIS_TAGS,
|
||||
ProductField.INGREDIENTS_TEXT,
|
||||
ProductField.LABELS_TAGS,
|
||||
ProductField.LABELS_TAGS_IN_LANGUAGES,
|
||||
ProductField.ENVIRONMENT_IMPACT_LEVELS,
|
||||
|
@ -837,6 +837,14 @@
|
||||
"completed_basic_details_btn_text": "Complete basic details",
|
||||
"not_implemented_snackbar_text": "Not implemented yet",
|
||||
"category_picker_page_appbar_text": "Categories",
|
||||
"edit_ingredients_extrait_ingredients_btn_text": "Extract ingredients",
|
||||
"@edit_ingredients_extrait_ingredients_btn_text": {
|
||||
"description": "Ingredients edition - Extract ingredients"
|
||||
},
|
||||
"edit_ingredients_refresh_photo_btn_text": "Refresh photo",
|
||||
"@edit_ingredients_refresh_photo_btn_text": {
|
||||
"description": "Ingredients edition - Refresh photo"
|
||||
},
|
||||
"user_list_dialog_new_title": "New list of products",
|
||||
"@user_list_dialog_new_title": {
|
||||
"description": "Title of the 'new user list' dialog"
|
||||
|
@ -8,24 +8,23 @@ import 'package:openfoodfacts/openfoodfacts.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:smooth_app/database/local_database.dart';
|
||||
import 'package:smooth_app/database/product_query.dart';
|
||||
import 'package:smooth_app/generic_lib/buttons/smooth_action_button.dart';
|
||||
import 'package:smooth_app/generic_lib/design_constants.dart';
|
||||
import 'package:smooth_app/helpers/picture_capture_helper.dart';
|
||||
import 'package:smooth_app/pages/image_crop_page.dart';
|
||||
import 'package:smooth_app/pages/product/common/product_refresher.dart';
|
||||
import 'package:smooth_app/themes/smooth_theme.dart';
|
||||
import 'package:smooth_app/themes/theme_provider.dart';
|
||||
|
||||
/// Page for editing the ingredients of a product and the image of the
|
||||
/// ingredients.
|
||||
class EditIngredientsPage extends StatefulWidget {
|
||||
const EditIngredientsPage({
|
||||
Key? key,
|
||||
this.imageIngredientsUrl,
|
||||
required this.product,
|
||||
this.refreshProductCallback,
|
||||
}) : super(key: key);
|
||||
|
||||
final Product product;
|
||||
final String? imageIngredientsUrl;
|
||||
final Function(BuildContext)? refreshProductCallback;
|
||||
|
||||
@override
|
||||
State<EditIngredientsPage> createState() => _EditIngredientsPageState();
|
||||
@ -37,27 +36,13 @@ class _EditIngredientsPageState extends State<EditIngredientsPage> {
|
||||
bool _updatingImage = false;
|
||||
bool _updatingIngredients = false;
|
||||
|
||||
static String _getIngredientsString(List<Ingredient>? ingredients) {
|
||||
return ingredients == null ? '' : ingredients.join(', ');
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller.text = _getIngredientsString(widget.product.ingredients);
|
||||
_controller.text = widget.product.ingredientsText ?? '';
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(EditIngredientsPage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
final String productIngredients =
|
||||
_getIngredientsString(widget.product.ingredients);
|
||||
if (productIngredients != _controller.text) {
|
||||
_controller.text = productIngredients;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSubmitField(String string) async {
|
||||
Future<void> _onSubmitField() async {
|
||||
final User user = ProductQuery.getUser();
|
||||
|
||||
setState(() {
|
||||
@ -65,7 +50,7 @@ class _EditIngredientsPageState extends State<EditIngredientsPage> {
|
||||
});
|
||||
|
||||
try {
|
||||
await _updateIngredientsText(string, user);
|
||||
await _updateIngredientsText(_controller.text, user);
|
||||
} catch (error) {
|
||||
final AppLocalizations appLocalizations = AppLocalizations.of(context)!;
|
||||
_showError(appLocalizations.ingredients_editing_error);
|
||||
@ -76,13 +61,13 @@ class _EditIngredientsPageState extends State<EditIngredientsPage> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onTapGetImage() async {
|
||||
Future<void> _onTapGetImage(bool isNewImage) async {
|
||||
setState(() {
|
||||
_updatingImage = true;
|
||||
});
|
||||
|
||||
try {
|
||||
await _getImage();
|
||||
await _getImage(isNewImage);
|
||||
} catch (error) {
|
||||
final AppLocalizations appLocalizations = AppLocalizations.of(context)!;
|
||||
_showError(appLocalizations.ingredients_editing_image_error);
|
||||
@ -108,7 +93,9 @@ class _EditIngredientsPageState extends State<EditIngredientsPage> {
|
||||
//
|
||||
// Returns a Future that resolves successfully only if everything succeeds,
|
||||
// otherwise it will resolve with the relevant error.
|
||||
Future<void> _getImage() async {
|
||||
Future<void> _getImage(bool isNewImage) async {
|
||||
bool isUploaded = true;
|
||||
if (isNewImage) {
|
||||
final File? croppedImageFile = await startImageCropping(context);
|
||||
|
||||
// If the user cancels.
|
||||
@ -121,13 +108,15 @@ class _EditIngredientsPageState extends State<EditIngredientsPage> {
|
||||
_imageProvider = FileImage(croppedImageFile);
|
||||
});
|
||||
|
||||
final bool isUploaded = await uploadCapturedPicture(
|
||||
isUploaded = await uploadCapturedPicture(
|
||||
context,
|
||||
barcode: widget.product.barcode!,
|
||||
imageField: ImageField.INGREDIENTS,
|
||||
imageUri: croppedImageFile.uri,
|
||||
);
|
||||
|
||||
croppedImageFile.delete();
|
||||
}
|
||||
|
||||
if (!isUploaded) {
|
||||
throw Exception('Image could not be uploaded.');
|
||||
@ -152,8 +141,6 @@ class _EditIngredientsPageState extends State<EditIngredientsPage> {
|
||||
setState(() {
|
||||
_controller.text = nextIngredients;
|
||||
});
|
||||
|
||||
await _updateIngredientsText(nextIngredients, user);
|
||||
}
|
||||
}
|
||||
|
||||
@ -165,7 +152,9 @@ class _EditIngredientsPageState extends State<EditIngredientsPage> {
|
||||
localDatabase: localDatabase,
|
||||
product: widget.product,
|
||||
);
|
||||
if (!savedAndRefreshed) {
|
||||
if (savedAndRefreshed) {
|
||||
await widget.refreshProductCallback?.call(context);
|
||||
} else {
|
||||
throw Exception("Couldn't save the product.");
|
||||
}
|
||||
}
|
||||
@ -184,10 +173,11 @@ class _EditIngredientsPageState extends State<EditIngredientsPage> {
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (widget.imageIngredientsUrl != null) {
|
||||
if (widget.product.imageIngredientsUrl != null) {
|
||||
children.add(ConstrainedBox(
|
||||
constraints: const BoxConstraints.expand(),
|
||||
child: _buildZoomableImage(NetworkImage(widget.imageIngredientsUrl!)),
|
||||
child: _buildZoomableImage(
|
||||
NetworkImage(widget.product.imageIngredientsUrl!)),
|
||||
));
|
||||
} else {
|
||||
children.add(Container(color: Colors.white));
|
||||
@ -201,7 +191,7 @@ class _EditIngredientsPageState extends State<EditIngredientsPage> {
|
||||
} else {
|
||||
children.add(_EditIngredientsBody(
|
||||
controller: _controller,
|
||||
imageIngredientsUrl: widget.imageIngredientsUrl,
|
||||
imageIngredientsUrl: widget.product.imageIngredientsUrl,
|
||||
onTapGetImage: _onTapGetImage,
|
||||
onSubmitField: _onSubmitField,
|
||||
updatingIngredients: _updatingIngredients,
|
||||
@ -251,22 +241,15 @@ class _EditIngredientsBody extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
final bool updatingIngredients;
|
||||
final String? imageIngredientsUrl;
|
||||
final Future<void> Function() onTapGetImage;
|
||||
final Future<void> Function(String) onSubmitField;
|
||||
final Future<void> Function(bool) onTapGetImage;
|
||||
final Future<void> Function() onSubmitField;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeProvider themeProvider = context.watch<ThemeProvider>();
|
||||
final ThemeData darkTheme = SmoothTheme.getThemeData(
|
||||
Brightness.dark,
|
||||
themeProvider.colorTag,
|
||||
);
|
||||
final AppLocalizations appLocalizations = AppLocalizations.of(context)!;
|
||||
|
||||
return Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: LARGE_SPACE),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
@ -275,26 +258,36 @@ class _EditIngredientsBody extends StatelessWidget {
|
||||
child: Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: LARGE_SPACE),
|
||||
child: _ActionButtons(
|
||||
getImage: onTapGetImage,
|
||||
hasImage: imageIngredientsUrl != null,
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: LARGE_SPACE, right: SMALL_SPACE),
|
||||
child: SmoothActionButton(
|
||||
text:
|
||||
appLocalizations.edit_ingredients_refresh_photo_btn_text,
|
||||
onPressed: () => onTapGetImage(true),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: SingleChildScrollView(
|
||||
child: Container(
|
||||
color: Colors.black,
|
||||
child: Theme(
|
||||
data: darkTheme,
|
||||
child: DefaultTextStyle(
|
||||
style: const TextStyle(color: Colors.white),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: ANGULAR_RADIUS,
|
||||
topRight: ANGULAR_RADIUS,
|
||||
)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(LARGE_SPACE),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
SmoothActionButton(
|
||||
text: appLocalizations
|
||||
.edit_ingredients_extrait_ingredients_btn_text,
|
||||
onPressed: () => onTapGetImage(false),
|
||||
),
|
||||
const SizedBox(height: MEDIUM_SPACE),
|
||||
TextField(
|
||||
enabled: !updatingIngredients,
|
||||
controller: controller,
|
||||
@ -305,71 +298,39 @@ class _EditIngredientsBody extends StatelessWidget {
|
||||
),
|
||||
maxLines: null,
|
||||
textInputAction: TextInputAction.done,
|
||||
onSubmitted: onSubmitField,
|
||||
onSubmitted: (_) => onSubmitField,
|
||||
),
|
||||
Text(appLocalizations
|
||||
.ingredients_editing_instructions),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The actions for the page in a row of FloatingActionButtons.
|
||||
class _ActionButtons extends StatelessWidget {
|
||||
const _ActionButtons({
|
||||
Key? key,
|
||||
required this.hasImage,
|
||||
required this.getImage,
|
||||
}) : super(key: key);
|
||||
|
||||
final bool hasImage;
|
||||
final VoidCallback getImage;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ColorScheme colorScheme = Theme.of(context).buttonTheme.colorScheme!;
|
||||
final List<Widget> children = hasImage
|
||||
? <Widget>[
|
||||
FloatingActionButton.small(
|
||||
tooltip: 'Retake photo',
|
||||
backgroundColor: colorScheme.background,
|
||||
foregroundColor: colorScheme.onBackground,
|
||||
onPressed: getImage,
|
||||
child: const Icon(Icons.refresh),
|
||||
),
|
||||
const SizedBox(width: MEDIUM_SPACE),
|
||||
FloatingActionButton.small(
|
||||
tooltip: 'Confirm',
|
||||
backgroundColor: colorScheme.primary,
|
||||
foregroundColor: colorScheme.onPrimary,
|
||||
const SizedBox(height: SMALL_SPACE),
|
||||
Text(appLocalizations.ingredients_editing_instructions,
|
||||
style: Theme.of(context).textTheme.caption),
|
||||
const SizedBox(height: MEDIUM_SPACE),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
SmoothActionButton(
|
||||
text: appLocalizations.cancel,
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Icon(Icons.check),
|
||||
),
|
||||
]
|
||||
: <Widget>[
|
||||
FloatingActionButton.small(
|
||||
tooltip: 'Take photo',
|
||||
backgroundColor: colorScheme.background,
|
||||
foregroundColor: colorScheme.onBackground,
|
||||
onPressed: getImage,
|
||||
child: const Icon(Icons.camera_alt),
|
||||
const SizedBox(width: LARGE_SPACE),
|
||||
SmoothActionButton(
|
||||
text: appLocalizations.save,
|
||||
onPressed: () async {
|
||||
await onSubmitField();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: MEDIUM_SPACE),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -238,9 +238,10 @@ class _ProductPageState extends State<ProductPage> {
|
||||
List<Widget> knowledgePanelWidgets = <Widget>[];
|
||||
if (snapshot.hasData) {
|
||||
// Render all KnowledgePanels
|
||||
knowledgePanelWidgets =
|
||||
KnowledgePanelsBuilder(setState: () => setState(() {}))
|
||||
.buildAll(
|
||||
knowledgePanelWidgets = KnowledgePanelsBuilder(
|
||||
setState: () => setState(() {}),
|
||||
refreshProductCallback: _refreshProduct,
|
||||
).buildAll(
|
||||
snapshot.data!,
|
||||
context: context,
|
||||
product: _product,
|
||||
|
Reference in New Issue
Block a user