Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: #878 - added a multiselect mode to product list page #1035

Merged
merged 35 commits into from
Jan 31, 2022
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
66a4f82
Merge pull request #20 from openfoodfacts/develop
monsieurtanuki Jan 3, 2022
a6ed5fe
Merge branch 'openfoodfacts:develop' into develop
monsieurtanuki Jan 3, 2022
f1eb63f
Merge branch 'openfoodfacts:develop' into develop
monsieurtanuki Jan 3, 2022
dd4338a
Merge branch 'openfoodfacts:develop' into develop
monsieurtanuki Jan 4, 2022
1811829
Merge branch 'openfoodfacts:develop' into develop
monsieurtanuki Jan 4, 2022
8b14423
Merge branch 'openfoodfacts:develop' into develop
monsieurtanuki Jan 4, 2022
52b8ae3
Merge branch 'openfoodfacts:develop' into develop
monsieurtanuki Jan 5, 2022
96bb3f1
Merge branch 'openfoodfacts:develop' into develop
monsieurtanuki Jan 6, 2022
7840c45
Merge branch 'openfoodfacts:develop' into develop
monsieurtanuki Jan 6, 2022
241785b
Merge branch 'openfoodfacts:develop' into develop
monsieurtanuki Jan 6, 2022
edd46be
Merge branch 'openfoodfacts:develop' into develop
monsieurtanuki Jan 7, 2022
7e6f576
Merge branch 'openfoodfacts:develop' into develop
monsieurtanuki Jan 8, 2022
32c1433
Merge branch 'openfoodfacts:develop' into develop
monsieurtanuki Jan 9, 2022
6cc8541
Merge branch 'openfoodfacts:develop' into develop
monsieurtanuki Jan 10, 2022
d680373
Merge branch 'openfoodfacts:develop' into develop
monsieurtanuki Jan 10, 2022
c0ea17d
Merge branch 'openfoodfacts:develop' into develop
monsieurtanuki Jan 13, 2022
b257696
Merge branch 'openfoodfacts:develop' into develop
monsieurtanuki Jan 13, 2022
2555a12
Merge branch 'openfoodfacts:develop' into develop
monsieurtanuki Jan 13, 2022
077a938
Merge branch 'openfoodfacts:develop' into develop
monsieurtanuki Jan 14, 2022
c264e7f
Merge branch 'openfoodfacts:develop' into develop
monsieurtanuki Jan 14, 2022
f568686
Merge branch 'openfoodfacts:develop' into develop
monsieurtanuki Jan 17, 2022
07af801
Merge branch 'openfoodfacts:develop' into develop
monsieurtanuki Jan 18, 2022
f73efb3
Merge branch 'openfoodfacts:develop' into develop
monsieurtanuki Jan 19, 2022
d141a78
Merge branch 'openfoodfacts:develop' into develop
monsieurtanuki Jan 20, 2022
791e510
Merge branch 'openfoodfacts:develop' into develop
monsieurtanuki Jan 22, 2022
e9ab185
Merge branch 'openfoodfacts:develop' into develop
monsieurtanuki Jan 22, 2022
9cc2025
Merge branch 'openfoodfacts:develop' into develop
monsieurtanuki Jan 24, 2022
55533c8
Merge branch 'openfoodfacts:develop' into develop
monsieurtanuki Jan 24, 2022
76283ff
Merge branch 'openfoodfacts:develop' into develop
monsieurtanuki Jan 27, 2022
3bda01d
feat: #878 - added a multiselect mode to product list page
monsieurtanuki Jan 28, 2022
5dc6b48
feat: #878 - added header with buttons; removed FABs
monsieurtanuki Jan 28, 2022
b4098ba
feat: #878 - one cancel button is enough
monsieurtanuki Jan 28, 2022
ef6c91a
feat: #878 - closer to the mocks
monsieurtanuki Jan 28, 2022
aa39bb8
feat: #878 - unrelated minor refactoring
monsieurtanuki Jan 29, 2022
6923e96
feat: #878 - UX fine-tuning
monsieurtanuki Jan 31, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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,18 @@ 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,
});

final Product product;
final VoidCallback? onTap;

@override
Widget build(BuildContext context) => SmoothProductCardFound(
heroTag: product.barcode!,
product: product,
onTap: onTap,
);
}
142 changes: 87 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,62 @@ class _ProductListPageState extends State<ProductListPage> {
}
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.white, // TODO(monsieurtanuki): night mode
foregroundColor: Colors.black,
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(
child: Text(
ProductQueryPageHelper.getProductListLabel(
productList,
context,
verbose: false,
if (_selectionMode && _selectedBarcodes.isNotEmpty)
ElevatedButton(
child: Text(
'Compare ${_selectedBarcodes.length} products'), // TODO(monsieurtanuki): localize
monsieurtanuki marked this conversation as resolved.
Show resolved Hide resolved
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: const Text(
'Compare Mode'), // TODO(monsieurtanuki): localize
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 +113,41 @@ 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,
),
),
],
),
),
);
if (dismissible) {
return Dismissible(
Expand All @@ -126,6 +157,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
7 changes: 6 additions & 1 deletion packages/smooth_app/lib/pages/scan/scan_page_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
import 'package:smooth_app/data_models/continuous_scan_model.dart';
import 'package:smooth_app/helpers/ui_helpers.dart';
import 'package:smooth_app/pages/personalized_ranking_page.dart';
import 'package:smooth_app/pages/product/common/product_query_page_helper.dart';
import 'package:smooth_app/widgets/ranking_floating_action_button.dart';

bool areButtonsRendered(ContinuousScanModel model) =>
Expand All @@ -16,7 +17,11 @@ Future<void> openPersonalizedRankingPage(BuildContext context) async {
context,
MaterialPageRoute<Widget>(
builder: (BuildContext context) => PersonalizedRankingPage(
model.productList,
productList: model.productList,
title: ProductQueryPageHelper.getProductListLabel(
model.productList,
context,
),
),
),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:smooth_app/generic_lib/animations/smooth_reveal_animation.dart';

// TODO(monsieurtanuki): we should probably remove that class to avoid confusion with the "compare" button
/// Floating Action Button dedicated to Personal Ranking
class RankingFloatingActionButton extends StatelessWidget {
const RankingFloatingActionButton({
Expand Down