Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 3897 - ingredients and packaging with multilingual OCR image+text #3911

Merged
merged 2 commits into from
Apr 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 25 additions & 108 deletions packages/smooth_app/lib/pages/product/add_basic_details_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,20 @@ import 'package:smooth_app/background/background_task_details.dart';
import 'package:smooth_app/cards/product_cards/product_image_carousel.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/widgets/language_selector.dart';
import 'package:smooth_app/generic_lib/widgets/smooth_text_form_field.dart';
import 'package:smooth_app/helpers/analytics_helper.dart';
import 'package:smooth_app/helpers/product_cards_helper.dart';
import 'package:smooth_app/pages/product/common/product_refresher.dart';
import 'package:smooth_app/pages/product/may_exit_page_helper.dart';
import 'package:smooth_app/pages/product/multilingual_helper.dart';
import 'package:smooth_app/pages/text_field_helper.dart';
import 'package:smooth_app/query/product_query.dart';
import 'package:smooth_app/widgets/smooth_app_bar.dart';
import 'package:smooth_app/widgets/smooth_scaffold.dart';

/// Input of a product's basic details, like name, quantity and brands.
///
/// There are 2 versions of the product name input:
/// 1. the old one - only one name per product
/// 2. the multilingual one
/// Typically, the old version will be used with "old" data, that have not
/// downloaded yet the "recent" [ProductField.NAME_ALL_LANGUAGES] field.
/// The product name input is either monolingual or multilingual, depending on
/// the product data version.
class AddBasicDetailsPage extends StatefulWidget {
const AddBasicDetailsPage(
this.product, {
Expand All @@ -45,49 +41,25 @@ class _AddBasicDetailsPageState extends State<AddBasicDetailsPage> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
late final Product _product;

/// Current language; only relevant/valid if _names is not empty.
late OpenFoodFactsLanguage _currentLanguage;

/// Current product name translations.
final Map<OpenFoodFactsLanguage, String> _names =
<OpenFoodFactsLanguage, String>{};
late final MultilingualHelper _multilingualHelper;

@override
void initState() {
super.initState();
_product = widget.product;
_weightController = TextEditingControllerWithInitialValue(
text: _getCleanName(_product.quantity ?? ''),
text: MultilingualHelper.getCleanText(_product.quantity ?? ''),
);
_brandNameController = TextEditingControllerWithInitialValue(
text: _formatProductBrands(_product.brands),
);
// checking if we use the multilingual version...
if (_product.productNameInLanguages != null) {
for (final OpenFoodFactsLanguage language
in _product.productNameInLanguages!.keys) {
final String name =
_getCleanName(_product.productNameInLanguages![language]);
if (name.isNotEmpty) {
_names[language] = name;
}
}
if (_names.isNotEmpty) {
final OpenFoodFactsLanguage language = ProductQuery.getLanguage();
if (_names.containsKey(language)) {
// best choice
_currentLanguage = language;
} else {
// fallback
_currentLanguage = _names.keys.first;
}
_productNameController.text = _names[_currentLanguage] ?? '';
}
}
// Fallback: we may have old data where there are no translations.
if (_names.isEmpty) {
_productNameController.text = _product.productName ?? '';
}
_multilingualHelper = MultilingualHelper(
controller: _productNameController,
);
_multilingualHelper.init(
multilingualTexts: _product.productNameInLanguages,
monolingualText: _product.productName,
);
}

@override
Expand All @@ -98,10 +70,9 @@ class _AddBasicDetailsPageState extends State<AddBasicDetailsPage> {
super.dispose();
}

String _formatProductBrands(String? text) =>
_getCleanName(text == null ? '' : formatProductBrands(text));

String _getCleanName(final String? name) => (name ?? '').trim();
String _formatProductBrands(String? text) => MultilingualHelper.getCleanText(
text == null ? '' : formatProductBrands(text),
);

@override
Widget build(BuildContext context) {
Expand Down Expand Up @@ -140,7 +111,7 @@ class _AddBasicDetailsPageState extends State<AddBasicDetailsPage> {
),
),
SizedBox(height: _heightSpace),
if (_names.isEmpty)
if (_multilingualHelper.isMonolingual())
SmoothTextFormField(
controller: _productNameController,
type: TextFieldTypes.PLAIN_TEXT,
Expand All @@ -150,27 +121,7 @@ class _AddBasicDetailsPageState extends State<AddBasicDetailsPage> {
Card(
child: Column(
children: <Widget>[
LanguageSelector(
setLanguage: (
final OpenFoodFactsLanguage? newLanguage,
) async {
if (newLanguage == null) {
return;
}
if (_currentLanguage == newLanguage) {
return;
}
_saveCurrentName();
setState(() {
_currentLanguage = newLanguage;
_names[_currentLanguage] ??= '';
_productNameController.text =
_names[_currentLanguage]!;
});
},
selectedLanguages: _names.keys,
displayedLanguage: _currentLanguage,
),
_multilingualHelper.getLanguageSelector(setState),
Padding(
padding: const EdgeInsets.all(8.0),
child: SmoothTextFormField(
Expand Down Expand Up @@ -231,10 +182,6 @@ class _AddBasicDetailsPageState extends State<AddBasicDetailsPage> {
}
}

/// Saves the current input for the current language.
void _saveCurrentName() =>
_names[_currentLanguage] = _productNameController.text;

/// Returns `true` if we should really exit the page.
///
/// Parameter [saving] tells about the context: are we leaving the page,
Expand Down Expand Up @@ -297,50 +244,20 @@ class _AddBasicDetailsPageState extends State<AddBasicDetailsPage> {
result ??= getBasicProduct();
result.brands = _formatProductBrands(_brandNameController.text);
}
if (_names.isEmpty) {
if (_getCleanName(_productNameController.text) !=
_getCleanName(_product.productName)) {
if (_multilingualHelper.isMonolingual()) {
final String? changed = _multilingualHelper.getChangedMonolingualText();
if (changed != null) {
result ??= getBasicProduct();
result.productName = _productNameController.text;
result.productName = changed;
}
} else {
_saveCurrentName();
final Map<OpenFoodFactsLanguage, String>? newNames =
_getNewNamesIfChanged();
if (newNames != null) {
final Map<OpenFoodFactsLanguage, String>? changed =
_multilingualHelper.getChangedMultilingualText();
if (changed != null) {
result ??= getBasicProduct();
result.productNameInLanguages = newNames;
result.productNameInLanguages = changed;
}
}
return result;
}

/// Returns all the new names, if any change happened.
Map<OpenFoodFactsLanguage, String>? _getNewNamesIfChanged() {
bool changed = false;
final Map<OpenFoodFactsLanguage, String> result =
<OpenFoodFactsLanguage, String>{};
final Map<OpenFoodFactsLanguage, String> oldNames =
_product.productNameInLanguages!;

void setNewName(final OpenFoodFactsLanguage language) {
final String newName = _getCleanName(_names[language]);
final String oldName = _getCleanName(oldNames[language]);
if (newName != oldName) {
changed = true;
}
// For the record: if name is empty, will remove the translation (sometimes).
result[language] = newName;
}

// setting new names, comparing them to old names for change flag.
_names.keys.forEach(setNewName);
// double-checking old names: have some old names been removed?
oldNames.keys.forEach(setNewName);

if (!changed) {
return null;
}
return result;
}
}
Loading