Skip to content

Commit

Permalink
feat: #878 - added a multiselect mode to product list page (#1035)
Browse files Browse the repository at this point in the history
Impacted files:
* `app_en.arb`: added a "compare mode" label
* `app_fr.arb`: added a "compare mode" label
* `personalized_ranking_page.dart`: added a `fromItems` constructor for more flexibility
* `product_list_item_simple.dart`: added optional `onTap` parameter in order to override the default behavior when checking/unchecking, and an optional `onLongPress` parameter in order to trigger selection mode
* `product_list_page.dart`: added a "multiselect" mode and a header with buttons; removed FABs; added longPress-to-selection-mode feature
* `product_query.dart`: unrelated removal of a harmless duplicate
* `product_query_page.dart`: refactored
* `ranking_floating_action_button.dart`: added a comment about getting rid of this class because of possible confusion
* `scan_page_helper.dart`: refactored
* `smooth_it_model.dart`: for flexibility, now using `List<Product>` instead of `ProductList`
* `smooth_product_card_found.dart`: added optional `onTap` parameter in order to override the default behavior when checking/unchecking
  • Loading branch information
monsieurtanuki authored Jan 31, 2022
1 parent 1b02310 commit 44cd470
Show file tree
Hide file tree
Showing 11 changed files with 152 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class SmoothProductCardFound extends StatelessWidget {
this.handle,
this.onLongPress,
this.refresh,
this.onTap,
});

