diff --git a/packages/smooth_app/android/Gemfile.lock b/packages/smooth_app/android/Gemfile.lock index e4c808f5cc6..b087ba1e0f8 100644 --- a/packages/smooth_app/android/Gemfile.lock +++ b/packages/smooth_app/android/Gemfile.lock @@ -16,7 +16,7 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.980.0) + aws-partitions (1.984.0) aws-sdk-core (3.209.1) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) @@ -25,7 +25,7 @@ GEM aws-sdk-kms (1.94.0) aws-sdk-core (~> 3, >= 3.207.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.166.0) + aws-sdk-s3 (1.167.0) aws-sdk-core (~> 3, >= 3.207.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) @@ -44,7 +44,7 @@ GEM domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.111.0) + excon (0.112.0) faraday (1.10.4) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -74,7 +74,7 @@ GEM faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.3.1) - fastlane (2.223.1) + fastlane (2.224.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -160,7 +160,7 @@ GEM httpclient (2.8.3) jmespath (1.6.2) json (2.7.2) - jwt (2.9.1) + jwt (2.9.3) base64 mini_magick (4.13.2) mini_mime (1.1.5) @@ -179,7 +179,7 @@ GEM trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.3.7) + rexml (3.3.8) rouge (2.0.7) ruby2_keywords (0.0.5) rubyzip (2.3.2) @@ -203,13 +203,13 @@ GEM uber (0.1.0) unicode-display_width (2.6.0) word_wrap (1.0.0) - xcodeproj (1.25.0) + xcodeproj (1.25.1) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) nanaimo (~> 0.3.0) - rexml (>= 3.3.2, < 4.0) + rexml (>= 3.3.6, < 4.0) xcpretty (0.3.0) rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) diff --git a/packages/smooth_app/assets/app/release_icon_transparent_1152x1152.png b/packages/smooth_app/assets/app/release_icon_transparent_1152x1152.png deleted file mode 100644 index aae1b7666cf..00000000000 Binary files a/packages/smooth_app/assets/app/release_icon_transparent_1152x1152.png and /dev/null differ diff --git a/packages/smooth_app/assets/app/release_icon_transparent_70pct_1152x1152.png b/packages/smooth_app/assets/app/release_icon_transparent_70pct_1152x1152.png deleted file mode 100644 index 630cf46a6c3..00000000000 Binary files a/packages/smooth_app/assets/app/release_icon_transparent_70pct_1152x1152.png and /dev/null differ diff --git a/packages/smooth_app/ios/Gemfile.lock b/packages/smooth_app/ios/Gemfile.lock index a0622316070..f11bf6f002a 100644 --- a/packages/smooth_app/ios/Gemfile.lock +++ b/packages/smooth_app/ios/Gemfile.lock @@ -16,7 +16,7 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.980.0) + aws-partitions (1.984.0) aws-sdk-core (3.209.1) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) @@ -25,7 +25,7 @@ GEM aws-sdk-kms (1.94.0) aws-sdk-core (~> 3, >= 3.207.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.166.0) + aws-sdk-s3 (1.167.0) aws-sdk-core (~> 3, >= 3.207.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) @@ -45,7 +45,7 @@ GEM domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.111.0) + excon (0.112.0) faraday (1.10.4) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -75,7 +75,7 @@ GEM faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.3.1) - fastlane (2.223.1) + fastlane (2.224.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -161,7 +161,7 @@ GEM httpclient (2.8.3) jmespath (1.6.2) json (2.7.2) - jwt (2.9.1) + jwt (2.9.3) base64 mini_magick (4.13.2) mini_mime (1.1.5) @@ -180,7 +180,7 @@ GEM trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.3.7) + rexml (3.3.8) rouge (2.0.7) ruby2_keywords (0.0.5) rubyzip (2.3.2) @@ -209,13 +209,13 @@ GEM xcode-install (2.8.1) claide (>= 0.9.1) fastlane (>= 2.1.0, < 3.0.0) - xcodeproj (1.25.0) + xcodeproj (1.25.1) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) nanaimo (~> 0.3.0) - rexml (>= 3.3.2, < 4.0) + rexml (>= 3.3.6, < 4.0) xcpretty (0.3.0) rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) diff --git a/packages/smooth_app/lib/background/background_task_add_other_price.dart b/packages/smooth_app/lib/background/background_task_add_other_price.dart new file mode 100644 index 00000000000..ee0c6bbdaf7 --- /dev/null +++ b/packages/smooth_app/lib/background/background_task_add_other_price.dart @@ -0,0 +1,123 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; +import 'package:provider/provider.dart'; +import 'package:smooth_app/background/background_task.dart'; +import 'package:smooth_app/background/background_task_price.dart'; +import 'package:smooth_app/background/operation_type.dart'; +import 'package:smooth_app/database/local_database.dart'; + +/// Background task about adding prices to an existing proof. +class BackgroundTaskAddOtherPrice extends BackgroundTaskPrice { + BackgroundTaskAddOtherPrice._({ + required super.processName, + required super.uniqueId, + required super.stamp, + // single + required this.proofId, + required super.date, + required super.currency, + required super.locationOSMId, + required super.locationOSMType, + // multi + required super.barcodes, + required super.pricesAreDiscounted, + required super.prices, + required super.pricesWithoutDiscount, + }); + + BackgroundTaskAddOtherPrice.fromJson(super.json) + : proofId = json[_jsonTagProofId] as int, + super.fromJson(); + + static const String _jsonTagProofId = 'proofId'; + + static const OperationType _operationType = OperationType.addOtherPrice; + + final int proofId; + + @override + Map toJson() { + final Map result = super.toJson(); + result[_jsonTagProofId] = proofId; + return result; + } + + /// Adds the background task about uploading a product image. + static Future addTask({ + required final BuildContext context, + required final int proofId, + required final DateTime date, + required final Currency currency, + required final int locationOSMId, + required final LocationOSMType locationOSMType, + required final List barcodes, + required final List pricesAreDiscounted, + required final List prices, + required final List pricesWithoutDiscount, + }) async { + final LocalDatabase localDatabase = context.read(); + final String uniqueId = await _operationType.getNewKey(localDatabase); + final BackgroundTask task = _getNewTask( + uniqueId: uniqueId, + proofId: proofId, + date: date, + currency: currency, + locationOSMId: locationOSMId, + locationOSMType: locationOSMType, + barcodes: barcodes, + pricesAreDiscounted: pricesAreDiscounted, + prices: prices, + pricesWithoutDiscount: pricesWithoutDiscount, + ); + if (!context.mounted) { + return; + } + await task.addToManager(localDatabase, context: context); + } + + /// Returns a new background task about changing a product. + static BackgroundTaskAddOtherPrice _getNewTask({ + required final String uniqueId, + required final int proofId, + required final DateTime date, + required final Currency currency, + required final int locationOSMId, + required final LocationOSMType locationOSMType, + required final List barcodes, + required final List pricesAreDiscounted, + required final List prices, + required final List pricesWithoutDiscount, + }) => + BackgroundTaskAddOtherPrice._( + uniqueId: uniqueId, + processName: _operationType.processName, + proofId: proofId, + date: date, + currency: currency, + locationOSMId: locationOSMId, + locationOSMType: locationOSMType, + barcodes: barcodes, + pricesAreDiscounted: pricesAreDiscounted, + prices: prices, + pricesWithoutDiscount: pricesWithoutDiscount, + stamp: BackgroundTaskPrice.getStamp( + date: date, + locationOSMId: locationOSMId, + locationOSMType: locationOSMType, + ), + ); + + @override + Future execute(final LocalDatabase localDatabase) async { + final String bearerToken = await getBearerToken(); + + await addPrices( + bearerToken: bearerToken, + proofId: proofId, + ); + + await closeSession(bearerToken: bearerToken); + } +} diff --git a/packages/smooth_app/lib/background/background_task_add_price.dart b/packages/smooth_app/lib/background/background_task_add_price.dart index f7bcad2d5e1..8e21f72f5c2 100644 --- a/packages/smooth_app/lib/background/background_task_add_price.dart +++ b/packages/smooth_app/lib/background/background_task_add_price.dart @@ -2,12 +2,12 @@ import 'dart:async'; import 'package:crop_image/crop_image.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:http_parser/http_parser.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:provider/provider.dart'; import 'package:smooth_app/background/background_task.dart'; import 'package:smooth_app/background/background_task_image.dart'; +import 'package:smooth_app/background/background_task_price.dart'; import 'package:smooth_app/background/background_task_upload.dart'; import 'package:smooth_app/background/operation_type.dart'; import 'package:smooth_app/database/local_database.dart'; @@ -18,12 +18,12 @@ import 'package:smooth_app/query/product_query.dart'; // TODO(monsieurtanuki): use transient file, in order to have instant access to proof image? /// Background task about adding a product price. -class BackgroundTaskAddPrice extends BackgroundTask { +class BackgroundTaskAddPrice extends BackgroundTaskPrice { BackgroundTaskAddPrice._({ required super.processName, required super.uniqueId, required super.stamp, - // single + // proof display required this.fullPath, required this.rotationDegrees, required this.cropX1, @@ -31,17 +31,17 @@ class BackgroundTaskAddPrice extends BackgroundTask { required this.cropX2, required this.cropY2, required this.proofType, - required this.date, - required this.currency, - required this.locationOSMId, - required this.locationOSMType, - // lines required this.eraserCoordinates, + // single + required super.date, + required super.currency, + required super.locationOSMId, + required super.locationOSMType, // multi - required this.barcodes, - required this.pricesAreDiscounted, - required this.prices, - required this.pricesWithoutDiscount, + required super.barcodes, + required super.pricesAreDiscounted, + required super.prices, + required super.pricesWithoutDiscount, }); BackgroundTaskAddPrice.fromJson(super.json) @@ -52,73 +52,10 @@ class BackgroundTaskAddPrice extends BackgroundTask { cropX2 = json[_jsonTagX2] as int? ?? 0, cropY2 = json[_jsonTagY2] as int? ?? 0, proofType = ProofType.fromOffTag(json[_jsonTagProofType] as String)!, - date = JsonHelper.stringTimestampToDate(json[_jsonTagDate] as String), - currency = Currency.fromName(json[_jsonTagCurrency] as String)!, - locationOSMId = json[_jsonTagOSMId] as int, - locationOSMType = - LocationOSMType.fromOffTag(json[_jsonTagOSMType] as String)!, - eraserCoordinates = - _fromJsonListDouble(json[_jsonTagEraserCoordinates]), - barcodes = json.containsKey(_jsonTagBarcode) - ? [json[_jsonTagBarcode] as String] - : _fromJsonListString(json[_jsonTagBarcodes])!, - pricesAreDiscounted = json.containsKey(_jsonTagIsDiscounted) - ? [json[_jsonTagIsDiscounted] as bool] - : _fromJsonListBool(json[_jsonTagAreDiscounted])!, - prices = json.containsKey(_jsonTagPrice) - ? [json[_jsonTagPrice] as double] - : _fromJsonListDouble(json[_jsonTagPrices])!, - pricesWithoutDiscount = json.containsKey(_jsonTagPriceWithoutDiscount) - ? [json[_jsonTagPriceWithoutDiscount] as double?] - : _fromJsonListNullableDouble(json[_jsonTagPricesWithoutDiscount])!, + eraserCoordinates = BackgroundTaskPrice.fromJsonListDouble( + json[_jsonTagEraserCoordinates]), super.fromJson(); - static List? _fromJsonListDouble(final List? input) { - if (input == null) { - return null; - } - final List result = []; - for (final dynamic item in input) { - result.add(item as double); - } - return result; - } - - static List? _fromJsonListNullableDouble( - final List? input, - ) { - if (input == null) { - return null; - } - final List result = []; - for (final dynamic item in input) { - result.add(item as double?); - } - return result; - } - - static List? _fromJsonListString(final List? input) { - if (input == null) { - return null; - } - final List result = []; - for (final dynamic item in input) { - result.add(item as String); - } - return result; - } - - static List? _fromJsonListBool(final List? input) { - if (input == null) { - return null; - } - final List result = []; - for (final dynamic item in input) { - result.add(item as bool); - } - return result; - } - static const String _jsonTagImagePath = 'imagePath'; static const String _jsonTagRotation = 'rotation'; static const String _jsonTagX1 = 'x1'; @@ -126,23 +63,7 @@ class BackgroundTaskAddPrice extends BackgroundTask { static const String _jsonTagX2 = 'x2'; static const String _jsonTagY2 = 'y2'; static const String _jsonTagProofType = 'proofType'; - static const String _jsonTagDate = 'date'; static const String _jsonTagEraserCoordinates = 'eraserCoordinates'; - static const String _jsonTagCurrency = 'currency'; - static const String _jsonTagOSMId = 'osmId'; - static const String _jsonTagOSMType = 'osmType'; - static const String _jsonTagBarcodes = 'barcodes'; - static const String _jsonTagAreDiscounted = 'areDiscounted'; - static const String _jsonTagPrices = 'prices'; - static const String _jsonTagPricesWithoutDiscount = 'pricesWithoutDiscount'; - @Deprecated('Use [_jsonTagBarcodes] instead') - static const String _jsonTagBarcode = 'barcode'; - @Deprecated('Use [_jsonTagAreDiscounted] instead') - static const String _jsonTagIsDiscounted = 'isDiscounted'; - @Deprecated('Use [_jsonTagPrices] instead') - static const String _jsonTagPrice = 'price'; - @Deprecated('Use [_jsonTagPricesWithoutDiscount] instead') - static const String _jsonTagPriceWithoutDiscount = 'priceWithoutDiscount'; static const OperationType _operationType = OperationType.addPrice; @@ -153,18 +74,8 @@ class BackgroundTaskAddPrice extends BackgroundTask { final int cropX2; final int cropY2; final ProofType proofType; - final DateTime date; - final Currency currency; - final int locationOSMId; - final LocationOSMType locationOSMType; final List? eraserCoordinates; - // per line - final List barcodes; - final List pricesAreDiscounted; - final List prices; - final List pricesWithoutDiscount; - @override Map toJson() { final Map result = super.toJson(); @@ -175,15 +86,7 @@ class BackgroundTaskAddPrice extends BackgroundTask { result[_jsonTagX2] = cropX2; result[_jsonTagY2] = cropY2; result[_jsonTagProofType] = proofType.offTag; - result[_jsonTagDate] = date.toIso8601String(); - result[_jsonTagCurrency] = currency.name; - result[_jsonTagOSMId] = locationOSMId; - result[_jsonTagOSMType] = locationOSMType.offTag; result[_jsonTagEraserCoordinates] = eraserCoordinates; - result[_jsonTagBarcodes] = barcodes; - result[_jsonTagAreDiscounted] = pricesAreDiscounted; - result[_jsonTagPrices] = prices; - result[_jsonTagPricesWithoutDiscount] = pricesWithoutDiscount; return result; } @@ -222,14 +125,6 @@ class BackgroundTaskAddPrice extends BackgroundTask { await task.addToManager(localDatabase, context: context); } - @override - (String, AlignmentGeometry)? getFloatingMessage( - final AppLocalizations appLocalizations) => - ( - appLocalizations.add_price_queued, - AlignmentDirectional.center, - ); - /// Returns a new background task about changing a product. static BackgroundTaskAddPrice _getNewTask({ required final String uniqueId, @@ -263,20 +158,13 @@ class BackgroundTaskAddPrice extends BackgroundTask { pricesAreDiscounted: pricesAreDiscounted, prices: prices, pricesWithoutDiscount: pricesWithoutDiscount, - stamp: _getStamp( + stamp: BackgroundTaskPrice.getStamp( date: date, locationOSMId: locationOSMId, locationOSMType: locationOSMType, ), ); - static String _getStamp({ - required final DateTime date, - required final int locationOSMId, - required final LocationOSMType locationOSMType, - }) => - 'no_barcode;price;$date;$locationOSMId;$locationOSMType'; - @override Future postExecute( final LocalDatabase localDatabase, @@ -297,9 +185,6 @@ class BackgroundTaskAddPrice extends BackgroundTask { } } - @override - Future preExecute(final LocalDatabase localDatabase) async {} - @override Future execute(final LocalDatabase localDatabase) async { final List offsets = []; @@ -340,21 +225,7 @@ class BackgroundTaskAddPrice extends BackgroundTask { return; } - // authentication - final User user = getUser(); - final MaybeError token = - await OpenPricesAPIClient.getAuthenticationToken( - username: user.userId, - password: user.password, - uriHelper: ProductQuery.uriPricesHelper, - ); - if (token.isError) { - throw Exception('Could not get token: ${token.error}'); - } - if (token.value.isEmpty) { - throw Exception('Unexpected empty token'); - } - final String bearerToken = token.value; + final String bearerToken = await getBearerToken(); // proof upload final Uri initialImageUri = Uri.parse(path); @@ -375,48 +246,11 @@ class BackgroundTaskAddPrice extends BackgroundTask { throw Exception('Could not upload proof: ${uploadProof.error}'); } - for (int i = 0; i < barcodes.length; i++) { - final Price newPrice = Price() - ..date = date - ..currency = currency - ..locationOSMId = locationOSMId - ..locationOSMType = locationOSMType - ..proofId = uploadProof.value.id - ..priceIsDiscounted = pricesAreDiscounted[i] - ..price = prices[i] - ..priceWithoutDiscount = pricesWithoutDiscount[i] - ..productCode = barcodes[i]; - - // create price - final MaybeError addedPrice = - await OpenPricesAPIClient.createPrice( - price: newPrice, - bearerToken: bearerToken, - uriHelper: ProductQuery.uriPricesHelper, - ); - if (addedPrice.isError) { - throw Exception('Could not add price: ${addedPrice.error}'); - } - } - - // close session - final MaybeError closedSession = - await OpenPricesAPIClient.deleteUserSession( - uriHelper: ProductQuery.uriPricesHelper, + await addPrices( bearerToken: bearerToken, + proofId: uploadProof.value.id, ); - if (closedSession.isError) { - // TODO(monsieurtanuki): do we really care? - // throw Exception('Could not close session: ${closedSession.error}'); - return; - } - if (!closedSession.value) { - // TODO(monsieurtanuki): do we really care? - // throw Exception('Could not really close session'); - return; - } - } - @override - bool isDeduplicable() => false; + await closeSession(bearerToken: bearerToken); + } } diff --git a/packages/smooth_app/lib/background/background_task_price.dart b/packages/smooth_app/lib/background/background_task_price.dart new file mode 100644 index 00000000000..8d5eb080d57 --- /dev/null +++ b/packages/smooth_app/lib/background/background_task_price.dart @@ -0,0 +1,226 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; +import 'package:smooth_app/background/background_task.dart'; +import 'package:smooth_app/database/local_database.dart'; +import 'package:smooth_app/query/product_query.dart'; + +/// Abstract background task about adding prices. +abstract class BackgroundTaskPrice extends BackgroundTask { + BackgroundTaskPrice({ + required super.processName, + required super.uniqueId, + required super.stamp, + // single + required this.date, + required this.currency, + required this.locationOSMId, + required this.locationOSMType, + // multi + required this.barcodes, + required this.pricesAreDiscounted, + required this.prices, + required this.pricesWithoutDiscount, + }); + + BackgroundTaskPrice.fromJson(super.json) + : date = JsonHelper.stringTimestampToDate(json[_jsonTagDate] as String), + currency = Currency.fromName(json[_jsonTagCurrency] as String)!, + locationOSMId = json[_jsonTagOSMId] as int, + locationOSMType = + LocationOSMType.fromOffTag(json[_jsonTagOSMType] as String)!, + barcodes = json.containsKey(_jsonTagBarcode) + ? [json[_jsonTagBarcode] as String] + : _fromJsonListString(json[_jsonTagBarcodes])!, + pricesAreDiscounted = json.containsKey(_jsonTagIsDiscounted) + ? [json[_jsonTagIsDiscounted] as bool] + : _fromJsonListBool(json[_jsonTagAreDiscounted])!, + prices = json.containsKey(_jsonTagPrice) + ? [json[_jsonTagPrice] as double] + : fromJsonListDouble(json[_jsonTagPrices])!, + pricesWithoutDiscount = json.containsKey(_jsonTagPriceWithoutDiscount) + ? [json[_jsonTagPriceWithoutDiscount] as double?] + : _fromJsonListNullableDouble(json[_jsonTagPricesWithoutDiscount])!, + super.fromJson(); + + static const String _jsonTagDate = 'date'; + static const String _jsonTagCurrency = 'currency'; + static const String _jsonTagOSMId = 'osmId'; + static const String _jsonTagOSMType = 'osmType'; + static const String _jsonTagBarcodes = 'barcodes'; + static const String _jsonTagAreDiscounted = 'areDiscounted'; + static const String _jsonTagPrices = 'prices'; + static const String _jsonTagPricesWithoutDiscount = 'pricesWithoutDiscount'; + @Deprecated('Use [_jsonTagBarcodes] instead') + static const String _jsonTagBarcode = 'barcode'; + @Deprecated('Use [_jsonTagAreDiscounted] instead') + static const String _jsonTagIsDiscounted = 'isDiscounted'; + @Deprecated('Use [_jsonTagPrices] instead') + static const String _jsonTagPrice = 'price'; + @Deprecated('Use [_jsonTagPricesWithoutDiscount] instead') + static const String _jsonTagPriceWithoutDiscount = 'priceWithoutDiscount'; + + static List? fromJsonListDouble(final List? input) { + if (input == null) { + return null; + } + final List result = []; + for (final dynamic item in input) { + result.add(item as double); + } + return result; + } + + static List? _fromJsonListNullableDouble( + final List? input, + ) { + if (input == null) { + return null; + } + final List result = []; + for (final dynamic item in input) { + result.add(item as double?); + } + return result; + } + + static List? _fromJsonListString(final List? input) { + if (input == null) { + return null; + } + final List result = []; + for (final dynamic item in input) { + result.add(item as String); + } + return result; + } + + static List? _fromJsonListBool(final List? input) { + if (input == null) { + return null; + } + final List result = []; + for (final dynamic item in input) { + result.add(item as bool); + } + return result; + } + + final DateTime date; + final Currency currency; + final int locationOSMId; + final LocationOSMType locationOSMType; + + // per line + final List barcodes; + final List pricesAreDiscounted; + final List prices; + final List pricesWithoutDiscount; + + @override + Map toJson() { + final Map result = super.toJson(); + result[_jsonTagDate] = date.toIso8601String(); + result[_jsonTagCurrency] = currency.name; + result[_jsonTagOSMId] = locationOSMId; + result[_jsonTagOSMType] = locationOSMType.offTag; + result[_jsonTagBarcodes] = barcodes; + result[_jsonTagAreDiscounted] = pricesAreDiscounted; + result[_jsonTagPrices] = prices; + result[_jsonTagPricesWithoutDiscount] = pricesWithoutDiscount; + return result; + } + + @override + (String, AlignmentGeometry)? getFloatingMessage( + final AppLocalizations appLocalizations) => + ( + appLocalizations.add_price_queued, + AlignmentDirectional.center, + ); + + @protected + static String getStamp({ + required final DateTime date, + required final int locationOSMId, + required final LocationOSMType locationOSMType, + }) => + 'no_barcode;price;$date;$locationOSMId;$locationOSMType'; + + @override + Future preExecute(final LocalDatabase localDatabase) async {} + + @protected + Future getBearerToken() async { + final User user = getUser(); + final MaybeError token = + await OpenPricesAPIClient.getAuthenticationToken( + username: user.userId, + password: user.password, + uriHelper: ProductQuery.uriPricesHelper, + ); + if (token.isError) { + throw Exception('Could not get token: ${token.error}'); + } + if (token.value.isEmpty) { + throw Exception('Unexpected empty token'); + } + return token.value; + } + + @protected + Future addPrices({ + required final String bearerToken, + required final int proofId, + }) async { + for (int i = 0; i < barcodes.length; i++) { + final Price newPrice = Price() + ..date = date + ..currency = currency + ..locationOSMId = locationOSMId + ..locationOSMType = locationOSMType + ..proofId = proofId + ..priceIsDiscounted = pricesAreDiscounted[i] + ..price = prices[i] + ..priceWithoutDiscount = pricesWithoutDiscount[i] + ..productCode = barcodes[i]; + + // create price + final MaybeError addedPrice = + await OpenPricesAPIClient.createPrice( + price: newPrice, + bearerToken: bearerToken, + uriHelper: ProductQuery.uriPricesHelper, + ); + if (addedPrice.isError) { + throw Exception('Could not add price: ${addedPrice.error}'); + } + } + } + + @protected + Future closeSession({ + required final String bearerToken, + }) async { + final MaybeError closedSession = + await OpenPricesAPIClient.deleteUserSession( + uriHelper: ProductQuery.uriPricesHelper, + bearerToken: bearerToken, + ); + if (closedSession.isError) { + // TODO(monsieurtanuki): do we really care? + // throw Exception('Could not close session: ${closedSession.error}'); + return; + } + if (!closedSession.value) { + // TODO(monsieurtanuki): do we really care? + // throw Exception('Could not really close session'); + return; + } + } + + @override + bool isDeduplicable() => false; +} diff --git a/packages/smooth_app/lib/background/operation_type.dart b/packages/smooth_app/lib/background/operation_type.dart index 92cee088a82..f1d83e874ce 100644 --- a/packages/smooth_app/lib/background/operation_type.dart +++ b/packages/smooth_app/lib/background/operation_type.dart @@ -1,5 +1,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:smooth_app/background/background_task.dart'; +import 'package:smooth_app/background/background_task_add_other_price.dart'; import 'package:smooth_app/background/background_task_add_price.dart'; import 'package:smooth_app/background/background_task_crop.dart'; import 'package:smooth_app/background/background_task_details.dart'; @@ -37,6 +38,7 @@ enum OperationType { fullRefresh('F', 'FULL_REFRESH'), languageRefresh('L', 'LANGUAGE_REFRESH'), addPrice('A', 'ADD_PRICE'), + addOtherPrice('E', 'ADD_OTHER_PRICE'), details('D', 'PRODUCT_EDIT'); const OperationType(this.header, this.processName); @@ -70,6 +72,7 @@ enum OperationType { BackgroundTask fromJson(Map map) => switch (this) { crop => BackgroundTaskCrop.fromJson(map), addPrice => BackgroundTaskAddPrice.fromJson(map), + addOtherPrice => BackgroundTaskAddOtherPrice.fromJson(map), details => BackgroundTaskDetails.fromJson(map), hungerGames => BackgroundTaskHungerGames.fromJson(map), image => BackgroundTaskImage.fromJson(map), @@ -89,6 +92,7 @@ enum OperationType { OperationType.details => appLocalizations.background_task_operation_details, OperationType.addPrice => 'Add price', + OperationType.addOtherPrice => 'Add price to existing proof', OperationType.image => appLocalizations.background_task_operation_image, OperationType.unselect => 'Unselect a product image', OperationType.hungerGames => 'Answering to a Hunger Games question', diff --git a/packages/smooth_app/lib/pages/locations/osm_location.dart b/packages/smooth_app/lib/pages/locations/osm_location.dart index dd4742ed3ae..d66733e76c9 100644 --- a/packages/smooth_app/lib/pages/locations/osm_location.dart +++ b/packages/smooth_app/lib/pages/locations/osm_location.dart @@ -18,6 +18,20 @@ class OsmLocation { this.osmValue, }); + OsmLocation.fromPrice(final Location location) + : osmId = location.osmId, + osmType = location.type, + longitude = location.longitude ?? 0, + latitude = location.latitude ?? 0, + name = location.name, + street = null, + city = location.city, + postcode = location.postcode, + country = location.country, + countryCode = location.countryCode, + osmKey = location.osmKey, + osmValue = location.osmValue; + final int osmId; final LocationOSMType osmType; final double longitude; 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 2418130f07d..32340b08fcd 100644 --- a/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart +++ b/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart @@ -272,7 +272,7 @@ class UserPreferencesAccount extends AbstractUserPreferences { displayOwner: true, displayProduct: true, uri: OpenPricesAPIClient.getUri( - path: 'app/prices', + path: 'prices', uriHelper: ProductQuery.uriPricesHelper, ), title: appLocalizations.all_search_prices_latest_title, @@ -294,11 +294,11 @@ class UserPreferencesAccount extends AbstractUserPreferences { ), _getPriceListTile( appLocalizations.all_search_prices_top_location_title, - 'app/locations', + 'locations', ), _getPriceListTile( appLocalizations.all_search_prices_top_product_title, - 'app/products', + 'products', ), _buildProductQueryTile( productQuery: PagedToBeCompletedProductQuery( diff --git a/packages/smooth_app/lib/pages/prices/get_prices_model.dart b/packages/smooth_app/lib/pages/prices/get_prices_model.dart index 84b5333f4f7..765a3b8bba2 100644 --- a/packages/smooth_app/lib/pages/prices/get_prices_model.dart +++ b/packages/smooth_app/lib/pages/prices/get_prices_model.dart @@ -56,7 +56,7 @@ class GetPricesModel { final String barcode, ) => OpenPricesAPIClient.getUri( - path: 'app/products/$barcode', + path: 'products/$barcode', uriHelper: ProductQuery.uriPricesHelper, ); diff --git a/packages/smooth_app/lib/pages/prices/price_currency_selector.dart b/packages/smooth_app/lib/pages/prices/price_currency_selector.dart index f0941f22db1..77ca9ffafab 100644 --- a/packages/smooth_app/lib/pages/prices/price_currency_selector.dart +++ b/packages/smooth_app/lib/pages/prices/price_currency_selector.dart @@ -1,36 +1,38 @@ import 'package:flutter/material.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:provider/provider.dart'; -import 'package:smooth_app/data_models/preferences/user_preferences.dart'; import 'package:smooth_app/generic_lib/buttons/smooth_large_button_with_icon.dart'; import 'package:smooth_app/pages/onboarding/currency_selector_helper.dart'; import 'package:smooth_app/pages/prices/currency_extension.dart'; +import 'package:smooth_app/pages/prices/price_model.dart'; /// Button that displays the currency for price adding. class PriceCurrencySelector extends StatelessWidget { PriceCurrencySelector(); - final CurrencySelectorHelper helper = CurrencySelectorHelper(); + final CurrencySelectorHelper _helper = CurrencySelectorHelper(); @override Widget build(BuildContext context) { - // TODO(monsieurtanuki): use PriceModel for currency? - final UserPreferences userPreferences = context.watch(); - final Currency selected = helper.getSelected( - userPreferences.userCurrencyCode, - ); + final PriceModel model = context.watch(); return SmoothLargeButtonWithIcon( - onPressed: () async { - final Currency? currency = await helper.openCurrencySelector( - context: context, - selected: selected, - ); - if (currency != null) { - await userPreferences.setUserCurrencyCode(currency.name); - } - }, - text: selected.getFullName(), - icon: helper.currencyIconData, + onPressed: model.proof != null + ? null + : () async { + final Currency? currency = await _helper.openCurrencySelector( + context: context, + selected: model.currency, + ); + if (currency == null) { + return; + } + if (!context.mounted) { + return; + } + model.currency = currency; + }, + text: model.currency.getFullName(), + icon: _helper.currencyIconData, ); } } diff --git a/packages/smooth_app/lib/pages/prices/price_date_card.dart b/packages/smooth_app/lib/pages/prices/price_date_card.dart index 7a48e3864c3..4acce28ac87 100644 --- a/packages/smooth_app/lib/pages/prices/price_date_card.dart +++ b/packages/smooth_app/lib/pages/prices/price_date_card.dart @@ -24,30 +24,33 @@ class PriceDateCard extends StatelessWidget { SmoothLargeButtonWithIcon( text: dateFormat.format(model.date), icon: Icons.calendar_month, - onPressed: () async { - final DateTime? newDate = await showDatePicker( - context: context, - locale: Locale(ProductQuery.getLanguage().offTag), - firstDate: model.firstDate, - lastDate: model.today, - builder: (final BuildContext context, final Widget? child) { - // for some reason we don't have a fine display without that theme. - // cf. https://stackoverflow.com/questions/50321182/how-to-customize-a-date-picker - final ThemeData themeData = - Theme.of(context).brightness == Brightness.light - ? ThemeData.light() - : ThemeData.dark(); - return Theme( - data: themeData.copyWith(), - child: child!, - ); - }, - ); - if (newDate == null) { - return; - } - model.date = newDate; - }, + onPressed: model.proof != null + ? null + : () async { + final DateTime? newDate = await showDatePicker( + context: context, + locale: Locale(ProductQuery.getLanguage().offTag), + firstDate: model.firstDate, + lastDate: model.today, + builder: + (final BuildContext context, final Widget? child) { + // for some reason we don't have a fine display without that theme. + // cf. https://stackoverflow.com/questions/50321182/how-to-customize-a-date-picker + final ThemeData themeData = + Theme.of(context).brightness == Brightness.light + ? ThemeData.light() + : ThemeData.dark(); + return Theme( + data: themeData.copyWith(), + child: child!, + ); + }, + ); + if (newDate == null) { + return; + } + model.date = newDate; + }, ), ], ), diff --git a/packages/smooth_app/lib/pages/prices/price_location_card.dart b/packages/smooth_app/lib/pages/prices/price_location_card.dart index ad34b1457d6..32b920fbba8 100644 --- a/packages/smooth_app/lib/pages/prices/price_location_card.dart +++ b/packages/smooth_app/lib/pages/prices/price_location_card.dart @@ -35,39 +35,42 @@ class PriceLocationCard extends StatelessWidget { location.getSubtitle() ?? location.getLatLng().toString(), icon: location == null ? _iconTodo : _iconDone, - onPressed: () async { - final LocalDatabase localDatabase = context.read(); - final List preloadedList = - []; - for (final OsmLocation osmLocation in model.locations) { - preloadedList.add( - SearchLocationPreloadedItem( - osmLocation, - popFirst: false, - ), - ); - } - final OsmLocation? osmLocation = - await Navigator.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => SearchPage( - SearchLocationHelper(), - preloadedList: preloadedList, - autofocus: false, - ), - ), - ); - if (osmLocation == null) { - return; - } - final DaoOsmLocation daoOsmLocation = - DaoOsmLocation(localDatabase); - await daoOsmLocation.put(osmLocation); - final List newOsmLocations = - await daoOsmLocation.getAll(); - model.locations = newOsmLocations; - }, + onPressed: model.proof != null + ? null + : () async { + final LocalDatabase localDatabase = + context.read(); + final List preloadedList = + []; + for (final OsmLocation osmLocation in model.locations!) { + preloadedList.add( + SearchLocationPreloadedItem( + osmLocation, + popFirst: false, + ), + ); + } + final OsmLocation? osmLocation = + await Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => SearchPage( + SearchLocationHelper(), + preloadedList: preloadedList, + autofocus: false, + ), + ), + ); + if (osmLocation == null) { + return; + } + final DaoOsmLocation daoOsmLocation = + DaoOsmLocation(localDatabase); + await daoOsmLocation.put(osmLocation); + final List newOsmLocations = + await daoOsmLocation.getAll(); + model.locations = newOsmLocations; + }, ), ], ), diff --git a/packages/smooth_app/lib/pages/prices/price_model.dart b/packages/smooth_app/lib/pages/prices/price_model.dart index 4deeb56f153..24cf51a9fbc 100644 --- a/packages/smooth_app/lib/pages/prices/price_model.dart +++ b/packages/smooth_app/lib/pages/prices/price_model.dart @@ -1,12 +1,14 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:provider/provider.dart'; +import 'package:smooth_app/background/background_task_add_other_price.dart'; import 'package:smooth_app/background/background_task_add_price.dart'; import 'package:smooth_app/data_models/preferences/user_preferences.dart'; import 'package:smooth_app/pages/crop_parameters.dart'; import 'package:smooth_app/pages/locations/osm_location.dart'; -import 'package:smooth_app/pages/onboarding/currency_selector_helper.dart'; import 'package:smooth_app/pages/prices/price_amount_model.dart'; import 'package:smooth_app/pages/prices/price_meta_product.dart'; @@ -14,15 +16,39 @@ import 'package:smooth_app/pages/prices/price_meta_product.dart'; class PriceModel with ChangeNotifier { PriceModel({ required final ProofType proofType, - required final List locations, + required final List? locations, + required final Currency currency, final PriceMetaProduct? initialProduct, - }) : _proofType = proofType, + }) : proof = null, + _proofType = proofType, _date = DateTime.now(), + _currency = currency, _locations = locations, priceAmountModels = [ if (initialProduct != null) PriceAmountModel(product: initialProduct), ]; + PriceModel.proof({ + required Proof this.proof, + }) : _proofType = proof.type!, + _date = proof.date!, + _locations = null, + _currency = proof.currency!, + priceAmountModels = []; + + /// Checks if a proof cannot be reused for prices adding. + /// + /// Sometimes we get partial data from the Prices server. + static bool isProofNotGoodEnough(final Proof proof) => + proof.currency == null || + proof.date == null || + proof.type == null || + proof.location == null || + proof.locationOSMId == null || + proof.locationOSMType == null || + proof.imageThumbPath == null || + proof.filePath == null; + final List priceAmountModels; CropParameters? _cropParameters; @@ -34,9 +60,11 @@ class PriceModel with ChangeNotifier { notifyListeners(); } + final Proof? proof; + ProofType _proofType; - ProofType get proofType => _proofType; + ProofType get proofType => proof != null ? proof!.type! : _proofType; set proofType(final ProofType proofType) { _proofType = proofType; @@ -55,18 +83,27 @@ class PriceModel with ChangeNotifier { final DateTime today = DateTime.now(); final DateTime firstDate = DateTime.utc(2020, 1, 1); - late List _locations; + List? _locations; - List get locations => _locations; + List? get locations => _locations; - set locations(final List locations) { + set locations(final List? locations) { _locations = locations; notifyListeners(); } - OsmLocation? get location => _locations.firstOrNull; + OsmLocation? get location => proof != null + ? OsmLocation.fromPrice(proof!.location!) + : _locations!.firstOrNull; + + Currency _currency; - late Currency _checkedCurrency; + Currency get currency => _currency; + + set currency(final Currency currency) { + _currency = currency; + notifyListeners(); + } // overriding in order to make it public @override @@ -75,14 +112,15 @@ class PriceModel with ChangeNotifier { /// Returns the error message of the parameter check, or null if OK. String? checkParameters(final BuildContext context) { final AppLocalizations appLocalizations = AppLocalizations.of(context); - if (cropParameters == null) { - return appLocalizations.prices_proof_mandatory; + if (proof == null) { + if (cropParameters == null) { + return appLocalizations.prices_proof_mandatory; + } + if (location == null) { + return appLocalizations.prices_location_mandatory; + } } - final UserPreferences userPreferences = context.read(); - _checkedCurrency = - CurrencySelectorHelper().getSelected(userPreferences.userCurrencyCode); - for (final PriceAmountModel priceAmountModel in priceAmountModels) { final String? checkParameters = priceAmountModel.checkParameters(context); if (checkParameters != null) { @@ -90,9 +128,8 @@ class PriceModel with ChangeNotifier { } } - if (location == null) { - return appLocalizations.prices_location_mandatory; - } + final UserPreferences userPreferences = context.read(); + unawaited(userPreferences.setUserCurrencyCode(currency.name)); return null; } @@ -109,15 +146,32 @@ class PriceModel with ChangeNotifier { prices.add(priceAmountModel.checkedPaidPrice); pricesWithoutDiscount.add(priceAmountModel.checkedPriceWithoutDiscount); } + if (proof != null) { + return BackgroundTaskAddOtherPrice.addTask( + context: context, + // per receipt + locationOSMId: proof!.locationOSMId!, + locationOSMType: proof!.locationOSMType!, + date: proof!.date!, + currency: proof!.currency!, + proofId: proof!.id, + // per item + barcodes: barcodes, + pricesAreDiscounted: pricesAreDiscounted, + prices: prices, + pricesWithoutDiscount: pricesWithoutDiscount, + ); + } return BackgroundTaskAddPrice.addTask( context: context, - // per receipt + // proof display cropObject: cropParameters!, + // per receipt locationOSMId: location!.osmId, locationOSMType: location!.osmType, date: date, proofType: proofType, - currency: _checkedCurrency, + currency: currency, // per item barcodes: barcodes, pricesAreDiscounted: pricesAreDiscounted, diff --git a/packages/smooth_app/lib/pages/prices/price_product_widget.dart b/packages/smooth_app/lib/pages/prices/price_product_widget.dart index 776b9445cc0..ef898f5ccd4 100644 --- a/packages/smooth_app/lib/pages/prices/price_product_widget.dart +++ b/packages/smooth_app/lib/pages/prices/price_product_widget.dart @@ -26,7 +26,7 @@ class PriceProductWidget extends StatelessWidget { final String name = priceProduct.name ?? priceProduct.code; final bool unknown = priceProduct.name == null; final String? imageURL = priceProduct.imageURL; - final int priceCount = priceProduct.priceCount; + final int priceCount = priceProduct.priceCount ?? 0; final List? brands = priceProduct.brands == '' ? null : priceProduct.brands?.split(','); final String? quantity = priceProduct.quantity == null diff --git a/packages/smooth_app/lib/pages/prices/price_proof_card.dart b/packages/smooth_app/lib/pages/prices/price_proof_card.dart index d972ae2932a..091d595d189 100644 --- a/packages/smooth_app/lib/pages/prices/price_proof_card.dart +++ b/packages/smooth_app/lib/pages/prices/price_proof_card.dart @@ -11,6 +11,7 @@ import 'package:smooth_app/pages/crop_parameters.dart'; import 'package:smooth_app/pages/image_crop_page.dart'; import 'package:smooth_app/pages/prices/price_model.dart'; import 'package:smooth_app/pages/proof_crop_helper.dart'; +import 'package:smooth_app/query/product_query.dart'; /// Card that displays the proof for price adding. class PriceProofCard extends StatelessWidget { @@ -27,6 +28,20 @@ class PriceProofCard extends StatelessWidget { child: Column( children: [ Text(appLocalizations.prices_proof_subtitle), + if (model.proof != null) + LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) => + Image( + image: NetworkImage( + model.proof! + .getFileUrl( + uriProductHelper: ProductQuery.uriPricesHelper, + isThumbnail: true, + )! + .toString(), + ), + ), + ), if (model.cropParameters != null) LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) => @@ -40,23 +55,27 @@ class PriceProofCard extends StatelessWidget { ), //Text(model.cropParameters!.smallCroppedFile.path), SmoothLargeButtonWithIcon( - text: model.cropParameters == null + text: model.proof == null && model.cropParameters == null ? appLocalizations.prices_proof_find : model.proofType == ProofType.receipt ? appLocalizations.prices_proof_receipt : appLocalizations.prices_proof_price_tag, - icon: model.cropParameters == null ? _iconTodo : _iconDone, - onPressed: () async { - final CropParameters? cropParameters = - await confirmAndUploadNewImage( - context, - cropHelper: ProofCropHelper(model: model), - isLoggedInMandatory: true, - ); - if (cropParameters != null) { - model.cropParameters = cropParameters; - } - }, + icon: model.proof == null && model.cropParameters == null + ? _iconTodo + : _iconDone, + onPressed: model.proof != null + ? null + : () async { + final CropParameters? cropParameters = + await confirmAndUploadNewImage( + context, + cropHelper: ProofCropHelper(model: model), + isLoggedInMandatory: true, + ); + if (cropParameters != null) { + model.cropParameters = cropParameters; + } + }, ), LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) => Row( @@ -67,8 +86,10 @@ class PriceProofCard extends StatelessWidget { title: Text(appLocalizations.prices_proof_receipt), value: ProofType.receipt, groupValue: model.proofType, - onChanged: (final ProofType? proofType) => - model.proofType = proofType!, + onChanged: model.proof != null + ? null + : (final ProofType? proofType) => + model.proofType = proofType!, ), ), SizedBox( @@ -77,8 +98,10 @@ class PriceProofCard extends StatelessWidget { title: Text(appLocalizations.prices_proof_price_tag), value: ProofType.priceTag, groupValue: model.proofType, - onChanged: (final ProofType? proofType) => - model.proofType = proofType!, + onChanged: model.proof != null + ? null + : (final ProofType? proofType) => + model.proofType = proofType!, ), ), ], diff --git a/packages/smooth_app/lib/pages/prices/price_proof_page.dart b/packages/smooth_app/lib/pages/prices/price_proof_page.dart index 84a8eea48e9..7077340dc7c 100644 --- a/packages/smooth_app/lib/pages/prices/price_proof_page.dart +++ b/packages/smooth_app/lib/pages/prices/price_proof_page.dart @@ -3,6 +3,9 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:intl/intl.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:smooth_app/helpers/launch_url_helper.dart'; +import 'package:smooth_app/pages/prices/price_model.dart'; +import 'package:smooth_app/pages/prices/product_price_add_page.dart'; +import 'package:smooth_app/pages/product/common/product_refresher.dart'; import 'package:smooth_app/query/product_query.dart'; import 'package:smooth_app/widgets/smooth_app_bar.dart'; import 'package:smooth_app/widgets/smooth_scaffold.dart'; @@ -21,6 +24,30 @@ class PriceProofPage extends StatelessWidget { final DateFormat dateFormat = DateFormat.yMd(ProductQuery.getLocaleString()).add_Hms(); return SmoothScaffold( + floatingActionButton: PriceModel.isProofNotGoodEnough(proof) + ? null + : FloatingActionButton.extended( + label: Text(appLocalizations.prices_add_a_price), + icon: const Icon(Icons.add), + onPressed: () async { + if (!await ProductRefresher().checkIfLoggedIn( + context, + isLoggedInMandatory: true, + )) { + return; + } + if (!context.mounted) { + return; + } + await Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => ProductPriceAddPage( + PriceModel.proof(proof: proof), + ), + ), + ); + }, + ), appBar: SmoothAppBar( title: Text(appLocalizations.user_search_proof_title), subTitle: Text(dateFormat.format(proof.created)), @@ -28,18 +55,39 @@ class PriceProofPage extends StatelessWidget { IconButton( tooltip: appLocalizations.prices_app_button, icon: const Icon(Icons.open_in_new), - onPressed: () async => LaunchUrlHelper.launchURL(_getUrl()), + onPressed: () async => LaunchUrlHelper.launchURL(_getUrl(true)), ), ], ), - body: Image( - image: NetworkImage(_getUrl()), - fit: BoxFit.cover, + body: Center( + child: Image.network( + _getUrl(false), + fit: BoxFit.cover, + loadingBuilder: (BuildContext context, Widget child, + ImageChunkEvent? loadingProgress) { + if (loadingProgress == null) { + return child; + } + return Center( + child: SizedBox( + width: double.maxFinite, + height: double.maxFinite, + child: Image.network( + _getUrl(true), + fit: BoxFit.contain, + ), + ), + ); + }, + ), ), ); } - String _getUrl() => proof - .getFileUrl(uriProductHelper: ProductQuery.uriPricesHelper) + String _getUrl(final bool isThumbnail) => proof + .getFileUrl( + uriProductHelper: ProductQuery.uriPricesHelper, + isThumbnail: isThumbnail, + ) .toString(); } diff --git a/packages/smooth_app/lib/pages/prices/price_user_button.dart b/packages/smooth_app/lib/pages/prices/price_user_button.dart index c9521ab4fc2..070af93e861 100644 --- a/packages/smooth_app/lib/pages/prices/price_user_button.dart +++ b/packages/smooth_app/lib/pages/prices/price_user_button.dart @@ -41,7 +41,7 @@ class PriceUserButton extends StatelessWidget { displayOwner: false, displayProduct: true, uri: OpenPricesAPIClient.getUri( - path: 'app/users/$user', + path: 'users/$user', uriHelper: ProductQuery.uriPricesHelper, ), title: showUserTitle(user: user, context: context), diff --git a/packages/smooth_app/lib/pages/prices/prices_proofs_page.dart b/packages/smooth_app/lib/pages/prices/prices_proofs_page.dart index d1a8d842f7d..baa2cf2ddbb 100644 --- a/packages/smooth_app/lib/pages/prices/prices_proofs_page.dart +++ b/packages/smooth_app/lib/pages/prices/prices_proofs_page.dart @@ -46,7 +46,7 @@ class _PricesProofsPageState extends State icon: const Icon(Icons.open_in_new), onPressed: () async => LaunchUrlHelper.launchURL( OpenPricesAPIClient.getUri( - path: 'app/dashboard/proofs', + path: 'dashboard/proofs', uriHelper: ProductQuery.uriPricesHelper, ).toString(), ), @@ -204,6 +204,7 @@ class _PriceProofImage extends StatelessWidget { proof .getFileUrl( uriProductHelper: ProductQuery.uriPricesHelper, + isThumbnail: true, ) .toString(), ), diff --git a/packages/smooth_app/lib/pages/prices/prices_users_page.dart b/packages/smooth_app/lib/pages/prices/prices_users_page.dart index 6e0b801c37d..f430bae6d40 100644 --- a/packages/smooth_app/lib/pages/prices/prices_users_page.dart +++ b/packages/smooth_app/lib/pages/prices/prices_users_page.dart @@ -45,7 +45,7 @@ class _PricesUsersPageState extends State icon: const Icon(Icons.open_in_new), onPressed: () async => LaunchUrlHelper.launchURL( OpenPricesAPIClient.getUri( - path: 'app/users', + path: 'users', uriHelper: ProductQuery.uriPricesHelper, ).toString(), ), @@ -79,6 +79,7 @@ class _PricesUsersPageState extends State final List children = []; for (final PriceUser item in result.items!) { + final int priceCount = item.priceCount ?? 0; children.add( SmoothCard( child: Wrap( @@ -91,13 +92,13 @@ class _PricesUsersPageState extends State context: context, ), iconData: Icons.label, - title: '${item.priceCount}', + title: '$priceCount', buttonStyle: ElevatedButton.styleFrom( foregroundColor: PriceCountWidget.getForegroundColor( - item.priceCount, + priceCount, ), backgroundColor: PriceCountWidget.getBackgroundColor( - item.priceCount, + priceCount, ), ), ), 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 9276299c707..13d71fac9fc 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 @@ -10,6 +10,7 @@ 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/pages/locations/osm_location.dart'; +import 'package:smooth_app/pages/onboarding/currency_selector_helper.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_currency_card.dart'; @@ -24,15 +25,11 @@ import 'package:smooth_app/widgets/smooth_scaffold.dart'; /// Single page that displays all the elements of price adding. class ProductPriceAddPage extends StatefulWidget { - const ProductPriceAddPage({ - required this.product, - required this.latestOsmLocations, - required this.proofType, - }); + const ProductPriceAddPage( + this.model, + ); - final PriceMetaProduct? product; - final List latestOsmLocations; - final ProofType proofType; + final PriceModel model; static Future showProductPage({ required final BuildContext context, @@ -55,12 +52,20 @@ class ProductPriceAddPage extends StatefulWidget { return; } + final UserPreferences userPreferences = context.read(); + final Currency currency = CurrencySelectorHelper().getSelected( + userPreferences.userCurrencyCode, + ); + await Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) => ProductPriceAddPage( - product: product, - latestOsmLocations: osmLocations, - proofType: proofType, + PriceModel( + proofType: proofType, + locations: osmLocations, + initialProduct: product, + currency: currency, + ), ), ), ); @@ -72,19 +77,13 @@ class ProductPriceAddPage extends StatefulWidget { class _ProductPriceAddPageState extends State with TraceableClientMixin { - late final PriceModel _model = PriceModel( - proofType: widget.proofType, - locations: widget.latestOsmLocations, - initialProduct: widget.product, - ); - final GlobalKey _formKey = GlobalKey(); @override Widget build(BuildContext context) { // TODO(monsieurtanuki): add WillPopScope2 return ChangeNotifierProvider.value( - value: _model, + value: widget.model, builder: ( final BuildContext context, final Widget? child, @@ -231,5 +230,6 @@ class _ProductPriceAddPageState extends State } @override - String get actionName => 'Opened price_page with ${widget.proofType.offTag}'; + String get actionName => + 'Opened price_page with ${widget.model.proofType.offTag}'; } diff --git a/packages/smooth_app/pubspec.lock b/packages/smooth_app/pubspec.lock index 4eb9b6dc02b..7218afe031f 100644 --- a/packages/smooth_app/pubspec.lock +++ b/packages/smooth_app/pubspec.lock @@ -253,22 +253,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff - url: "https://pub.dev" - source: hosted - version: "2.0.3" - cli_util: - dependency: transitive - description: - name: cli_util - sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 - url: "https://pub.dev" - source: hosted - version: "0.4.1" clock: dependency: transitive description: @@ -623,14 +607,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.4+1" - flutter_launcher_icons: - dependency: "direct dev" - description: - name: flutter_launcher_icons - sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" - url: "https://pub.dev" - source: hosted - version: "0.13.1" flutter_lints: dependency: "direct dev" description: @@ -775,10 +751,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "5cf5fdcf853b0629deb35891c7af643be900c3dcaed7489009f9e7dbcfe55ab6" + sha256: "6f1b756f6e863259a99135ff3c95026c3cdca17d10ebef2bba2261a25ddc8bbc" url: "https://pub.dev" source: hosted - version: "14.2.8" + version: "14.3.0" graphs: dependency: transitive description: @@ -1100,10 +1076,10 @@ packages: dependency: "direct main" description: name: openfoodfacts - sha256: "9ae8bfaed74c0e59bfe2cd217e2ad805d2d52710ec563861ea728176b8cd419f" + sha256: d35a213d6354246e3b27e0b18fe3def658e00c337346cf11c4f8ba082f92dfaa url: "https://pub.dev" source: hosted - version: "3.15.0" + version: "3.16.0" openfoodfacts_flutter_lints: dependency: "direct dev" description: diff --git a/packages/smooth_app/pubspec.yaml b/packages/smooth_app/pubspec.yaml index 81f2d9e9267..6580a2790c1 100644 --- a/packages/smooth_app/pubspec.yaml +++ b/packages/smooth_app/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: sdk: flutter async: 2.11.0 - go_router: 14.2.8 + go_router: 14.3.0 barcode_widget: 2.0.4 carousel_slider: 5.0.0 cupertino_icons: 1.0.8 @@ -99,7 +99,7 @@ dependencies: path: ../scanner/zxing - openfoodfacts: 3.15.0 + openfoodfacts: 3.16.0 # openfoodfacts: # path: ../../../openfoodfacts-dart @@ -108,7 +108,6 @@ dev_dependencies: sdk: flutter flutter_driver: sdk: flutter - flutter_launcher_icons: 0.13.1 flutter_test: sdk: flutter mockito: 5.4.4 @@ -118,18 +117,6 @@ dev_dependencies: openfoodfacts_flutter_lints: git: https://github.com/openfoodfacts/openfoodfacts_flutter_lints.git -# 'flutter pub run flutter_launcher_icons:main' to update -flutter_icons: - android: "launcher_icon" - ios: true - remove_alpha_ios: true - image_path: "assets/app/release_icon_transparent_1152x1152.png" - adaptive_icon_background: "#FFFFFF" - # Only the inner 72x72dp of the 108x108dp adaptive icon is shown - # (extra padding of 18dp on all sides is used for visual effects) - # https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive - adaptive_icon_foreground: "assets/app/release_icon_transparent_70pct_1152x1152.png" - # 'flutter pub run flutter_native_splash:create' to update flutter_native_splash: color: "#FFFFFF"