Skip to content

Commit

Permalink
feat: openfoodfacts#2380 - autocompletion for emb codes, labels, cate…
Browse files Browse the repository at this point in the history
…gories and countries (openfoodfacts#2506)

New file:
* `autocomplete.dart`: The default Material-style Autocomplete options.

Impacted files:
* `simple_input_page.dart`: now using an autocomplete widget for whoever supports it.
* `simple_input_page_helpers.dart`: new method in order to get the `TagType` for autocompletion.
  • Loading branch information
monsieurtanuki authored Jul 5, 2022
1 parent ab4624b commit a1048bb
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 12 deletions.
65 changes: 65 additions & 0 deletions packages/smooth_app/lib/pages/product/autocomplete.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

/// The default Material-style Autocomplete options.
///
/// Was copied from material/autocomplete.dart as they were not kind enough
/// to make it public.
/// Inspiration was found in https://stackoverflow.com/questions/66935362
class AutocompleteOptions<T extends Object> extends StatelessWidget {
const AutocompleteOptions({
Key? key,
required this.displayStringForOption,
required this.onSelected,
required this.options,
required this.maxOptionsHeight,
}) : super(key: key);

final AutocompleteOptionToString<T> displayStringForOption;

final AutocompleteOnSelected<T> onSelected;

final Iterable<T> options;
final double maxOptionsHeight;

@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.topLeft,
child: Material(
elevation: 4.0,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: maxOptionsHeight),
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final T option = options.elementAt(index);
return InkWell(
onTap: () {
onSelected(option);
},
child: Builder(builder: (BuildContext context) {
final bool highlight =
AutocompleteHighlightedOption.of(context) == index;
if (highlight) {
SchedulerBinding.instance
.addPostFrameCallback((Duration timeStamp) {
Scrollable.ensureVisible(context, alignment: 0.5);
});
}
return Container(
color: highlight ? Theme.of(context).focusColor : null,
padding: const EdgeInsets.all(16.0),
child: Text(displayStringForOption(option)),
);
}),
);
},
),
),
),
);
}
}
79 changes: 67 additions & 12 deletions packages/smooth_app/lib/pages/product/simple_input_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:openfoodfacts/utils/TagType.dart';
import 'package:provider/provider.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/helpers/product_cards_helper.dart';
import 'package:smooth_app/pages/product/autocomplete.dart';
import 'package:smooth_app/pages/product/common/product_refresher.dart';
import 'package:smooth_app/pages/product/simple_input_page_helpers.dart';
import 'package:smooth_app/query/product_query.dart';
import 'package:smooth_app/widgets/smooth_scaffold.dart';

