Skip to content

Commit

Permalink
feat: 4996 - reordering product languages (#5025)
Browse files Browse the repository at this point in the history
New file:
* `language_priority.dart`: Helper around the language priority.

Impacted files:
* `add_basic_details_page.dart`: minor refactoring
* `dao_string_list.dart`: now storing the latest languages used locally in the app
* `edit_ocr_page.dart`: minor refactoring
* `language_selector.dart`: now showing app and product languages as first languages
* `multilingual_helper.dart`: minor refactoring
  • Loading branch information
monsieurtanuki authored Feb 4, 2024
1 parent dc65c00 commit 4d9c7fc
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 27 deletions.
5 changes: 5 additions & 0 deletions packages/smooth_app/lib/database/dao_string_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,15 @@ class DaoStringList extends AbstractDao {
/// Key for the list of task ids.
static const String keyTasks = 'tasks';

/// Key for the list of latest languages used in the app.
static const String keyLanguages = 'languages';

/// Max lengths of each key (null means no limit).
static const Map<String, int?> _maxLengths = <String, int?>{
keySearchHistory: 10,
keyTasks: null,
// TODO(monsieurtanuki): more "latest" languages are possible if we create a page to remove some of them
keyLanguages: 1,
};

@override
Expand Down
92 changes: 92 additions & 0 deletions packages/smooth_app/lib/generic_lib/widgets/language_priority.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:smooth_app/database/dao_string_list.dart';
import 'package:smooth_app/query/product_query.dart';

/// Helper around the language priority.
///
/// cf. https://github.com/openfoodfacts/smooth-app/issues/4996
class LanguagePriority {
LanguagePriority({
required final Product? product,
required final Iterable<OpenFoodFactsLanguage>? selectedLanguages,
required final DaoStringList daoStringList,
}) {
_addAll(selectedLanguages);
_addStringList(daoStringList.getAll(DaoStringList.keyLanguages));
_add(ProductQuery.getLanguage());
if (product == null) {
return;
}
_add(product.lang);
_addMap(product.productNameInLanguages);
_addImages(product.images);
_addMap(product.packagingTextInLanguages);
_addMap(product.ingredientsTextInLanguages);
_addMap(product.labelsTagsInLanguages);
_addMap(product.categoriesTagsInLanguages);
_addMap(product.countriesTagsInLanguages);
}

final List<OpenFoodFactsLanguage> _languages = <OpenFoodFactsLanguage>[];

void _addMap(final Map<OpenFoodFactsLanguage, dynamic>? map) {
if (map == null) {
return;
}
_addAll(map.keys);
}

void _addImages(final Iterable<ProductImage>? productImages) {
if (productImages == null) {
return;
}
for (final ProductImage productImage in productImages) {
_add(productImage.language);
}
}

void _add(final OpenFoodFactsLanguage? language) {
if (language == null) {
return;
}
if (_languages.contains(language)) {
return;
}
_languages.add(language);
}

void _addString(final String languageCode) {
try {
final OpenFoodFactsLanguage? language =
OpenFoodFactsLanguage.fromOffTag(languageCode);
_add(language);
} catch (e) {
// just ignore
}
}

void _addStringList(final List<String> languageCodes) =>
languageCodes.forEach(_addString);

void _addAll(final Iterable<OpenFoodFactsLanguage>? languages) {
if (languages == null) {
return;
}
languages.forEach(_add);
}

int? compare(final OpenFoodFactsLanguage a, final OpenFoodFactsLanguage b) {
final bool selectedA = _languages.contains(a);
final bool selectedB = _languages.contains(b);
if (selectedA) {
if (!selectedB) {
return -1;
}
} else {
if (selectedB) {
return 1;
}
}
return null;
}
}
61 changes: 38 additions & 23 deletions packages/smooth_app/lib/generic_lib/widgets/language_selector.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:provider/provider.dart';
import 'package:smooth_app/database/dao_string_list.dart';
import 'package:smooth_app/database/local_database.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_priority.dart';
import 'package:smooth_app/generic_lib/widgets/smooth_text_form_field.dart';
import 'package:smooth_app/pages/preferences/user_preferences_languages_list.dart';
import 'package:smooth_app/query/product_query.dart';
Expand All @@ -16,6 +20,7 @@ class LanguageSelector extends StatelessWidget {
this.foregroundColor,
this.icon,
this.padding,
this.product,
});

/// What to do when the language is selected.
Expand All @@ -31,6 +36,9 @@ class LanguageSelector extends StatelessWidget {
final IconData? icon;
final EdgeInsetsGeometry? padding;

/// Product from which we can extract the languages that matter.
final Product? product;

static const Languages _languages = Languages();

@override
Expand All @@ -42,16 +50,28 @@ class LanguageSelector extends StatelessWidget {
final String currentLanguageCode = ProductQuery.getLanguage().code;
language = LanguageHelper.fromJson(currentLanguageCode);
}
final String nameInEnglish = _languages.getNameInEnglish(language);
final String nameInLanguage = _languages.getNameInLanguage(language);
final DaoStringList daoStringList =
DaoStringList(context.read<LocalDatabase>());
final LanguagePriority languagePriority = LanguagePriority(
product: product,
selectedLanguages: selectedLanguages,
daoStringList: daoStringList,
);
return Material(
type: MaterialType.transparency,
child: InkWell(
onTap: () async {
final OpenFoodFactsLanguage? language = await openLanguageSelector(
final OpenFoodFactsLanguage? language = await _openLanguageSelector(
context,
selectedLanguages: selectedLanguages,
languagePriority: languagePriority,
);
if (language != null) {
await daoStringList.add(
DaoStringList.keyLanguages,
language.offTag,
);
}
await setLanguage(language);
},
borderRadius: ANGULAR_BORDER_RADIUS,
Expand All @@ -72,7 +92,7 @@ class LanguageSelector extends StatelessWidget {
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: LARGE_SPACE),
child: Text(
'$nameInLanguage ($nameInEnglish)',
_getCompleteName(language),
softWrap: false,
overflow: TextOverflow.fade,
style: Theme.of(context)
Expand All @@ -97,9 +117,10 @@ class LanguageSelector extends StatelessWidget {
/// Returns the language selected by the user.
///
/// [selectedLanguages] have a specific "more important" display.
static Future<OpenFoodFactsLanguage?> openLanguageSelector(
Future<OpenFoodFactsLanguage?> _openLanguageSelector(
final BuildContext context, {
final Iterable<OpenFoodFactsLanguage>? selectedLanguages,
required final LanguagePriority languagePriority,
}) async {
final ScrollController scrollController = ScrollController();
final AppLocalizations appLocalizations = AppLocalizations.of(context);
Expand All @@ -109,19 +130,9 @@ class LanguageSelector extends StatelessWidget {
_languages.getSupportedLanguagesNameInEnglish();
leftovers.sort(
(OpenFoodFactsLanguage a, OpenFoodFactsLanguage b) {
// Selected languages first.
final bool selectedA =
selectedLanguages != null && selectedLanguages.contains(a);
final bool selectedB =
selectedLanguages != null && selectedLanguages.contains(b);
if (selectedA) {
if (!selectedB) {
return -1;
}
} else {
if (selectedB) {
return 1;
}
final int? compare = languagePriority.compare(a, b);
if (compare != null) {
return compare;
}
// Sorted in English
return _languages
Expand Down Expand Up @@ -170,17 +181,13 @@ class LanguageSelector extends StatelessWidget {
controller: scrollController,
itemBuilder: (BuildContext context, int index) {
final OpenFoodFactsLanguage language = filteredList[index];
final String nameInLanguage =
_languages.getNameInLanguage(language);
final String nameInEnglish =
_languages.getNameInEnglish(language);
final bool selected = selectedLanguages != null &&
selectedLanguages.contains(language);
return ListTile(
dense: true,
trailing: selected ? const Icon(Icons.check) : null,
title: TextHighlighter(
text: '$nameInLanguage ($nameInEnglish)',
text: _getCompleteName(language),
filter: languageSelectorController.text,
selected: selected,
),
Expand All @@ -204,4 +211,12 @@ class LanguageSelector extends StatelessWidget {
),
);
}

String _getCompleteName(
final OpenFoodFactsLanguage language,
) {
final String nameInLanguage = _languages.getNameInLanguage(language);
final String nameInEnglish = _languages.getNameInEnglish(language);
return '$nameInLanguage ($nameInEnglish)';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,10 @@ class _AddBasicDetailsPageState extends State<AddBasicDetailsPage> {
Card(
child: Column(
children: <Widget>[
_multilingualHelper
.getLanguageSelector(setState),
_multilingualHelper.getLanguageSelector(
setState: setState,
product: _product,
),
Padding(
padding: const EdgeInsets.all(8.0),
child: SmoothTextFormField(
Expand Down
5 changes: 4 additions & 1 deletion packages/smooth_app/lib/pages/product/edit_ocr_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,10 @@ class _EditOcrPageState extends State<EditOcrPage> with UpToDateMixin {
child: Column(
children: <Widget>[
if (!_multilingualHelper.isMonolingual())
_multilingualHelper.getLanguageSelector(setState),
_multilingualHelper.getLanguageSelector(
setState: setState,
product: upToDateProduct,
),
if (transientFile.isServerImage())
SmoothActionButtonsBar.single(
action: SmoothActionButton(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,12 @@ class MultilingualHelper {
// TODO(monsieurtanuki): we would be better off always never monolingual
bool isMonolingual() => _initialMultilingualTexts.isEmpty;

Widget getLanguageSelector(void Function(void Function()) setState) =>
Widget getLanguageSelector({
required void Function(void Function()) setState,
required Product product,
}) =>
LanguageSelector(
product: product,
setLanguage: (
final OpenFoodFactsLanguage? newLanguage,
) async {
Expand Down

0 comments on commit 4d9c7fc

Please sign in to comment.