diff --git a/packages/smooth_app/lib/helpers/time_helper.dart b/packages/smooth_app/lib/helpers/time_helper.dart new file mode 100644 index 00000000000..9ae6f57e18d --- /dev/null +++ b/packages/smooth_app/lib/helpers/time_helper.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +String getDaysAgoLabel(BuildContext context, final int daysAgo) { + final AppLocalizations appLocalizations = AppLocalizations.of(context)!; + final int weeksAgo = (daysAgo.toDouble() / 7).round(); + final int monthsAgo = (daysAgo.toDouble() / (365.25 / 12)).round(); + if (daysAgo == 0) { + return appLocalizations.today; + } + if (daysAgo == 1) { + return appLocalizations.yesterday; + } + if (daysAgo < 7) { + return appLocalizations.plural_ago_days(daysAgo); + } + if (weeksAgo == 1) { + return appLocalizations.plural_ago_weeks(1); + } + if (monthsAgo == 0) { + return appLocalizations.plural_ago_weeks(weeksAgo); + } + if (monthsAgo == 1) { + return appLocalizations.plural_ago_months(1); + } + return appLocalizations.plural_ago_months(monthsAgo); +} \ No newline at end of file diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index 47cf9750bf1..6f58ff9d8dc 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -182,6 +182,8 @@ "@my_pantry_hint": {}, "my_shopping_hint": "My shopping list", "@my_shopping_hint": {}, + "today": "Today", + "yesterday": "Yesterday", "my_list_hint": "My custom list", "@my_list_hint": {}, "enter_text": "Please enter some text", @@ -198,6 +200,7 @@ "my_shopping_lists": "My shopping lists", "my_lists": "My lists", "food_categories": "Food categories", + "history": "History", "search_history": "Search history", "label_preferences": "preferences", "@label_preferences": {}, diff --git a/packages/smooth_app/lib/pages/history_page.dart b/packages/smooth_app/lib/pages/history_page.dart index 3a2ee3ec24b..3f9e553d280 100644 --- a/packages/smooth_app/lib/pages/history_page.dart +++ b/packages/smooth_app/lib/pages/history_page.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; 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/product/common/product_list_page.dart'; +import 'package:smooth_app/pages/product/common/product_list_widget.dart'; +import 'package:smooth_app/pages/smooth_bottom_navigation_bar.dart'; class HistoryPage extends StatefulWidget { const HistoryPage(); @@ -29,6 +31,17 @@ class _HistoryPageState extends State { @override Widget build(BuildContext context) { - return ProductListPage(productList); + final AppLocalizations appLocalizations = AppLocalizations.of(context)!; + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + title: Text(appLocalizations.history), + ), + body: ProductListWidget(productList), + bottomNavigationBar: SmoothBottomNavigationBar( + tab: SmoothBottomNavigationTab.History, + ), + ); } } diff --git a/packages/smooth_app/lib/pages/product/common/product_list_page.dart b/packages/smooth_app/lib/pages/product/common/product_list_page.dart index 1947300506b..3f64956fcad 100644 --- a/packages/smooth_app/lib/pages/product/common/product_list_page.dart +++ b/packages/smooth_app/lib/pages/product/common/product_list_page.dart @@ -1,14 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:openfoodfacts/model/Product.dart'; import 'package:provider/provider.dart'; -import 'package:smooth_app/data_models/product_extra.dart'; 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.dart'; +import 'package:smooth_app/pages/product/common/product_list_widget.dart'; import 'package:smooth_app/pages/product/common/product_query_page_helper.dart'; import 'package:smooth_app/pages/smooth_bottom_navigation_bar.dart'; import 'package:smooth_app/themes/smooth_theme.dart'; @@ -36,30 +34,6 @@ class _ProductListPageState extends State { first = false; productList = widget.productList; } - final List products = productList.getList(); - final Map productExtras = productList.productExtras; - final List<_Meta> metas = <_Meta>[]; - if (productList.listType == ProductList.LIST_TYPE_HISTORY || - productList.listType == ProductList.LIST_TYPE_SCAN_HISTORY || - productList.listType == ProductList.LIST_TYPE_SCAN_SESSION) { - final int nowInMillis = LocalDatabase.nowInMillis(); - const int DAY_IN_MILLIS = 24 * 3600 * 1000; - String? daysAgoLabel; - for (final Product product in products) { - final int timestamp = productExtras[product.barcode]!.intValue; - final int daysAgo = ((nowInMillis - timestamp) / DAY_IN_MILLIS).round(); - final String tmpDaysAgoLabel = _getDaysAgoLabel(daysAgo); - if (daysAgoLabel != tmpDaysAgoLabel) { - daysAgoLabel = tmpDaysAgoLabel; - metas.add(_Meta.daysAgoLabel(daysAgoLabel)); - } - metas.add(_Meta.product(product)); - } - } else { - for (final Product product in products) { - metas.add(_Meta.product(product)); - } - } bool renamable = false; bool deletable = false; bool dismissible = false; @@ -188,7 +162,7 @@ class _ProductListPageState extends State { ), ], ), - floatingActionButton: metas.isEmpty + floatingActionButton: productList.isEmpty() ? null : FloatingActionButton( child: const Icon(Icons.emoji_events_outlined), @@ -203,104 +177,18 @@ class _ProductListPageState extends State { setState(() {}); }, ), - body: metas.isEmpty + body: productList.isEmpty() ? Center( child: Text(appLocalizations.no_prodcut_in_list, style: Theme.of(context).textTheme.subtitle1), ) - : ReorderableListView.builder( - onReorder: (final int oldIndex, final int newIndex) async { - productList.reorder(oldIndex, newIndex); - daoProductList - .put(productList); // careful: if "await", flickering - setState(() {}); - }, - buildDefaultDragHandles: false, - itemCount: metas.length, - itemBuilder: (BuildContext context, int index) { - final _Meta meta = metas[index]; - if (!meta.isProduct()) { - return ListTile( - key: Key(meta.daysAgoLabel!), - leading: const Icon(Icons.history), - title: Text(meta.daysAgoLabel!), - ); - } - final Product product = meta.product!; - final Widget child = Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, vertical: 8.0), - child: ProductListItem( - product: product, - productList: productList, - listRefresher: () => setState(() {}), - daoProductList: daoProductList, - reorderIndex: reorderable ? index : null, - ), - ); - if (dismissible) { - return Dismissible( - background: Container(color: colorScheme.background), - key: Key(product.barcode!), - onDismissed: (final DismissDirection direction) async { - final bool removed = productList.remove(product.barcode!); - if (removed) { - await daoProductList.put(productList); - setState(() => metas.removeAt(index)); - } - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(removed - ? 'Product removed' - : 'Could not remove product'), - duration: const Duration(seconds: 3), - ), - ); - // TODO(monsieurtanuki): add a snackbar ("put back the food") - }, - child: child, - ); - } - return Container( - key: Key(product.barcode!), - child: child, - ); - }, - ), + : ProductListWidget(productList, + reorderable: reorderable, + timestamps: productList.listType == + ProductList.LIST_TYPE_HISTORY || + productList.listType == ProductList.LIST_TYPE_SCAN_HISTORY || + productList.listType == ProductList.LIST_TYPE_SCAN_SESSION, + dismissible: dismissible), ); } - - static String _getDaysAgoLabel(final int daysAgo) { - final int weeksAgo = (daysAgo.toDouble() / 7).round(); - final int monthsAgo = (daysAgo.toDouble() / (365.25 / 12)).round(); - if (daysAgo == 0) { - return 'Today'; - } - if (daysAgo == 1) { - return 'Yesterday'; - } - if (daysAgo < 7) { - return '$daysAgo days ago'; - } - if (weeksAgo == 1) { - return 'One week ago'; - } - if (monthsAgo == 0) { - return '$weeksAgo weeks ago'; - } - if (monthsAgo == 1) { - return 'One month ago'; - } - return '$monthsAgo months ago'; - } -} - -class _Meta { - _Meta.product(this.product) : daysAgoLabel = null; - _Meta.daysAgoLabel(this.daysAgoLabel) : product = null; - - final Product? product; - final String? daysAgoLabel; - - bool isProduct() => product != null; } diff --git a/packages/smooth_app/lib/pages/product/common/product_list_widget.dart b/packages/smooth_app/lib/pages/product/common/product_list_widget.dart new file mode 100644 index 00000000000..56bfd31d5f2 --- /dev/null +++ b/packages/smooth_app/lib/pages/product/common/product_list_widget.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:openfoodfacts/model/Product.dart'; +import 'package:provider/provider.dart'; +import 'package:smooth_app/data_models/product_extra.dart'; +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/product/common/product_list_item.dart'; +import 'package:smooth_app/helpers/time_helper.dart'; + +class ProductListWidget extends StatefulWidget { + const ProductListWidget( + this.productList, { + Key? key, + this.reorderable = false, + this.timestamps = false, + this.dismissible = false, + }) : super(key: key); + final ProductList productList; + final bool timestamps; + final bool reorderable; + final bool dismissible; + @override + State createState() => _ProductListWidgetState(); +} + +class _ProductListWidgetState extends State { + @override + Widget build(BuildContext context) { + final LocalDatabase localDatabase = context.watch(); + final DaoProductList daoProductList = DaoProductList(localDatabase); + final List<_Meta> metas = <_Meta>[]; + final List products = widget.productList.getList(); + final Map productExtras = + widget.productList.productExtras; + final ColorScheme colorScheme = Theme.of(context).colorScheme; + if (widget.timestamps) { + final int nowInMillis = LocalDatabase.nowInMillis(); + const int DAY_IN_MILLIS = 24 * 3600 * 1000; + String? daysAgoLabel; + for (final Product product in products) { + final int timestamp = productExtras[product.barcode]!.intValue; + final int daysAgo = ((nowInMillis - timestamp) / DAY_IN_MILLIS).round(); + final String tmpDaysAgoLabel = getDaysAgoLabel(context, daysAgo); + if (daysAgoLabel != tmpDaysAgoLabel) { + daysAgoLabel = tmpDaysAgoLabel; + metas.add(_Meta.daysAgoLabel(daysAgoLabel)); + } + metas.add(_Meta.product(product)); + } + } else { + for (final Product product in products) { + metas.add(_Meta.product(product)); + } + } + return ReorderableListView.builder( + onReorder: (final int oldIndex, final int newIndex) async { + widget.productList.reorder(oldIndex, newIndex); + daoProductList + .put(widget.productList); // careful: if "await", flickering + setState(() {}); + }, + buildDefaultDragHandles: false, + itemCount: metas.length, + itemBuilder: (BuildContext context, int index) { + final _Meta meta = metas[index]; + if (!meta.isProduct()) { + return ListTile( + key: Key(meta.daysAgoLabel!), + leading: const Icon(Icons.history), + title: Text(meta.daysAgoLabel!), + ); + } + final Product product = meta.product!; + final Widget child = Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: ProductListItem( + product: product, + productList: widget.productList, + listRefresher: () => setState(() {}), + daoProductList: daoProductList, + reorderIndex: widget.reorderable ? index : null, + ), + ); + if (widget.dismissible) { + return Dismissible( + background: Container(color: colorScheme.background), + key: Key(product.barcode!), + onDismissed: (final DismissDirection direction) async { + final bool removed = widget.productList.remove(product.barcode!); + if (removed) { + await daoProductList.put(widget.productList); + setState(() => metas.removeAt(index)); + } + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + removed ? 'Product removed' : 'Could not remove product'), + duration: const Duration(seconds: 3), + ), + ); + // TODO(monsieurtanuki): add a snackbar ("put back the food") + }, + child: child, + ); + } + return Container( + key: Key(product.barcode!), + child: child, + ); + }, + ); + } +} + +class _Meta { + _Meta.product(this.product) : daysAgoLabel = null; + _Meta.daysAgoLabel(this.daysAgoLabel) : product = null; + + final Product? product; + final String? daysAgoLabel; + + bool isProduct() => product != null; +}