diff --git a/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart b/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart index 04b2e2762b8..f6b7a9ac755 100644 --- a/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart +++ b/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart @@ -18,7 +18,6 @@ import 'package:smooth_app/pages/preferences/user_preferences_item.dart'; import 'package:smooth_app/pages/preferences/user_preferences_list_tile.dart'; import 'package:smooth_app/pages/preferences/user_preferences_page.dart'; import 'package:smooth_app/pages/prices/get_prices_model.dart'; -import 'package:smooth_app/pages/prices/price_meta_product.dart'; import 'package:smooth_app/pages/prices/price_user_button.dart'; import 'package:smooth_app/pages/prices/prices_page.dart'; import 'package:smooth_app/pages/prices/prices_proofs_page.dart'; @@ -243,7 +242,6 @@ class UserPreferencesAccount extends AbstractUserPreferences { appLocalizations.prices_add_a_receipt, () async => ProductPriceAddPage.showProductPage( context: context, - product: PriceMetaProduct.empty(), proofType: ProofType.receipt, ), Icons.add_shopping_cart, @@ -252,7 +250,6 @@ class UserPreferencesAccount extends AbstractUserPreferences { appLocalizations.prices_add_price_tags, () async => ProductPriceAddPage.showProductPage( context: context, - product: PriceMetaProduct.empty(), proofType: ProofType.priceTag, ), Icons.add_shopping_cart, diff --git a/packages/smooth_app/lib/pages/prices/price_add_product_card.dart b/packages/smooth_app/lib/pages/prices/price_add_product_card.dart new file mode 100644 index 00000000000..d385285e017 --- /dev/null +++ b/packages/smooth_app/lib/pages/prices/price_add_product_card.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; +import 'package:smooth_app/database/local_database.dart'; +import 'package:smooth_app/generic_lib/buttons/smooth_large_button_with_icon.dart'; +import 'package:smooth_app/generic_lib/dialogs/smooth_alert_dialog.dart'; +import 'package:smooth_app/generic_lib/widgets/smooth_card.dart'; +import 'package:smooth_app/generic_lib/widgets/smooth_text_form_field.dart'; +import 'package:smooth_app/pages/prices/price_amount_model.dart'; +import 'package:smooth_app/pages/prices/price_meta_product.dart'; +import 'package:smooth_app/pages/prices/price_model.dart'; +import 'package:smooth_app/pages/prices/price_scan_page.dart'; + +/// Card where the user can input a price product: type the barcode or scan. +class PriceAddProductCard extends StatelessWidget { + const PriceAddProductCard(); + + static const TextInputType _textInputType = TextInputType.number; + + @override + Widget build(BuildContext context) { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + return SmoothCard( + child: Column( + children: [ + ListTile( + title: Text( + appLocalizations.prices_add_an_item, + ), + ), + SmoothLargeButtonWithIcon( + text: appLocalizations.barcode, + icon: Icons.text_fields, + onPressed: () async { + final TextEditingController controller = TextEditingController(); + final String? barcode = await showDialog( + context: context, + builder: (final BuildContext context) => StatefulBuilder( + builder: ( + final BuildContext context, + void Function(VoidCallback fn) setState, + ) => + SmoothAlertDialog( + title: appLocalizations.prices_add_an_item, + body: SmoothTextFormField( + autofocus: true, + type: TextFieldTypes.PLAIN_TEXT, + controller: controller, + hintText: appLocalizations.barcode, + textInputType: _textInputType, + onChanged: (_) { + final String barcode = controller.text; + final String cleanBarcode = _getCleanBarcode(barcode); + setState(() => controller.text = cleanBarcode); + }, + onFieldSubmitted: (_) => !_isValidBarcode(controller.text) + ? null + : Navigator.of(context).pop(controller.text), + ), + positiveAction: SmoothActionButton( + text: appLocalizations.validate, + onPressed: !_isValidBarcode(controller.text) + ? null + : () => Navigator.of(context).pop(controller.text), + ), + negativeAction: SmoothActionButton( + text: appLocalizations.cancel, + onPressed: () => Navigator.of(context).pop(), + ), + ), + ), + ); + if (barcode == null) { + return; + } + if (!context.mounted) { + return; + } + await _addToList(barcode, context); + }, + ), + SmoothLargeButtonWithIcon( + text: appLocalizations.prices_barcode_reader_action, + icon: Icons.barcode_reader, + onPressed: () async { + final String? barcode = await Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => const PriceScanPage(), + ), + ); + if (barcode == null) { + return; + } + if (!context.mounted) { + return; + } + await _addToList(barcode, context); + }, + ), + ], + ), + ); + } + + Future _addToList( + final String barcode, + final BuildContext context, + ) async { + final LocalDatabase localDatabase = context.read(); + final PriceModel priceModel = Provider.of( + context, + listen: false, + ); + priceModel.priceAmountModels.add( + PriceAmountModel( + product: PriceMetaProduct.unknown( + barcode, + localDatabase, + priceModel, + ), + ), + ); + priceModel.notifyListeners(); + } + + bool _isValidBarcode(final String barcode) => barcode.length >= 8; + + // Probably there's a regexp for that, but at least it's readable code. + String _getCleanBarcode(final String input) { + const int ascii0 = 48; + const int ascii9 = 48 + 10 - 1; + + final StringBuffer buffer = StringBuffer(); + for (int i = 0; i < input.length; i++) { + final int charCode = input.codeUnitAt(i); + if (charCode >= ascii0 && charCode <= ascii9) { + buffer.writeCharCode(charCode); + } + } + return buffer.toString(); + } +} diff --git a/packages/smooth_app/lib/pages/prices/price_amount_card.dart b/packages/smooth_app/lib/pages/prices/price_amount_card.dart index 314c5209045..893ff6d8144 100644 --- a/packages/smooth_app/lib/pages/prices/price_amount_card.dart +++ b/packages/smooth_app/lib/pages/prices/price_amount_card.dart @@ -1,87 +1,79 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; import 'package:smooth_app/generic_lib/buttons/smooth_large_button_with_icon.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_card.dart'; import 'package:smooth_app/pages/prices/price_amount_field.dart'; import 'package:smooth_app/pages/prices/price_amount_model.dart'; -import 'package:smooth_app/pages/prices/price_meta_product.dart'; import 'package:smooth_app/pages/prices/price_model.dart'; import 'package:smooth_app/pages/prices/price_product_list_tile.dart'; -import 'package:smooth_app/pages/prices/price_product_search_page.dart'; /// Card that displays the amounts (discounted or not) for price adding. class PriceAmountCard extends StatefulWidget { - PriceAmountCard({ - required this.priceModel, + const PriceAmountCard({ required this.index, - required this.refresh, - }) : model = priceModel.priceAmountModels[index], - total = priceModel.priceAmountModels.length; + required super.key, + }); - final PriceModel priceModel; - final PriceAmountModel model; final int index; - final int total; - // TODO(monsieurtanuki): not elegant, the display was not refreshed when removing an item - final VoidCallback refresh; @override State createState() => _PriceAmountCardState(); } class _PriceAmountCardState extends State { - final TextEditingController _controllerPaid = TextEditingController(); - final TextEditingController _controllerWithoutDiscount = - TextEditingController(); + late final TextEditingController _controllerPaid; + late final TextEditingController _controllerWithoutDiscount; + + @override + void initState() { + super.initState(); + final PriceAmountModel model = Provider.of( + context, + listen: false, + ).priceAmountModels[widget.index]; + _controllerPaid = TextEditingController(text: model.paidPrice); + _controllerWithoutDiscount = + TextEditingController(text: model.priceWithoutDiscount); + } + + @override + void dispose() { + _controllerPaid.dispose(); + _controllerWithoutDiscount.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { final AppLocalizations appLocalizations = AppLocalizations.of(context); - final bool isEmpty = widget.model.product.barcode.isEmpty; + final PriceModel priceModel = Provider.of(context); + final PriceAmountModel model = priceModel.priceAmountModels[widget.index]; + final int total = priceModel.priceAmountModels.length; + return SmoothCard( child: Column( children: [ Text( '${appLocalizations.prices_amount_subtitle}' - '${widget.total == 1 ? '' : ' (${widget.index + 1}/${widget.total})'}', + '${total == 1 ? '' : ' (${widget.index + 1}/$total)'}', ), PriceProductListTile( - product: widget.model.product, - trailingIconData: isEmpty - ? Icons.edit - : widget.total == 1 - ? null - : Icons.clear, - onPressed: isEmpty - ? () async { - final PriceMetaProduct? product = - await Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) => - const PriceProductSearchPage(), - ), - ); - if (product == null) { - return; - } - setState(() => widget.model.product = product); - } - : widget.total == 1 - ? null - : () { - widget.priceModel.priceAmountModels - .removeAt(widget.index); - widget.refresh.call(); - }, + product: model.product, + trailingIconData: total == 1 ? null : Icons.clear, + onPressed: total == 1 + ? null + : () { + priceModel.priceAmountModels.removeAt(widget.index); + priceModel.notifyListeners(); + }, ), SmoothLargeButtonWithIcon( - icon: widget.model.promo - ? Icons.check_box - : Icons.check_box_outline_blank, + icon: model.promo ? Icons.check_box : Icons.check_box_outline_blank, text: appLocalizations.prices_amount_is_discounted, onPressed: () => setState( - () => widget.model.promo = !widget.model.promo, + () => model.promo = !model.promo, ), ), const SizedBox(height: SMALL_SPACE), @@ -91,17 +83,17 @@ class _PriceAmountCardState extends State { child: PriceAmountField( controller: _controllerPaid, isPaidPrice: true, - model: widget.model, + model: model, ), ), const SizedBox(width: LARGE_SPACE), Expanded( - child: !widget.model.promo + child: !model.promo ? Container() : PriceAmountField( controller: _controllerWithoutDiscount, isPaidPrice: false, - model: widget.model, + model: model, ), ), ], diff --git a/packages/smooth_app/lib/pages/prices/price_amount_model.dart b/packages/smooth_app/lib/pages/prices/price_amount_model.dart index d635342fb33..089d4cfd4e3 100644 --- a/packages/smooth_app/lib/pages/prices/price_amount_model.dart +++ b/packages/smooth_app/lib/pages/prices/price_amount_model.dart @@ -10,12 +10,8 @@ class PriceAmountModel { PriceMetaProduct product; - String _paidPrice = ''; - String _priceWithoutDiscount = ''; - - set paidPrice(final String value) => _paidPrice = value; - - set priceWithoutDiscount(final String value) => _priceWithoutDiscount = value; + String paidPrice = ''; + String priceWithoutDiscount = ''; late double _checkedPaidPrice; double? _checkedPriceWithoutDiscount; @@ -37,11 +33,11 @@ class PriceAmountModel { if (product.barcode.isEmpty) { return appLocalizations.prices_amount_no_product; } - _checkedPaidPrice = validateDouble(_paidPrice)!; + _checkedPaidPrice = validateDouble(paidPrice)!; _checkedPriceWithoutDiscount = null; if (promo) { - if (_priceWithoutDiscount.isNotEmpty) { - _checkedPriceWithoutDiscount = validateDouble(_priceWithoutDiscount); + if (priceWithoutDiscount.isNotEmpty) { + _checkedPriceWithoutDiscount = validateDouble(priceWithoutDiscount); if (_checkedPriceWithoutDiscount == null) { return appLocalizations.prices_amount_price_incorrect; } diff --git a/packages/smooth_app/lib/pages/prices/price_meta_product.dart b/packages/smooth_app/lib/pages/prices/price_meta_product.dart index cfda26b292e..9414f074bed 100644 --- a/packages/smooth_app/lib/pages/prices/price_meta_product.dart +++ b/packages/smooth_app/lib/pages/prices/price_meta_product.dart @@ -1,9 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; +import 'package:smooth_app/data_models/fetched_product.dart'; +import 'package:smooth_app/database/dao_product.dart'; +import 'package:smooth_app/database/local_database.dart'; import 'package:smooth_app/generic_lib/widgets/images/smooth_image.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_product_image.dart'; import 'package:smooth_app/helpers/product_cards_helper.dart'; +import 'package:smooth_app/pages/prices/price_model.dart'; +import 'package:smooth_app/pages/product/common/product_refresher.dart'; /// Meta version of a product, coming from OFF or from Prices. class PriceMetaProduct { @@ -15,36 +20,35 @@ class PriceMetaProduct { : _product = null, _priceProduct = priceProduct, _barcode = null; - PriceMetaProduct.empty() - : _product = null, - _priceProduct = null, - _barcode = null; - PriceMetaProduct.unknown(final String barcode) - : _product = null, + PriceMetaProduct.unknown( + final String barcode, + final LocalDatabase localDatabase, + final PriceModel priceModel, + ) : _product = null, _priceProduct = null, - _barcode = barcode; + _barcode = barcode { + _search(localDatabase, priceModel); + } - final Product? _product; + Product? _product; final PriceProduct? _priceProduct; + bool _loading = false; final String? _barcode; - // TODO(monsieurtanuki): refine this test - bool get isValid => barcode.length >= 8; - String get barcode { if (_product != null) { - return _product.barcode!; + return _product!.barcode!; } if (_priceProduct != null) { return _priceProduct.code; } - return _barcode ?? ''; + return _barcode!; } String getName(final AppLocalizations appLocalizations) { if (_product != null) { return getProductNameAndBrands( - _product, + _product!, appLocalizations, ); } @@ -54,13 +58,16 @@ class PriceMetaProduct { if (barcode.isEmpty) { return appLocalizations.prices_barcode_search_none_yet; } + if (_loading) { + return appLocalizations.prices_barcode_search_running(barcode); + } return appLocalizations.prices_barcode_search_not_found; } Widget getImageWidget(final double size) { if (_product != null) { return SmoothMainProductImage( - product: _product, + product: _product!, width: size, height: size, ); @@ -78,4 +85,32 @@ class PriceMetaProduct { height: size, ); } + + Future _search( + final LocalDatabase localDatabase, + final PriceModel priceModel, + ) async { + _loading = true; + try { + final Product? product = await DaoProduct(localDatabase).get(barcode); + if (product != null) { + _product = product; + return; + } + final FetchedProduct fetchAndRefreshed = + await ProductRefresher().silentFetchAndRefresh( + localDatabase: localDatabase, + barcode: barcode, + ); + if (fetchAndRefreshed.product == null) { + return; + } + _product = fetchAndRefreshed.product; + } catch (e) { + // we don't care, it will end up as "unknown product" + } finally { + _loading = false; + priceModel.notifyListeners(); + } + } } diff --git a/packages/smooth_app/lib/pages/prices/price_model.dart b/packages/smooth_app/lib/pages/prices/price_model.dart index ae5978231ac..4deeb56f153 100644 --- a/packages/smooth_app/lib/pages/prices/price_model.dart +++ b/packages/smooth_app/lib/pages/prices/price_model.dart @@ -15,12 +15,12 @@ class PriceModel with ChangeNotifier { PriceModel({ required final ProofType proofType, required final List locations, - required final PriceMetaProduct product, + final PriceMetaProduct? initialProduct, }) : _proofType = proofType, _date = DateTime.now(), _locations = locations, priceAmountModels = [ - PriceAmountModel(product: product), + if (initialProduct != null) PriceAmountModel(product: initialProduct), ]; final List priceAmountModels; @@ -68,6 +68,10 @@ class PriceModel with ChangeNotifier { late Currency _checkedCurrency; + // overriding in order to make it public + @override + void notifyListeners() => super.notifyListeners(); + /// Returns the error message of the parameter check, or null if OK. String? checkParameters(final BuildContext context) { final AppLocalizations appLocalizations = AppLocalizations.of(context); diff --git a/packages/smooth_app/lib/pages/prices/price_product_search_page.dart b/packages/smooth_app/lib/pages/prices/price_product_search_page.dart deleted file mode 100644 index fcfc88b81fb..00000000000 --- a/packages/smooth_app/lib/pages/prices/price_product_search_page.dart +++ /dev/null @@ -1,239 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:matomo_tracker/matomo_tracker.dart'; -import 'package:openfoodfacts/openfoodfacts.dart'; -import 'package:provider/provider.dart'; -import 'package:smooth_app/data_models/fetched_product.dart'; -import 'package:smooth_app/database/dao_product.dart'; -import 'package:smooth_app/database/local_database.dart'; -import 'package:smooth_app/generic_lib/design_constants.dart'; -import 'package:smooth_app/generic_lib/dialogs/smooth_alert_dialog.dart'; -import 'package:smooth_app/generic_lib/loading_dialog.dart'; -import 'package:smooth_app/generic_lib/widgets/smooth_back_button.dart'; -import 'package:smooth_app/generic_lib/widgets/smooth_text_form_field.dart'; -import 'package:smooth_app/helpers/camera_helper.dart'; -import 'package:smooth_app/pages/prices/price_meta_product.dart'; -import 'package:smooth_app/pages/prices/price_product_list_tile.dart'; -import 'package:smooth_app/pages/prices/price_scan_page.dart'; -import 'package:smooth_app/pages/product/common/product_refresher.dart'; -import 'package:smooth_app/widgets/smooth_app_bar.dart'; -import 'package:smooth_app/widgets/smooth_scaffold.dart'; - -/// Product Search Page, for Prices. -class PriceProductSearchPage extends StatefulWidget { - const PriceProductSearchPage({ - this.product, - }); - - final PriceMetaProduct? product; - - // TODO(monsieurtanuki): as a parameter, add a list of barcodes already there: we're not supposed to select twice the same product - - @override - State createState() => _PriceProductSearchPageState(); -} - -class _PriceProductSearchPageState extends State - with TraceableClientMixin { - final TextEditingController _controller = TextEditingController(); - - late PriceMetaProduct? _product = widget.product; - - // TODO(monsieurtanuki): TextInputAction + focus - static const TextInputType _textInputType = TextInputType.number; - - @override - void initState() { - super.initState(); - _controller.text = _product?.barcode ?? ''; - } - - static const String _barcodeHint = '7300400481588'; - - @override - Widget build(BuildContext context) { - final AppLocalizations appLocalizations = AppLocalizations.of(context); - final PriceMetaProduct priceMetaProduct = - _product ?? PriceMetaProduct.unknown(_controller.text); - // TODO(monsieurtanuki): add WillPopScope2 - return SmoothScaffold( - appBar: SmoothAppBar( - centerTitle: false, - leading: const SmoothBackButton(), - title: Text(appLocalizations.prices_barcode_search_title), - ), - floatingActionButton: !CameraHelper.hasACamera - ? null - : FloatingActionButton.extended( - onPressed: () async => _scan(context), - label: Text(appLocalizations.prices_barcode_reader_action), - icon: const Icon(Icons.barcode_reader), - ), - body: SingleChildScrollView( - padding: const EdgeInsets.all(LARGE_SPACE), - child: Column( - children: [ - // TODO(monsieurtanuki): add a "clear" button - // TODO(monsieurtanuki): add an automatic "validate barcode" feature (cf. https://en.wikipedia.org/wiki/International_Article_Number#Check_digit) - SmoothTextFormField( - type: TextFieldTypes.PLAIN_TEXT, - controller: _controller, - hintText: _barcodeHint, - textInputType: _textInputType, - onChanged: (_) async => _onChanged(context), - onFieldSubmitted: (_) async => _onFieldSubmitted(context), - prefixIcon: const Icon(CupertinoIcons.barcode), - textInputAction: TextInputAction.search, - ), - if (priceMetaProduct.isValid) - Padding( - padding: const EdgeInsets.symmetric(vertical: LARGE_SPACE), - child: PriceProductListTile( - product: priceMetaProduct, - trailingIconData: Icons.check_circle, - onPressed: () => Navigator.of(context).pop(priceMetaProduct), - ), - ), - ], - ), - ), - ); - } - - Future _localSearch( - final String barcode, - final LocalDatabase localDatabase, - ) async => - DaoProduct(localDatabase).get(barcode); - - Future _serverSearch( - final String barcode, - final LocalDatabase localDatabase, - final BuildContext context, - ) async { - final AppLocalizations appLocalizations = AppLocalizations.of(context); - final FetchedProduct? fetchAndRefreshed = - await LoadingDialog.run( - future: ProductRefresher().silentFetchAndRefresh( - localDatabase: localDatabase, - barcode: barcode, - ), - context: context, - title: appLocalizations.prices_barcode_search_running(barcode), - ); - if (fetchAndRefreshed == null) { - // the user probably cancelled - return null; - } - if (fetchAndRefreshed.product == null) { - if (context.mounted) { - await LoadingDialog.error( - context: context, - title: fetchAndRefreshed.getErrorTitle(appLocalizations), - ); - } - } - return fetchAndRefreshed.product; - } - - // Probably there's a regexp for that, but at least it's readable code. - String _getCleanBarcode(final String input) { - const int ascii0 = 48; - const int ascii9 = 48 + 10 - 1; - - final StringBuffer buffer = StringBuffer(); - for (int i = 0; i < input.length; i++) { - final int charCode = input.codeUnitAt(i); - if (charCode >= ascii0 && charCode <= ascii9) { - buffer.writeCharCode(charCode); - } - } - return buffer.toString(); - } - - Future _onChanged(final BuildContext context) async { - final String barcode = _controller.text; - final String cleanBarcode = _getCleanBarcode(barcode); - if (barcode != cleanBarcode) { - setState(() => _controller.text = cleanBarcode); - return; - } - - if (_product != null) { - setState(() => _product = null); - } - - final LocalDatabase localDatabase = context.read(); - final Product? product = await _localSearch( - barcode, - localDatabase, - ); - if (product != null) { - setState(() => _product = PriceMetaProduct.product(product)); - return; - } - setState(() {}); - } - - Future _onFieldSubmitted(final BuildContext context) async { - final String barcode = _controller.text; - if (barcode.isEmpty) { - return; - } - - final LocalDatabase localDatabase = context.read(); - final Product? product = await _serverSearch( - barcode, - localDatabase, - context, - ); - if (product != null) { - setState(() => _product = PriceMetaProduct.product(product)); - } - } - - Future _scan(final BuildContext context) async { - final AppLocalizations appLocalizations = AppLocalizations.of(context); - final String? barcode = await Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) => const PriceScanPage(), - ), - ); - if (barcode == null) { - return; - } - _controller.text = barcode; - if (!context.mounted) { - return; - } - await _onChanged(context); - if (_product != null) { - return; - } - if (!context.mounted) { - return; - } - final bool? accepts = await showDialog( - context: context, - builder: (final BuildContext context) => SmoothAlertDialog( - body: Text(appLocalizations.prices_barcode_search_question), - neutralAction: SmoothActionButton( - text: appLocalizations.cancel, - onPressed: () => Navigator.of(context).pop(false), - ), - positiveAction: SmoothActionButton( - text: appLocalizations.yes, - onPressed: () => Navigator.of(context).pop(true), - ), - ), - ); - if (!context.mounted) { - return; - } - if (accepts != true) { - return; - } - await _onFieldSubmitted(context); - } -} diff --git a/packages/smooth_app/lib/pages/prices/product_price_add_page.dart b/packages/smooth_app/lib/pages/prices/product_price_add_page.dart index 733098d9703..9276299c707 100644 --- a/packages/smooth_app/lib/pages/prices/product_price_add_page.dart +++ b/packages/smooth_app/lib/pages/prices/product_price_add_page.dart @@ -6,20 +6,17 @@ import 'package:provider/provider.dart'; import 'package:smooth_app/data_models/preferences/user_preferences.dart'; import 'package:smooth_app/database/dao_osm_location.dart'; import 'package:smooth_app/database/local_database.dart'; -import 'package:smooth_app/generic_lib/buttons/smooth_large_button_with_icon.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/generic_lib/dialogs/smooth_alert_dialog.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_back_button.dart'; -import 'package:smooth_app/generic_lib/widgets/smooth_card.dart'; import 'package:smooth_app/pages/locations/osm_location.dart'; +import 'package:smooth_app/pages/prices/price_add_product_card.dart'; import 'package:smooth_app/pages/prices/price_amount_card.dart'; -import 'package:smooth_app/pages/prices/price_amount_model.dart'; import 'package:smooth_app/pages/prices/price_currency_card.dart'; import 'package:smooth_app/pages/prices/price_date_card.dart'; import 'package:smooth_app/pages/prices/price_location_card.dart'; import 'package:smooth_app/pages/prices/price_meta_product.dart'; import 'package:smooth_app/pages/prices/price_model.dart'; -import 'package:smooth_app/pages/prices/price_product_search_page.dart'; import 'package:smooth_app/pages/prices/price_proof_card.dart'; import 'package:smooth_app/pages/product/common/product_refresher.dart'; import 'package:smooth_app/widgets/smooth_app_bar.dart'; @@ -33,13 +30,13 @@ class ProductPriceAddPage extends StatefulWidget { required this.proofType, }); - final PriceMetaProduct product; + final PriceMetaProduct? product; final List latestOsmLocations; final ProofType proofType; static Future showProductPage({ required final BuildContext context, - required final PriceMetaProduct product, + final PriceMetaProduct? product, required final ProofType proofType, }) async { if (!await ProductRefresher().checkIfLoggedIn( @@ -78,123 +75,107 @@ class _ProductPriceAddPageState extends State late final PriceModel _model = PriceModel( proofType: widget.proofType, locations: widget.latestOsmLocations, - product: widget.product, + initialProduct: widget.product, ); final GlobalKey _formKey = GlobalKey(); @override Widget build(BuildContext context) { - final AppLocalizations appLocalizations = AppLocalizations.of(context); // TODO(monsieurtanuki): add WillPopScope2 - return ChangeNotifierProvider( - create: (_) => _model, - child: Form( - key: _formKey, - child: SmoothScaffold( - appBar: SmoothAppBar( - centerTitle: false, - leading: const SmoothBackButton(), - title: Text( - appLocalizations.prices_add_n_prices( - _model.priceAmountModels.length, + return ChangeNotifierProvider.value( + value: _model, + builder: ( + final BuildContext context, + final Widget? child, + ) { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + final PriceModel model = Provider.of(context); + return Form( + key: _formKey, + child: SmoothScaffold( + appBar: SmoothAppBar( + centerTitle: false, + leading: const SmoothBackButton(), + title: Text( + appLocalizations.prices_add_n_prices( + model.priceAmountModels.length, + ), ), + actions: [ + IconButton( + icon: const Icon(Icons.info), + onPressed: () async => _doesAcceptWarning(justInfo: true), + ), + ], ), - actions: [ - IconButton( - icon: const Icon(Icons.info), - onPressed: () async => _doesAcceptWarning(justInfo: true), + body: SingleChildScrollView( + padding: const EdgeInsets.all(LARGE_SPACE), + child: Column( + children: [ + const PriceProofCard(), + const SizedBox(height: LARGE_SPACE), + const PriceDateCard(), + const SizedBox(height: LARGE_SPACE), + const PriceLocationCard(), + const SizedBox(height: LARGE_SPACE), + const PriceCurrencyCard(), + const SizedBox(height: LARGE_SPACE), + for (int i = 0; i < model.priceAmountModels.length; i++) + PriceAmountCard( + key: Key(model.priceAmountModels[i].product.barcode), + index: i, + ), + const PriceAddProductCard(), + // so that the last items don't get hidden by the FAB + const SizedBox(height: MINIMUM_TOUCH_SIZE * 2), + ], ), - ], - ), - body: SingleChildScrollView( - padding: const EdgeInsets.all(LARGE_SPACE), - child: Column( - children: [ - const PriceProofCard(), - const SizedBox(height: LARGE_SPACE), - const PriceDateCard(), - const SizedBox(height: LARGE_SPACE), - const PriceLocationCard(), - const SizedBox(height: LARGE_SPACE), - const PriceCurrencyCard(), - const SizedBox(height: LARGE_SPACE), - for (int i = 0; i < _model.priceAmountModels.length; i++) - PriceAmountCard( - priceModel: _model, - index: i, - refresh: () => setState(() {}), - ), - // TODO(monsieurtanuki): check if there's an empty barcode before displaying this card - SmoothCard( - child: SmoothLargeButtonWithIcon( - text: appLocalizations.prices_add_an_item, - icon: Icons.add, - onPressed: () async { - final PriceMetaProduct? product = - await Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) => - const PriceProductSearchPage(), - ), - ); - if (product == null) { + ), + floatingActionButton: FloatingActionButton.extended( + onPressed: model.priceAmountModels.isEmpty + ? null + : () async { + if (!await _check(context)) { + return; + } + if (!context.mounted) { return; } - setState( - () => _model.priceAmountModels.add( - PriceAmountModel( - product: product, - ), - ), - ); - }, - ), - ), - // so that the last items don't get hidden by the FAB - const SizedBox(height: MINIMUM_TOUCH_SIZE * 2), - ], - ), - ), - floatingActionButton: FloatingActionButton.extended( - onPressed: () async { - if (!await _check(context)) { - return; - } - if (!context.mounted) { - return; - } - final UserPreferences userPreferences = - context.read(); - const String flagTag = UserPreferences.TAG_PRICE_PRIVACY_WARNING; - final bool? already = userPreferences.getFlag(flagTag); - if (already != true) { - final bool? accepts = await _doesAcceptWarning(justInfo: false); - if (accepts != true) { - return; - } - await userPreferences.setFlag(flagTag, true); - } - if (!context.mounted) { - return; - } + final UserPreferences userPreferences = + context.read(); + const String flagTag = + UserPreferences.TAG_PRICE_PRIVACY_WARNING; + final bool? already = userPreferences.getFlag(flagTag); + if (already != true) { + final bool? accepts = + await _doesAcceptWarning(justInfo: false); + if (accepts != true) { + return; + } + await userPreferences.setFlag(flagTag, true); + } + if (!context.mounted) { + return; + } - await _model.addTask(context); - if (!context.mounted) { - return; - } - Navigator.of(context).pop(); - }, - icon: const Icon(Icons.send), - label: Text( - appLocalizations.prices_send_n_prices( - _model.priceAmountModels.length, + await model.addTask(context); + if (!context.mounted) { + return; + } + Navigator.of(context).pop(); + }, + icon: const Icon(Icons.send), + label: Text( + appLocalizations.prices_send_n_prices( + model.priceAmountModels.length, + ), ), ), ), - ), - ), + ); + }, ); } @@ -228,7 +209,8 @@ class _ProductPriceAddPageState extends State String? error; try { - error = _model.checkParameters(context); + error = Provider.of(context, listen: false) + .checkParameters(context); } catch (e) { error = e.toString(); }