Skip to content

Commit

Permalink
feat: Create a screen listing all attributes for a product (#4714)
Browse files Browse the repository at this point in the history
* feat: Create a screen listing all attributes for a product

- resolves: #4673
- I have just added a simple button in edit product page just to test the
new page. It will be removed and added in the paticular screen.
- taking `edit_product_page.dart` as a base `product_attribute_page.dart`
is designed as suggested by teolemon.
- Added all the mentioned section field in the issue.

* refactor: chnages done according to the feedback

- created seperte file for `svg_icon.dar`
- created newFile for `attribute_first_row_widget.dart`
- removed refreshIndicator
- useed stringbuffer for string concatenation
- fix: linting errors

* fix: move nutrients extraction to init state

- to avoid recalculation on every build

* remove usage of knowledgepanel, use product.nutriments

* move ingredient extraction to initstate

* helper abstract class for attribute_first_row_widget

* remove ui file and abstract ontap function

* tiny fix

* change ontap function to future<void>

* Update packages/smooth_app/lib/pages/product/attribute_first_row_helper.dart

---------

Co-authored-by: monsieurtanuki <[email protected]>
  • Loading branch information
Sudhanva-Nadiger and monsieurtanuki authored Nov 4, 2023
1 parent 4ab2150 commit 21469af
Show file tree
Hide file tree
Showing 5 changed files with 354 additions and 39 deletions.
41 changes: 41 additions & 0 deletions packages/smooth_app/lib/generic_lib/widgets/svg_icon.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import 'dart:ui' as ui;

import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:smooth_app/generic_lib/design_constants.dart';
import 'package:smooth_app/helpers/app_helper.dart';

/// SVG that looks like a ListTile icon.
class SvgIcon extends StatelessWidget {
const SvgIcon(this.assetName, {this.dontAddColor = false});

final String assetName;
final bool dontAddColor;

@override
Widget build(BuildContext context) => SvgPicture.asset(
assetName,
height: DEFAULT_ICON_SIZE,
width: DEFAULT_ICON_SIZE,
colorFilter: dontAddColor
? null
: ui.ColorFilter.mode(
_iconColor(Theme.of(context)),
ui.BlendMode.srcIn,
),
package: AppHelper.APP_PACKAGE,
);

/// Returns the standard icon color in a [ListTile].
///
/// Simplified version from [ListTile], which was anyway not kind enough
/// to make it public.
Color _iconColor(ThemeData theme) {
switch (theme.brightness) {
case Brightness.light:
return Colors.black45;
case Brightness.dark:
return Colors.white;
}
}
}
4 changes: 4 additions & 0 deletions packages/smooth_app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1357,6 +1357,10 @@
"@edit_product_form_save": {
"description": "Product edition - Nutrition facts - Save button"
},
"no_data_available": "No data available",
"@no_data_available": {
"description": "When there are no data to display"
},
"product_field_website_title": "Website",
"@product_field_website_title": {
"description": "Title of a product field: website"
Expand Down
193 changes: 193 additions & 0 deletions packages/smooth_app/lib/pages/product/attribute_first_row_helper.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:smooth_app/generic_lib/widgets/svg_icon.dart';
import 'package:smooth_app/helpers/analytics_helper.dart';
import 'package:smooth_app/pages/product/common/product_refresher.dart';
import 'package:smooth_app/pages/product/nutrition_page_loaded.dart';
import 'package:smooth_app/pages/product/product_field_editor.dart';
import 'package:smooth_app/pages/product/simple_input_page_helpers.dart';

class StringPair {
const StringPair({
required this.first,
this.second,
});

final String first;
final String? second;
}

abstract class AttributeFirstRowHelper {
List<StringPair> getAllTerms();

Widget? getLeadingIcon();

String getTitle(BuildContext context);

Future<void> onTap({
required BuildContext context,
});
}

class AttributeFirstRowSimpleHelper extends AttributeFirstRowHelper {
AttributeFirstRowSimpleHelper({
required this.helper,
});

final AbstractSimpleInputPageHelper helper;

@override
List<StringPair> getAllTerms() {
final List<StringPair> allTerms = <StringPair>[];

for (final String element in helper.terms) {
allTerms.add(
StringPair(
first: element,
),
);
}

return allTerms;
}

@override
Widget? getLeadingIcon() {
return helper.getIcon();
}

@override
String getTitle(BuildContext context) {
final AppLocalizations appLocalizations = AppLocalizations.of(context);
return helper.getTitle(
appLocalizations,
);
}

@override
Future<void> onTap({
required BuildContext context,
}) {
return ProductFieldSimpleEditor(helper).edit(
context: context,
product: helper.product,
);
}
}

class AttributeFirstRowNutritionHelper extends AttributeFirstRowHelper {
AttributeFirstRowNutritionHelper({
required this.product,
});

final Product product;

@override
List<StringPair> getAllTerms() {
final List<StringPair> allNutrients = <StringPair>[];
product.nutriments?.toData().forEach(
(String nutrientName, String quantity) {
allNutrients.add(
StringPair(
first: nutrientName.split('_100g')[0],
second: quantity,
),
);
},
);

return allNutrients;
}

@override
Widget? getLeadingIcon() {
return const SvgIcon(
'assets/cacheTintable/scale-balance.svg',
dontAddColor: true,
);
}

@override
String getTitle(BuildContext context) {
final AppLocalizations appLocalizations = AppLocalizations.of(context);
return appLocalizations.nutrition_page_title;
}

@override
Future<void> onTap({
required BuildContext context,
}) async {
if (!await ProductRefresher().checkIfLoggedIn(
context,
isLoggedInMandatory: true,
)) {
return;
}

AnalyticsHelper.trackProductEdit(
AnalyticsEditEvents.nutrition_Facts,
product.barcode!,
);

if (!context.mounted) {
return;
}

await NutritionPageLoaded.showNutritionPage(
product: product,
isLoggedInMandatory: true,
context: context,
);
}
}

class AttributeFirstRowIngredientsHelper extends AttributeFirstRowHelper {
AttributeFirstRowIngredientsHelper({
required this.product,
});

final Product product;

@override
List<StringPair> getAllTerms() {
final List<StringPair> allIngredients = <StringPair>[];
product.ingredients?.forEach(
(Ingredient element) {
if (element.text != null) {
allIngredients.add(
StringPair(
first: element.text!,
),
);
}
},
);

return allIngredients;
}

@override
Widget? getLeadingIcon() {
return const SvgIcon(
'assets/cacheTintable/ingredients.svg',
dontAddColor: true,
);
}

@override
String getTitle(BuildContext context) {
final AppLocalizations appLocalizations = AppLocalizations.of(context);
return appLocalizations.ingredients;
}

@override
Future<void> onTap({
required BuildContext context,
}) {
return ProductFieldOcrIngredientEditor().edit(
context: context,
product: product,
);
}
}
112 changes: 112 additions & 0 deletions packages/smooth_app/lib/pages/product/attribute_first_row_widget.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:smooth_app/pages/product/attribute_first_row_helper.dart';

class AttributeFirstRowWidget extends StatefulWidget {
const AttributeFirstRowWidget({
required this.helper,
});

final AttributeFirstRowHelper helper;

@override
State<AttributeFirstRowWidget> createState() =>
_AttributeFirstRowWidgetState();
}

class _AttributeFirstRowWidgetState extends State<AttributeFirstRowWidget> {
bool _showAllTerms = false;
late final List<StringPair> allTerms;

@override
void initState() {
super.initState();
allTerms = widget.helper.getAllTerms();
}

@override
Widget build(BuildContext context) {
final AppLocalizations appLocalizations = AppLocalizations.of(context);
final ThemeData theme = Theme.of(context);
const int numberThreshold = 4;
final bool hasManyTerms = allTerms.length > numberThreshold;
final List<StringPair> firstTerms = allTerms
.take(
numberThreshold,
)
.toList();

if (firstTerms.isEmpty) {
firstTerms.add(
StringPair(first: appLocalizations.no_data_available),
);
}
return Column(
children: <Widget>[
ListTile(
leading: widget.helper.getLeadingIcon(),
title: Text(
widget.helper.getTitle(context),
),
trailing: const Icon(
Icons.edit,
),
titleTextStyle: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 20.0,
color: theme.primaryColor,
),
iconColor: theme.primaryColor,
tileColor: theme.colorScheme.secondary,
onTap: () async => widget.helper.onTap(context: context),
),
_termsList(
_showAllTerms ? allTerms : firstTerms,
borderFlag: !hasManyTerms,
),
if (hasManyTerms) ...<Widget>[
Padding(
padding: const EdgeInsets.only(left: 100.0),
child: ExpansionTile(
onExpansionChanged: (bool value) => setState(() {
_showAllTerms = value;
}),
title: const Text(
'Expand',
style: TextStyle(
decoration: TextDecoration.underline,
),
),
),
)
]
],
);
}

Widget _termsList(
List<StringPair> terms, {
bool borderFlag = false,
}) {
return ListView.builder(
padding: const EdgeInsets.only(left: 100.0),
itemCount: terms.length,
shrinkWrap: true,
itemBuilder: (_, int index) {
return ListTile(
title: Text(
terms[index].first,
style: const TextStyle(fontWeight: FontWeight.bold),
),
shape: (index == terms.length - 1 && borderFlag)
? null
: const Border(
bottom: BorderSide(),
),
trailing:
terms[index].second != null ? Text(terms[index].second!) : null,
);
},
);
}
}
Loading

0 comments on commit 21469af

Please sign in to comment.