Skip to content

Commit

Permalink
feat: #657 - nutriscore+ecoscore, then mandatory attributes, then gro…
Browse files Browse the repository at this point in the history
…ups and important attributes (#658)

Impacted file:
* `summary_card.dart`: new methods `_getMandatoryAttributeIds` and `_getOrderedAndFilteredAttributes`; added environment group (so far, only forest_footprint); added a virtual group for mandatory attributes on top of the "normal" attribute groups
  • Loading branch information
monsieurtanuki committed Nov 16, 2021
1 parent 54fc665 commit 09a21c3
Showing 1 changed file with 145 additions and 76 deletions.
221 changes: 145 additions & 76 deletions packages/smooth_app/lib/pages/product/summary_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:openfoodfacts/model/Attribute.dart';
import 'package:openfoodfacts/model/AttributeGroup.dart';
import 'package:openfoodfacts/model/Product.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:openfoodfacts/personalized_search/preference_importance.dart';
import 'package:smooth_app/cards/data_cards/score_card.dart';
import 'package:smooth_app/cards/expandables/attribute_list_expandable.dart';
import 'package:smooth_app/data_models/product_preferences.dart';
Expand All @@ -14,6 +15,7 @@ import 'package:smooth_app/helpers/score_card_helper.dart';
import 'package:smooth_ui_library/smooth_ui_library.dart';
import 'package:smooth_ui_library/util/ui_helpers.dart';

/// Main attributes, to be displayed on top
const List<String> _SCORE_ATTRIBUTE_IDS = <String>[
Attribute.ATTRIBUTE_NUTRISCORE,
Attribute.ATTRIBUTE_ECOSCORE,
Expand All @@ -25,6 +27,7 @@ const List<String> _ATTRIBUTE_GROUP_ORDER = <String>[
AttributeGroup.ATTRIBUTE_GROUP_PROCESSING,
AttributeGroup.ATTRIBUTE_GROUP_NUTRITIONAL_QUALITY,
AttributeGroup.ATTRIBUTE_GROUP_LABELS,
AttributeGroup.ATTRIBUTE_GROUP_ENVIRONMENT,
];

// Each row in the summary card takes roughly 40px.
Expand Down Expand Up @@ -128,58 +131,78 @@ class _SummaryCardState extends State<SummaryCard> {
// Each Score card takes about 1.5 rows to render.
totalPrintableRows -= (1.5 * scoreAttributes.length).ceil();

final List<AttributeGroup> attributeGroupsToBeRendered =
_getAttributeGroupsToBeRendered();
final List<Widget> displayedGroups = <Widget>[];

// First, a virtual group with mandatory attributes of all groups
final List<Widget> attributeChips = _buildAttributeChips(
_getMandatoryAttributes(),
dontIncludeAnAttributeWhoseStatusIsUnknown: false,
);
if (attributeChips.isNotEmpty) {
displayedGroups.add(
_buildAttributeGroup(
_buildAttributeGroupHeader(
isFirstGroup: displayedGroups.isEmpty,
groupName: null,
),
attributeChips,
),
);
}
// Then, all groups, each with very important and important attributes
for (final String groupId in _ATTRIBUTE_GROUP_ORDER) {
final Iterable<AttributeGroup> groupIterable = widget
._product.attributeGroups!
.where((AttributeGroup group) => group.id == groupId);
if (groupIterable.isEmpty) {
continue;
}
final AttributeGroup group = groupIterable.single;
final List<Widget> attributeChips = _buildAttributeChips(
_getOrderedAndFilteredAttributes(
group,
<String>[
PreferenceImportance.ID_VERY_IMPORTANT,
PreferenceImportance.ID_IMPORTANT,
],
),
dontIncludeAnAttributeWhoseStatusIsUnknown:
group.id == AttributeGroup.ATTRIBUTE_GROUP_LABELS,
);
if (attributeChips.isNotEmpty) {
displayedGroups.add(
_buildAttributeGroup(
_buildAttributeGroupHeader(
isFirstGroup: displayedGroups.isEmpty,
groupName: group.id == AttributeGroup.ATTRIBUTE_GROUP_ALLERGENS
? group.name!
: null),
attributeChips,
),
);
}
}

final Widget attributesContainer = Container(
alignment: Alignment.topLeft,
margin: const EdgeInsets.only(bottom: 16),
child: Column(
children: <Widget>[
for (final AttributeGroup group in attributeGroupsToBeRendered)
_buildAttributeGroup(
context,
group,
group == attributeGroupsToBeRendered.first,
),
],
),
child: Column(children: displayedGroups),
);
return Column(
children: <Widget>[
_buildProductTitleTile(context),
for (final Attribute attribute in scoreAttributes)
ScoreCard(
iconUrl: attribute.iconUrl!,
description: attribute.descriptionShort ?? attribute.description!,
description:
attribute.descriptionShort ?? attribute.description ?? '',
cardEvaluation: getCardEvaluationFromAttribute(attribute),
),
attributesContainer,
],
);
}

List<AttributeGroup> _getAttributeGroupsToBeRendered() {
final List<AttributeGroup> attributeGroupsToBeRendered = <AttributeGroup>[];
for (final String groupId in _ATTRIBUTE_GROUP_ORDER) {
final Iterable<AttributeGroup> groupIterable = widget
._product.attributeGroups!
.where((AttributeGroup group) => group.id == groupId);
if (groupIterable.isEmpty) {
continue;
}
final AttributeGroup group = groupIterable.single;

final bool containsImportantAttributes = group.attributes!.any(
(Attribute attribute) =>
widget._productPreferences.isAttributeImportant(attribute.id!) ==
true);
if (containsImportantAttributes) {
attributeGroupsToBeRendered.add(group);
}
}
return attributeGroupsToBeRendered;
}

Widget _buildProductCompatibilityHeader(BuildContext context) {
final ProductCompatibility compatibility =
getProductCompatibility(widget._productPreferences, widget._product);
Expand Down Expand Up @@ -226,33 +249,14 @@ class _SummaryCardState extends State<SummaryCard> {
);
}

/// Builds an AttributeGroup, if [isFirstGroup] is true the group doesn't get
/// a divider header.
Widget _buildAttributeGroup(
BuildContext context,
AttributeGroup group,
bool isFirstGroup,
final Widget header,
final List<Widget> attributeChips,
) {
final List<Widget> attributeChips = <Widget>[];
for (final Attribute attribute in group.attributes!) {
final Widget? attributeChip = _buildAttributeChipForValidAttributes(
attribute: attribute,
returnNullIfStatusUnknown:
group.id == AttributeGroup.ATTRIBUTE_GROUP_LABELS,
);
if (attributeChip != null &&
attributeChips.length / 2 < totalPrintableRows) {
attributeChips.add(attributeChip);
}
}
if (attributeChips.isEmpty) {
return EMPTY_WIDGET;
}
totalPrintableRows =
totalPrintableRows - (attributeChips.length / 2).ceil();
totalPrintableRows -= (attributeChips.length / 2).ceil();
return Column(
children: <Widget>[
_buildAttributeGroupHeader(context, group, isFirstGroup),
header,
Container(
alignment: Alignment.topLeft,
child: Wrap(
Expand All @@ -264,19 +268,33 @@ class _SummaryCardState extends State<SummaryCard> {
);
}

/// The attribute group header can either be group name or a divider depending
/// upon the type of the group.
Widget _buildAttributeGroupHeader(
BuildContext context,
AttributeGroup group,
bool isFirstGroup,
) {
if (group.id == AttributeGroup.ATTRIBUTE_GROUP_ALLERGENS) {
List<Widget> _buildAttributeChips(
final List<Attribute> attributes, {
required final bool dontIncludeAnAttributeWhoseStatusIsUnknown,
}) {
final List<Widget> result = <Widget>[];
for (final Attribute attribute in attributes) {
final Widget? attributeChip = _buildAttributeChipForValidAttributes(
attribute: attribute,
returnNullIfStatusUnknown: dontIncludeAnAttributeWhoseStatusIsUnknown,
);
if (attributeChip != null && result.length / 2 < totalPrintableRows) {
result.add(attributeChip);
}
}
return result;
}

Widget _buildAttributeGroupHeader({
required bool isFirstGroup,
String? groupName,
}) {
if (groupName != null) {
return Container(
alignment: Alignment.topLeft,
padding: const EdgeInsets.only(top: SMALL_SPACE, bottom: LARGE_SPACE),
child: Text(
group.name!,
groupName,
style:
Theme.of(context).textTheme.bodyText2!.apply(color: Colors.grey),
),
Expand All @@ -296,15 +314,6 @@ class _SummaryCardState extends State<SummaryCard> {
required Attribute attribute,
required bool returnNullIfStatusUnknown,
}) {
if (attribute.id == null || _SCORE_ATTRIBUTE_IDS.contains(attribute.id)) {
// Score Attribute Ids have already been rendered.
return null;
}
if (widget._productPreferences.isAttributeImportant(attribute.id!) !=
true) {
// Not an important attribute.
return null;
}
if (returnNullIfStatusUnknown &&
attribute.status == Attribute.STATUS_UNKNOWN) {
return null;
Expand All @@ -327,4 +336,64 @@ class _SummaryCardState extends State<SummaryCard> {
]));
});
}

/// Returns the mandatory attributes, ordered by attribute group order
List<Attribute> _getMandatoryAttributes() {
final List<Attribute> result = <Attribute>[];
if (widget._product.attributeGroups == null) {
return result;
}
const List<String> filter = <String>[PreferenceImportance.ID_MANDATORY];
final Map<String, List<Attribute>> mandatoryAttributesByGroup =
<String, List<Attribute>>{};
// collecting all the mandatory attributes, by group
for (final AttributeGroup attributeGroup
in widget._product.attributeGroups!) {
mandatoryAttributesByGroup[attributeGroup.id!] =
_getOrderedAndFilteredAttributes(attributeGroup, filter);
}
// now ordering by attribute group order
for (final String attributeGroupId in _ATTRIBUTE_GROUP_ORDER) {
final List<Attribute>? attributes =
mandatoryAttributesByGroup[attributeGroupId];
if (attributes != null) {
result.addAll(attributes);
}
}
return result;
}

/// Returns the attributes that match the filter, ordered by filter order
///
/// [_SCORE_ATTRIBUTE_IDS] attributes are not included, as they are already
/// dealt with somewhere else.
List<Attribute> _getOrderedAndFilteredAttributes(
final AttributeGroup attributeGroup,
final List<String> orderedImportanceFilter,
) {
final List<Attribute> result = <Attribute>[];
if (attributeGroup.attributes == null) {
return result;
}
final Map<String, List<Attribute>> attributeByImportances =
<String, List<Attribute>>{};
for (final Attribute attribute in attributeGroup.attributes!) {
final String attributeId = attribute.id!;
if (_SCORE_ATTRIBUTE_IDS.contains(attributeId)) {
continue;
}
final String importanceId =
widget._productPreferences.getImportanceIdForAttributeId(attributeId);
if (orderedImportanceFilter.contains(importanceId)) {
attributeByImportances[importanceId] ??= <Attribute>[];
attributeByImportances[importanceId]!.add(attribute);
}
}
for (final String importanceId in orderedImportanceFilter) {
if (attributeByImportances[importanceId] != null) {
result.addAll(attributeByImportances[importanceId]!);
}
}
return result;
}
}

0 comments on commit 09a21c3

Please sign in to comment.