/// Simple input page: we have a list of terms, we add, we remove, we save.
Expand All @@ -27,6 +30,8 @@ class SimpleInputPage extends StatefulWidget {

class _SimpleInputPageState extends State<SimpleInputPage> {
final TextEditingController _controller = TextEditingController();
final FocusNode _focusNode = FocusNode();
final GlobalKey _autocompleteKey = GlobalKey();

@override
void initState() {
Expand Down Expand Up @@ -66,20 +71,70 @@ class _SimpleInputPageState extends State<SimpleInputPage> {
ListTile(
onTap: () => _addItemsFromController(),
trailing: const Icon(Icons.add_circle),
title: TextField(
decoration: InputDecoration(
filled: true,
border: const OutlineInputBorder(
borderRadius: CIRCULAR_BORDER_RADIUS,
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: SMALL_SPACE,
vertical: SMALL_SPACE,
title: RawAutocomplete<String>(
key: _autocompleteKey,
focusNode: _focusNode,
textEditingController: _controller,
optionsBuilder: (final TextEditingValue value) async {
final List<String> result = <String>[];
final String input = value.text.trim();
if (input.isEmpty) {
return result;
}
final TagType? tagType = widget.helper.getTagType();
if (tagType == null) {
return result;
}
// TODO(monsieurtanuki): ask off-dart to return Strings instead of dynamic?
final List<dynamic> data = await OpenFoodAPIClient
.getAutocompletedSuggestions(
tagType,
language: ProductQuery.getLanguage()!,
limit:
1000000, // lower max count on the server anyway
input: value.text.trim(),
);
for (final dynamic item in data) {
result.add(item.toString());
}
result.sort();
return result;
},
fieldViewBuilder: (BuildContext context,
TextEditingController textEditingController,
FocusNode focusNode,
VoidCallback onFieldSubmitted) =>
TextField(
controller: textEditingController,
decoration: InputDecoration(
filled: true,
border: const OutlineInputBorder(
borderRadius: CIRCULAR_BORDER_RADIUS,
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: SMALL_SPACE,
vertical: SMALL_SPACE,
),
hintText:
widget.helper.getAddHint(appLocalizations),
),
hintText: widget.helper.getAddHint(appLocalizations),
autofocus: true,
focusNode: focusNode,
),
optionsViewBuilder: (
BuildContext context,
AutocompleteOnSelected<String> onSelected,
Iterable<String> options,
) =>
AutocompleteOptions<String>(
displayStringForOption:
RawAutocomplete.defaultStringForOption,
onSelected: onSelected,
options: options,
maxOptionsHeight:
MediaQuery.of(context).size.height / 2,
),
controller: _controller,
),
),
if (widget.helper.getAddExplanations(appLocalizations) !=
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:openfoodfacts/utils/TagType.dart';
import 'package:smooth_app/query/product_query.dart';

/// Abstract helper for Simple Input Page.
Expand Down Expand Up @@ -76,6 +77,9 @@ abstract class AbstractSimpleInputPageHelper {
@protected
void changeProduct(final Product changedProduct);

/// Returns the tag type for autocomplete suggestions.
TagType? getTagType();

/// Returns null is no change was made, or a Product to be saved on the BE.
Product? getChangedProduct() {
if (!_changed) {
Expand Down Expand Up @@ -115,6 +119,9 @@ class SimpleInputPageStoreHelper extends AbstractSimpleInputPageHelper {
@override
String getAddHint(final AppLocalizations appLocalizations) =>
appLocalizations.edit_product_form_item_stores_hint;

@override
TagType? getTagType() => null;
}

/// Implementation for "Emb Code" of an [AbstractSimpleInputPageHelper].
Expand All @@ -137,6 +144,9 @@ class SimpleInputPageEmbCodeHelper extends AbstractSimpleInputPageHelper {
@override
String getAddExplanations(final AppLocalizations appLocalizations) =>
appLocalizations.edit_product_form_item_emb_codes_explanations;

@override
TagType? getTagType() => TagType.EMB_CODES;
}

/// Abstraction, for "in language" field, of an [AbstractSimpleInputPageHelper].
Expand Down Expand Up @@ -222,6 +232,9 @@ class SimpleInputPageLabelHelper
@override
String getAddHint(final AppLocalizations appLocalizations) =>
appLocalizations.edit_product_form_item_labels_hint;

@override
TagType? getTagType() => TagType.LABELS;
}

/// Implementation for "Categories" of an [AbstractSimpleInputPageHelper].
Expand All @@ -245,6 +258,9 @@ class SimpleInputPageCategoryHelper
@override
String getAddHint(final AppLocalizations appLocalizations) =>
appLocalizations.edit_product_form_item_categories_hint;

@override
TagType? getTagType() => TagType.CATEGORIES;
}

/// Implementation for "Countries" of an [AbstractSimpleInputPageHelper].
Expand Down Expand Up @@ -272,4 +288,7 @@ class SimpleInputPageCountryHelper
@override
String getAddExplanations(final AppLocalizations appLocalizations) =>
appLocalizations.edit_product_form_item_countries_explanations;

@override
TagType? getTagType() => TagType.COUNTRIES;
}

0 comments on commit a1048bb

Please sign in to comment.