final Product product;
Expand All @@ -29,6 +30,7 @@ class SmoothProductCardFound extends StatelessWidget {
final Widget? handle;
final VoidCallback? onLongPress;
final VoidCallback? refresh;
final VoidCallback? onTap;

@override
Widget build(BuildContext context) {
Expand All @@ -47,15 +49,16 @@ class SmoothProductCardFound extends StatelessWidget {
final ProductCompatibilityResult compatibility =
getProductCompatibility(context.watch<ProductPreferences>(), product);
return GestureDetector(
onTap: () async {
await Navigator.push<Widget>(
context,
MaterialPageRoute<Widget>(
builder: (BuildContext context) => ProductPage(product),
),
);
refresh?.call();
},
onTap: onTap ??
() async {
await Navigator.push<Widget>(
context,
MaterialPageRoute<Widget>(
builder: (BuildContext context) => ProductPage(product),
),
);
refresh?.call();
},
onLongPress: () {
onLongPress?.call();
},
Expand Down
6 changes: 2 additions & 4 deletions packages/smooth_app/lib/data_models/smooth_it_model.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import 'package:openfoodfacts/model/Product.dart';
import 'package:openfoodfacts/personalized_search/matched_product.dart';
import 'package:smooth_app/data_models/product_list.dart';
import 'package:smooth_app/data_models/product_preferences.dart';

/// Tabs where ranked products are displayed
Expand All @@ -16,12 +15,11 @@ class SmoothItModel {
<MatchTab, List<MatchedProduct>>{};

void refresh(
final ProductList productList,
final List<Product> products,
final ProductPreferences productPreferences,
) {
final List<Product> unprocessedProducts = productList.getList();
final List<MatchedProduct> allProducts =
MatchedProduct.sort(unprocessedProducts, productPreferences);
MatchedProduct.sort(products, productPreferences);
_categorizedProducts.clear();
_categorizedProducts[MatchTab.ALL] = allProducts;
for (final MatchedProduct matchedProduct in allProducts) {
Expand Down
1 change: 0 additions & 1 deletion packages/smooth_app/lib/database/product_query.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ abstract class ProductQuery {
ProductField.ECOSCORE_DATA,
ProductField.ECOSCORE_GRADE,
ProductField.ECOSCORE_SCORE,
ProductField.ENVIRONMENT_IMPACT_LEVELS,
];

Future<SearchResult> getSearchResult();
Expand Down
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 @@ -566,5 +566,9 @@
"placeholders": {
"count": {}
}
},
"compare_products_mode": "Compare Mode",
"@compare_products_mode": {
"description": "Button to switch to 'compare products mode'"
}
}
4 changes: 4 additions & 0 deletions packages/smooth_app/lib/l10n/app_fr.arb
Original file line number Diff line number Diff line change
Expand Up @@ -520,5 +520,9 @@
"placeholders": {
"count": {}
}
},
"compare_products_mode": "Comparer",
"@compare_products_mode": {
"description": "Bouton qui permet de rentrer en mode 'comparer les produits'"
}
}
29 changes: 15 additions & 14 deletions packages/smooth_app/lib/pages/personalized_ranking_page.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:openfoodfacts/personalized_search/matched_product.dart';
import 'package:provider/provider.dart';
import 'package:smooth_app/cards/product_cards/smooth_product_card_found.dart';
Expand All @@ -8,12 +9,20 @@ import 'package:smooth_app/data_models/product_preferences.dart';
import 'package:smooth_app/data_models/smooth_it_model.dart';
import 'package:smooth_app/database/dao_product_list.dart';
import 'package:smooth_app/database/local_database.dart';
import 'package:smooth_app/pages/product/common/product_query_page_helper.dart';

class PersonalizedRankingPage extends StatefulWidget {
const PersonalizedRankingPage(this.productList);
PersonalizedRankingPage({
required final ProductList productList,
required this.title,
}) : products = productList.getList();

final ProductList productList;
const PersonalizedRankingPage.fromItems({
required this.products,
required this.title,
});

final List<Product> products;
final String title;

@override
State<PersonalizedRankingPage> createState() =>
Expand Down Expand Up @@ -43,7 +52,7 @@ class _PersonalizedRankingPageState extends State<PersonalizedRankingPage> {
context.watch<ProductPreferences>();
final LocalDatabase localDatabase = context.watch<LocalDatabase>();
final DaoProductList daoProductList = DaoProductList(localDatabase);
_model.refresh(widget.productList, productPreferences);
_model.refresh(widget.products, productPreferences);
final AppLocalizations appLocalizations = AppLocalizations.of(context)!;
final List<Color> colors = <Color>[];
final List<String> titles = <String>[];
Expand Down Expand Up @@ -96,13 +105,7 @@ class _PersonalizedRankingPageState extends State<PersonalizedRankingPage> {
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Flexible(
child: Text(
ProductQueryPageHelper.getProductListLabel(
widget.productList,
context,
),
overflow: TextOverflow.fade,
),
child: Text(widget.title, overflow: TextOverflow.fade),
),
],
),
Expand Down Expand Up @@ -130,10 +133,8 @@ class _PersonalizedRankingPageState extends State<PersonalizedRankingPage> {
Dismissible(
key: Key(matchedProduct.product.barcode!),
onDismissed: (final DismissDirection direction) async {
final bool removed =
widget.productList.remove(matchedProduct.product.barcode!);
final bool removed = widget.products.remove(matchedProduct.product);
if (removed) {
await daoProductList.put(widget.productList);
setState(() {});
}
ScaffoldMessenger.of(context).showSnackBar(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,21 @@ import 'package:smooth_app/data_models/product_list.dart';

/// Widget for a [ProductList] item (simple product list)
class ProductListItemSimple extends StatelessWidget {
const ProductListItemSimple({required this.product});
const ProductListItemSimple({
required this.product,
this.onTap,
this.onLongPress,
});

final Product product;
final VoidCallback? onTap;
final VoidCallback? onLongPress;

@override
Widget build(BuildContext context) => SmoothProductCardFound(
heroTag: product.barcode!,
product: product,
onTap: onTap,
onLongPress: onLongPress,
);
}
149 changes: 94 additions & 55 deletions packages/smooth_app/lib/pages/product/common/product_list_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ import 'package:smooth_app/data_models/product_list.dart';
import 'package:smooth_app/database/dao_product_list.dart';
import 'package:smooth_app/database/local_database.dart';
import 'package:smooth_app/pages/personalized_ranking_page.dart';
import 'package:smooth_app/pages/product/common/product_list_dialog_helper.dart';
import 'package:smooth_app/pages/product/common/product_list_item_simple.dart';
import 'package:smooth_app/pages/product/common/product_query_page_helper.dart';
import 'package:smooth_app/widgets/ranking_floating_action_button.dart';

class ProductListPage extends StatefulWidget {
const ProductListPage(this.productList);
Expand All @@ -23,6 +21,8 @@ class ProductListPage extends StatefulWidget {
class _ProductListPageState extends State<ProductListPage> {
late ProductList productList;
bool first = true;
final Set<String> _selectedBarcodes = <String>{};
bool _selectionMode = false;

@override
Widget build(BuildContext context) {
Expand All @@ -48,62 +48,66 @@ class _ProductListPageState extends State<ProductListPage> {
}
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.white, // TODO(monsieurtanuki): night mode
foregroundColor: Colors.black,
title: Row(
mainAxisAlignment: _selectionMode && _selectedBarcodes.isEmpty
? MainAxisAlignment.end // just the cancel button, at the end
: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(
child: Text(
ProductQueryPageHelper.getProductListLabel(
productList,
context,
verbose: false,
if (_selectionMode && _selectedBarcodes.isNotEmpty)
ElevatedButton(
child: Text(
appLocalizations.plural_compare_x_products(
_selectedBarcodes.length,
),
),
onPressed: () async {
final List<Product> list = <Product>[];
for (final Product product in products) {
if (_selectedBarcodes.contains(product.barcode)) {
list.add(product);
}
}
await Navigator.push<Widget>(
context,
MaterialPageRoute<Widget>(
builder: (BuildContext context) =>
PersonalizedRankingPage.fromItems(
products: list,
title: 'Your ranking',
),
),
);
setState(() => _selectionMode = false);
},
),
if (_selectionMode)
ElevatedButton(
onPressed: () => setState(() => _selectionMode = false),
child: Text(appLocalizations.cancel),
),
if (!_selectionMode)
Flexible(
child: Text(
ProductQueryPageHelper.getProductListLabel(
productList,
context,
verbose: false,
),
overflow: TextOverflow.fade,
),
),
if ((!_selectionMode) && products.isNotEmpty)
Flexible(
child: ElevatedButton(
child: Text(appLocalizations.compare_products_mode),
onPressed: () => setState(() => _selectionMode = true),
),
overflow: TextOverflow.fade,
),
),
],
),
actions: !dismissible
? null
: <Widget>[
PopupMenuButton<String>(
itemBuilder: (final BuildContext context) =>
<PopupMenuEntry<String>>[
PopupMenuItem<String>(
value: 'clear',
child: Text(appLocalizations.clear),
enabled: true,
),
],
onSelected: (final String value) async {
switch (value) {
case 'clear':
if (await ProductListDialogHelper.instance
.openClear(context, daoProductList, productList)) {
localDatabase.notifyListeners();
}
break;
default:
throw Exception('Unknown value: $value');
}
},
),
],
),
floatingActionButton: products.isEmpty
? null
: RankingFloatingActionButton(
color: Colors.black,
onPressed: () async {
await Navigator.push<Widget>(
context,
MaterialPageRoute<Widget>(
builder: (BuildContext context) =>
PersonalizedRankingPage(productList),
),
);
setState(() {});
},
),
body: products.isEmpty
? Center(
child: Text(appLocalizations.no_prodcut_in_list,
Expand All @@ -113,10 +117,44 @@ class _ProductListPageState extends State<ProductListPage> {
itemCount: products.length,
itemBuilder: (BuildContext context, int index) {
final Product product = products[index];
final Widget child = Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0, vertical: 8.0),
child: ProductListItemSimple(product: product),
final String barcode = product.barcode!;
final bool selected = _selectedBarcodes.contains(barcode);
void onTap() => setState(
() {
if (selected) {
_selectedBarcodes.remove(barcode);
} else {
_selectedBarcodes.add(barcode);
}
},
);
final Widget child = GestureDetector(
onTap: _selectionMode ? onTap : null,
child: Container(
padding: EdgeInsets.symmetric(
horizontal: _selectionMode ? 0 : 12.0,
vertical: 8.0,
),
child: Row(
children: <Widget>[
if (_selectionMode)
Icon(
selected
? Icons.check_box
: Icons.check_box_outline_blank,
),
Expanded(
child: ProductListItemSimple(
product: product,
onTap: _selectionMode ? onTap : null,
onLongPress: !_selectionMode
? () => setState(() => _selectionMode = true)
: null,
),
),
],
),
),
);
if (dismissible) {
return Dismissible(
Expand All @@ -126,6 +164,7 @@ class _ProductListPageState extends State<ProductListPage> {
final bool removed = productList.remove(product.barcode!);
if (removed) {
await daoProductList.put(productList);
_selectedBarcodes.remove(product.barcode);
setState(() => products.removeAt(index));
}
ScaffoldMessenger.of(context).showSnackBar(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,11 @@ class _ProductQueryPageState extends State<ProductQueryPage> {
context,
MaterialPageRoute<Widget>(
builder: (BuildContext context) => PersonalizedRankingPage(
_model.supplier.getProductList(),
productList: _model.supplier.getProductList(),
title: ProductQueryPageHelper.getProductListLabel(
_model.supplier.getProductList(),
context,
),
),
),
),
Expand Down
Loading

0 comments on commit 44cd470

Please sign in to comment.