diff --git a/packages/smooth_app/ios/Runner.xcodeproj/project.pbxproj b/packages/smooth_app/ios/Runner.xcodeproj/project.pbxproj
index 7151bd5c0f..bf2b4585c0 100644
--- a/packages/smooth_app/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/smooth_app/ios/Runner.xcodeproj/project.pbxproj
@@ -566,4 +566,4 @@
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
-}
+}
\ No newline at end of file
diff --git a/packages/smooth_app/ios/Runner/Info.plist b/packages/smooth_app/ios/Runner/Info.plist
index 926161b854..f38ae585b2 100644
--- a/packages/smooth_app/ios/Runner/Info.plist
+++ b/packages/smooth_app/ios/Runner/Info.plist
@@ -1,59 +1,59 @@
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleDisplayName
- OpenFoodFacts
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- OpenFoodFacts
- CFBundlePackageType
- APPL
- CFBundleShortVersionString
- $(FLUTTER_BUILD_NAME)
- CFBundleSignature
- ????
- CFBundleVersion
- $(FLUTTER_BUILD_NUMBER)
- LSRequiresIPhoneOS
-
- UILaunchStoryboardName
- LaunchScreen
- UIMainStoryboardFile
- Main
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
-
- UISupportedInterfaceOrientations~ipad
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
-
- UIViewControllerBasedStatusBarAppearance
-
- UIStatusBarHidden
-
- NSPhotoLibraryUsageDescription
- This app needs to access photo library for product photos uploads
- NSCameraUsageDescription
- This app needs Camera Usage for scanning barcodes and cropping photos
- LSApplicationQueriesSchemes
-
- https
- http
- mailto
-
- UIRequiresFullScreen
-
- CADisableMinimumFrameDurationOnPhone
+
+ CADisableMinimumFrameDurationOnPhone
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ OpenFoodFacts
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ OpenFoodFacts
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSApplicationQueriesSchemes
+
+ https
+ http
+ mailto
+
+ LSRequiresIPhoneOS
+
+ NSCameraUsageDescription
+ This app needs Camera Usage for scanning barcodes and cropping photos
+ NSPhotoLibraryUsageDescription
+ This app needs to access photo library for product photos uploads
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UIRequiresFullScreen
+
+ UIStatusBarHidden
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+
+ UIViewControllerBasedStatusBarAppearance
+
diff --git a/packages/smooth_app/lib/cards/data_cards/image_upload_card.dart b/packages/smooth_app/lib/cards/data_cards/image_upload_card.dart
index b6824fb3d7..0bc11c59b4 100644
--- a/packages/smooth_app/lib/cards/data_cards/image_upload_card.dart
+++ b/packages/smooth_app/lib/cards/data_cards/image_upload_card.dart
@@ -1,5 +1,4 @@
import 'dart:io';
-
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
@@ -46,35 +45,17 @@ class _ImageUploadCardState extends State {
if (!mounted) {
return;
}
- final bool isUploaded = await uploadCapturedPicture(
+ await uploadCapturedPicture(
context,
barcode: widget.product
.barcode!, //Probably throws an error, but this is not a big problem when we got a product without a barcode
imageField: widget.productImageData.imageField,
imageUri: croppedImageFile.uri,
);
- croppedImageFile.delete();
+
if (!mounted) {
return;
}
- if (isUploaded) {
- if (widget.productImageData.imageField == ImageField.OTHER) {
- final AppLocalizations appLocalizations =
- AppLocalizations.of(context);
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text(appLocalizations.other_photo_uploaded),
- duration: const Duration(seconds: 3),
- action: SnackBarAction(
- label: appLocalizations.more_photos,
- onPressed: _getImage,
- ),
- ),
- );
- } else {
- await widget.onUpload(context);
- }
- }
}
}
diff --git a/packages/smooth_app/lib/data_models/continuous_scan_model.dart b/packages/smooth_app/lib/data_models/continuous_scan_model.dart
index 457c3ab117..8b0a8a781b 100644
--- a/packages/smooth_app/lib/data_models/continuous_scan_model.dart
+++ b/packages/smooth_app/lib/data_models/continuous_scan_model.dart
@@ -8,6 +8,7 @@ import 'package:smooth_app/data_models/product_list.dart';
import 'package:smooth_app/database/dao_product.dart';
import 'package:smooth_app/database/dao_product_list.dart';
import 'package:smooth_app/database/local_database.dart';
+import 'package:smooth_app/generic_lib/duration_constants.dart';
import 'package:smooth_app/helpers/analytics_helper.dart';
import 'package:smooth_app/query/barcode_product_query.dart';
import 'package:smooth_app/services/smooth_services.dart';
@@ -170,7 +171,7 @@ class ContinuousScanModel with ChangeNotifier {
try {
// We try to load the fresh copy of product from the server
final FetchedProduct fetchedProduct =
- await _queryBarcode(barcode).timeout(const Duration(seconds: 5));
+ await _queryBarcode(barcode).timeout(SnackBarDuration.long);
if (fetchedProduct.product != null) {
_addProduct(barcode, ScannedProductState.CACHED);
return true;
diff --git a/packages/smooth_app/lib/data_models/onboarding_data_product.dart b/packages/smooth_app/lib/data_models/onboarding_data_product.dart
index 9b8cad5941..35e7123afd 100644
--- a/packages/smooth_app/lib/data_models/onboarding_data_product.dart
+++ b/packages/smooth_app/lib/data_models/onboarding_data_product.dart
@@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:smooth_app/data_models/abstract_onboarding_data.dart';
import 'package:smooth_app/database/local_database.dart';
+import 'package:smooth_app/generic_lib/duration_constants.dart';
import 'package:smooth_app/query/product_query.dart';
/// Helper around a product we download, store and reuse at onboarding.
@@ -42,7 +43,7 @@ class OnboardingDataProduct extends AbstractOnboardingData {
language: ProductQuery.getLanguage(),
country: ProductQuery.getCountry(),
),
- ).timeout(const Duration(seconds: 5));
+ ).timeout(SnackBarDuration.long);
@override
String getAssetPath() => assetPath;
diff --git a/packages/smooth_app/lib/data_models/onboarding_loader.dart b/packages/smooth_app/lib/data_models/onboarding_loader.dart
index 04d1ae9417..63715b0149 100644
--- a/packages/smooth_app/lib/data_models/onboarding_loader.dart
+++ b/packages/smooth_app/lib/data_models/onboarding_loader.dart
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:smooth_app/data_models/onboarding_data_product.dart';
import 'package:smooth_app/database/local_database.dart';
+import 'package:smooth_app/generic_lib/duration_constants.dart';
import 'package:smooth_app/generic_lib/loading_dialog.dart';
import 'package:smooth_app/pages/onboarding/onboarding_flow_navigator.dart';
@@ -33,7 +34,7 @@ class OnboardingLoader {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(appLocalizations.onboarding_welcome_loading_error),
- duration: const Duration(seconds: 2),
+ duration: SnackBarDuration.short,
behavior: SnackBarBehavior.floating,
elevation: 0,
),
diff --git a/packages/smooth_app/lib/generic_lib/duration_constants.dart b/packages/smooth_app/lib/generic_lib/duration_constants.dart
index 48e0e64df7..4df9e451ad 100644
--- a/packages/smooth_app/lib/generic_lib/duration_constants.dart
+++ b/packages/smooth_app/lib/generic_lib/duration_constants.dart
@@ -6,3 +6,12 @@ class SmoothAnimationsDuration {
static const Duration medium = Duration(milliseconds: 400);
static const Duration long = Duration(milliseconds: 500);
}
+
+class SnackBarDuration {
+ const SnackBarDuration._();
+
+ static const Duration brief = Duration(seconds: 1);
+ static const Duration short = Duration(seconds: 2);
+ static const Duration medium = Duration(seconds: 3);
+ static const Duration long = Duration(seconds: 5);
+}
diff --git a/packages/smooth_app/lib/helpers/background_task_helper.dart b/packages/smooth_app/lib/helpers/background_task_helper.dart
new file mode 100644
index 0000000000..1be43e4d4b
--- /dev/null
+++ b/packages/smooth_app/lib/helpers/background_task_helper.dart
@@ -0,0 +1,249 @@
+import 'dart:convert';
+import 'dart:io';
+import 'package:openfoodfacts/openfoodfacts.dart';
+import 'package:openfoodfacts/utils/CountryHelper.dart';
+import 'package:smooth_app/database/dao_product.dart';
+import 'package:smooth_app/database/local_database.dart';
+import 'package:smooth_app/query/product_query.dart';
+import 'package:task_manager/task_manager.dart';
+
+/// Task Manager IDs and extras
+/// Broadly classify the tasks into 2 categories
+/// Task ID for image uploads
+const String IMAGE_UPLOAD_TASK = 'IMAGE_UPLOAD';
+
+/// Task ID for product edition
+const String PRODUCT_EDIT_TASK = 'PRODUCT_EDIT';
+
+/// Extra used for product edition
+/// Constant for ingredient edit task
+const String INGREDIENT_EDIT = 'INGREDIENTS_EDIT';
+
+/// Constant for nutrition edit task
+const String NUTRITION_EDIT = 'NUTRITION_EDIT';
+
+/// Constant for basic details edit task
+const String BASIC_DETAILS = 'BASIC_DETAILS';
+
+/// Response code sent by the server in case of a success
+const int SUCESS_CODE = 1;
+
+/// Runs whenever a task is started in the background.
+/// Whatever invoked with TaskManager.addTask() will be run in this method.
+/// Gets automatically invoked when there is a task added to the queue and the network conditions are favorable.
+Future callbackDispatcher(
+ LocalDatabase localDatabase,
+) async {
+ await TaskManager().init(
+ runTasksInIsolates: false,
+ executor: (Task inputData) async {
+ final String processName = inputData.data!['processName'] as String;
+ switch (processName) {
+ case IMAGE_UPLOAD_TASK:
+ return uploadImage(inputData.data!, localDatabase);
+
+ case PRODUCT_EDIT_TASK:
+ return otherDetails(inputData.data!, localDatabase);
+
+ default:
+ return TaskResult.success;
+ }
+ },
+ );
+ return TaskResult.success;
+}
+
+/// This takes the product JSON and uploads the data to OpenFoodFacts server
+/// and queries the updated [Product], then it updates the product in the local database
+Future otherDetails(
+ Map inputData,
+ LocalDatabase localDatabase,
+) async {
+ final BackgroundOtherDetailsInput inputTask =
+ BackgroundOtherDetailsInput.fromJson(inputData);
+ final Map productMap =
+ json.decode(inputTask.inputMap) as Map;
+ final User user =
+ User.fromJson(jsonDecode(inputTask.user) as Map);
+
+ await OpenFoodAPIClient.saveProduct(
+ user,
+ Product.fromJson(productMap),
+ language: LanguageHelper.fromJson(inputTask.languageCode),
+ country: CountryHelper.fromJson(inputTask.country),
+ );
+
+ final DaoProduct daoProduct = DaoProduct(localDatabase);
+ final ProductQueryConfiguration configuration = ProductQueryConfiguration(
+ inputTask.barcode,
+ fields: ProductQuery.fields,
+ language: LanguageHelper.fromJson(inputTask.languageCode),
+ country: CountryHelper.fromJson(inputTask.country),
+ );
+
+ final ProductResult queryResult =
+ await OpenFoodAPIClient.getProduct(configuration);
+
+ if (queryResult.status == SUCESS_CODE) {
+ final Product? product = queryResult.product;
+ if (product != null) {
+ await daoProduct.put(product);
+ localDatabase.notifyListeners();
+ }
+ }
+
+ // Returns true to let platform know that the task is completed
+ return TaskResult.success;
+}
+
+/// This takes an image and uploads it to OpenFoodFacts servers
+/// and queries the updated [Product].
+/// Then it updates the product in the local database.
+Future uploadImage(
+ Map inputData,
+ LocalDatabase localDatabase,
+) async {
+ final BackgroundImageInputData inputTask =
+ BackgroundImageInputData.fromJson(inputData);
+ final User user =
+ User.fromJson(jsonDecode(inputTask.user) as Map);
+
+ final SendImage image = SendImage(
+ lang: LanguageHelper.fromJson(inputTask.languageCode),
+ barcode: inputTask.barcode,
+ imageField: ImageFieldExtension.getType(inputTask.imageField),
+ imageUri: Uri.parse(inputTask.imagePath),
+ );
+
+ await OpenFoodAPIClient.addProductImage(user, image);
+
+ // Go to the file system and delete the file that was uploaded
+ File(inputTask.imagePath).deleteSync();
+ final DaoProduct daoProduct = DaoProduct(localDatabase);
+ final ProductQueryConfiguration configuration = ProductQueryConfiguration(
+ inputTask.barcode,
+ fields: ProductQuery.fields,
+ language: LanguageHelper.fromJson(inputTask.languageCode),
+ country: CountryHelper.fromJson(inputTask.country),
+ );
+
+ final ProductResult queryResult =
+ await OpenFoodAPIClient.getProduct(configuration);
+ if (queryResult.status == SUCESS_CODE) {
+ final Product? product = queryResult.product;
+ if (product != null) {
+ await daoProduct.put(product);
+ localDatabase.notifyListeners();
+ }
+ }
+
+ return TaskResult.success;
+}
+
+/// Helper class for serialization and deserialization of data used by the task manager
+class BackgroundImageInputData {
+ BackgroundImageInputData({
+ required this.processName,
+ required this.uniqueId,
+ required this.barcode,
+ required this.imageField,
+ required this.imagePath,
+ required this.languageCode,
+ required this.user,
+ required this.country,
+ });
+
+ BackgroundImageInputData.fromJson(Map json)
+ : processName = json['processName'] as String,
+ uniqueId = json['uniqueId'] as String,
+ barcode = json['barcode'] as String,
+ imageField = json['imageField'] as String,
+ imagePath = json['imagePath'] as String,
+ languageCode = json['languageCode'] as String,
+ user = json['user'] as String,
+ country = json['country'] as String;
+
+ final String processName;
+ final String uniqueId;
+ final String barcode;
+ final String imageField;
+ final String imagePath;
+ final String languageCode;
+ final String user;
+ final String country;
+
+ Map toJson() => {
+ 'processName': processName,
+ 'uniqueId': uniqueId,
+ 'barcode': barcode,
+ 'imageField': imageField,
+ 'imagePath': imagePath,
+ 'languageCode': languageCode,
+ 'user': user,
+ 'country': country,
+ };
+}
+
+class BackgroundOtherDetailsInput {
+ BackgroundOtherDetailsInput({
+ required this.processName,
+ required this.uniqueId,
+ required this.barcode,
+ required this.languageCode,
+ required this.inputMap,
+ required this.user,
+ required this.country,
+ });
+
+ BackgroundOtherDetailsInput.fromJson(Map json)
+ : processName = json['processName'] as String,
+ uniqueId = json['uniqueId'] as String,
+ barcode = json['barcode'] as String,
+ languageCode = json['languageCode'] as String,
+ inputMap = json['inputMap'] as String,
+ user = json['user'] as String,
+ country = json['country'] as String;
+
+ final String processName;
+ final String uniqueId;
+ final String barcode;
+ final String languageCode;
+ final String inputMap;
+ final String user;
+ final String country;
+
+ Map toJson() => {
+ 'processName': processName,
+ 'uniqueId': uniqueId,
+ 'barcode': barcode,
+ 'languageCode': languageCode,
+ 'inputMap': inputMap,
+ 'user': user,
+ 'country': country,
+ };
+}
+
+/// Generates a unique id for the background task
+/// This ensures that the background task is unique and also
+/// ensures that in case of conflicts, the background task is replaced
+/// Example: 00000000_BASIC_DETAILS_en_us_"random_user_id"
+class UniqueIdGenerator {
+ const UniqueIdGenerator._();
+ static String generateUniqueId(
+ String barcode,
+ String processIdentifier,
+ ) {
+ final StringBuffer stringBuffer = StringBuffer();
+ stringBuffer
+ ..write(barcode)
+ ..write('_')
+ ..write(processIdentifier)
+ ..write('_')
+ ..write(ProductQuery.getLanguage().code)
+ ..write('_')
+ ..write(ProductQuery.getCountry()!.iso2Code)
+ ..write('_')
+ ..write(ProductQuery.getUser().userId);
+ return stringBuffer.toString();
+ }
+}
diff --git a/packages/smooth_app/lib/helpers/picture_capture_helper.dart b/packages/smooth_app/lib/helpers/picture_capture_helper.dart
index e4661caece..9437a6eee7 100644
--- a/packages/smooth_app/lib/helpers/picture_capture_helper.dart
+++ b/packages/smooth_app/lib/helpers/picture_capture_helper.dart
@@ -1,10 +1,16 @@
+import 'dart:convert';
+import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
+import 'package:openfoodfacts/utils/CountryHelper.dart';
import 'package:provider/provider.dart';
import 'package:smooth_app/data_models/continuous_scan_model.dart';
-import 'package:smooth_app/generic_lib/loading_dialog.dart';
+import 'package:smooth_app/database/local_database.dart';
+import 'package:smooth_app/generic_lib/duration_constants.dart';
+import 'package:smooth_app/helpers/background_task_helper.dart';
import 'package:smooth_app/query/product_query.dart';
+import 'package:task_manager/task_manager.dart';
Future uploadCapturedPicture(
BuildContext context, {
@@ -13,32 +19,68 @@ Future uploadCapturedPicture(
required Uri imageUri,
}) async {
final AppLocalizations appLocalizations = AppLocalizations.of(context);
- final SendImage image = SendImage(
- lang: ProductQuery.getLanguage(),
+ final LocalDatabase localDatabase = context.read();
+ final String uniqueId = _getUniqueId(imageField, barcode);
+ final BackgroundImageInputData backgroundImageInputData =
+ BackgroundImageInputData(
+ processName: IMAGE_UPLOAD_TASK,
+ uniqueId: uniqueId,
barcode: barcode,
- imageField: imageField,
- imageUri: imageUri,
+ imageField: imageField.value,
+ imagePath: File(imageUri.path).path,
+ languageCode: ProductQuery.getLanguage().code,
+ user: jsonEncode(ProductQuery.getUser().toJson()),
+ country: ProductQuery.getCountry()!.iso2Code,
);
- final Status? result = await LoadingDialog.run(
- context: context,
- future: OpenFoodAPIClient.addProductImage(
- ProductQuery.getUser(),
- image,
+ await TaskManager().addTask(
+ Task(
+ data: backgroundImageInputData.toJson(),
+ uniqueId: uniqueId,
+ ),
+ );
+
+ localDatabase.notifyListeners();
+ // ignore: use_build_context_synchronously
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(
+ appLocalizations.image_upload_queued,
+ ),
+ duration: SnackBarDuration.medium,
),
- title: appLocalizations.uploading_image,
);
- if (result == null || result.error != null || result.status != 'status ok') {
- await LoadingDialog.error(
- context: context,
- title: appLocalizations.error_occurred,
- );
- return false;
- }
//ignore: use_build_context_synchronously
await _updateContinuousScanModel(context, barcode);
return true;
}
+/// Generates a unique id for the task,in case of tasks with the same name,
+/// it gets replaced with the new one,also for other images we randomize the
+/// id with date time so that it runs separately.
+/// Example: 00000000_front_en_us_"random_user_id" or
+/// 00000000_other_en_us_"random_user_id"_1661677638662
+String _getUniqueId(ImageField imageField, String barcode) {
+// Use String buffer to concatenate strings
+ final StringBuffer stringBuffer = StringBuffer();
+ stringBuffer
+ ..write(barcode)
+ ..write('_')
+ ..write(imageField.value)
+ ..write('_')
+ ..write(ProductQuery.getLanguage().code)
+ ..write('_')
+ ..write(ProductQuery.getCountry()!.iso2Code)
+ ..write('_')
+ ..write(ProductQuery.getUser().userId);
+ if (imageField != ImageField.OTHER) {
+ return stringBuffer.toString();
+ }
+ stringBuffer
+ ..write('_')
+ ..write(DateTime.now().millisecondsSinceEpoch);
+ return stringBuffer.toString();
+}
+
Future _updateContinuousScanModel(
BuildContext context, String barcode) async {
final ContinuousScanModel model = context.read();
diff --git a/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_page.dart b/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_page.dart
index a591d6d5f8..2a6142ef4b 100644
--- a/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_page.dart
+++ b/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_page.dart
@@ -8,6 +8,7 @@ import 'package:openfoodfacts/model/Product.dart';
import 'package:provider/provider.dart';
import 'package:smooth_app/database/local_database.dart';
import 'package:smooth_app/generic_lib/design_constants.dart';
+import 'package:smooth_app/generic_lib/duration_constants.dart';
import 'package:smooth_app/generic_lib/widgets/smooth_card.dart';
import 'package:smooth_app/knowledge_panel/knowledge_panels/knowledge_panel_expanded_card.dart';
import 'package:smooth_app/pages/inherited_data_manager.dart';
@@ -93,7 +94,7 @@ class _KnowledgePanelPageState extends State
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(appLocalizations.product_refreshed),
- duration: const Duration(seconds: 2),
+ duration: SnackBarDuration.short,
),
);
}
diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb
index d383d294ce..4cb954b758 100644
--- a/packages/smooth_app/lib/l10n/app_en.arb
+++ b/packages/smooth_app/lib/l10n/app_en.arb
@@ -1573,6 +1573,10 @@
"@choose_from_gallery": {
"description": "Button label for choosing a photo from gallery"
},
+ "image_upload_queued": "The image will be uploaded in the background as soon as possible.",
+ "@image_upload_queued": {
+ "description": "Message when a photo is queued for upload"
+ },
"expand_nutrition_facts": "Expand nutrition facts table",
"@expand_nutrition_facts": {
"description": "Label for expanding nutrition facts table in application setting"
@@ -1614,6 +1618,10 @@
"@help_with_openfoodfacts": {
"description": "Label for the email title"
},
+ "product_task_background_schedule": "The product will be updated in the background as soon as possible.",
+ "@product_task_background_schedule": {
+ "description": "Message when a product is scheduled for background update"
+ },
"no_email_client_available_dialog_title": "No email application!",
"@no_email_client_available_dialog_title": {
"description": "Title for the dialog when no email client is installed on the device"
diff --git a/packages/smooth_app/lib/main.dart b/packages/smooth_app/lib/main.dart
index a61ba52ab4..fe199f2290 100644
--- a/packages/smooth_app/lib/main.dart
+++ b/packages/smooth_app/lib/main.dart
@@ -18,6 +18,7 @@ import 'package:smooth_app/data_models/user_preferences.dart';
import 'package:smooth_app/database/dao_string.dart';
import 'package:smooth_app/database/local_database.dart';
import 'package:smooth_app/helpers/analytics_helper.dart';
+import 'package:smooth_app/helpers/background_task_helper.dart';
import 'package:smooth_app/helpers/camera_helper.dart';
import 'package:smooth_app/helpers/data_importer/smooth_app_data_importer.dart';
import 'package:smooth_app/helpers/network_config.dart';
@@ -41,7 +42,6 @@ Future main({final bool screenshots = false}) async {
final WidgetsBinding widgetsBinding =
WidgetsFlutterBinding.ensureInitialized();
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
-
if (kReleaseMode) {
await AnalyticsHelper.initSentry(
appRunner: () => runApp(const SmoothApp()),
@@ -99,6 +99,7 @@ Future _init1() async {
),
daoString: DaoString(_localDatabase),
);
+ await callbackDispatcher(_localDatabase);
AnalyticsHelper.setCrashReports(_userPreferences.crashReports);
ProductQuery.setCountry(_userPreferences.userCountryCode);
diff --git a/packages/smooth_app/lib/pages/offline_tasks_page.dart b/packages/smooth_app/lib/pages/offline_tasks_page.dart
new file mode 100644
index 0000000000..6abbe2fa89
--- /dev/null
+++ b/packages/smooth_app/lib/pages/offline_tasks_page.dart
@@ -0,0 +1,202 @@
+import 'package:flutter/material.dart';
+import 'package:smooth_app/generic_lib/duration_constants.dart';
+import 'package:task_manager/task_manager.dart';
+
+// TODO(ashaman999): add the translations later
+const int POPUP_MENU_FIRST_ITEM = 0;
+
+class OfflineTaskPage extends StatefulWidget {
+ const OfflineTaskPage({
+ super.key,
+ });
+
+ @override
+ State createState() => _OfflineTaskState();
+}
+
+class _OfflineTaskState extends State {
+ Future> _fetchListItems() async {
+ return TaskManager().listPendingTasks().then(
+ (Iterable value) => value.toList(
+ growable: false,
+ ),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('Pending Background Tasks'),
+ actions: [
+ PopupMenuButton(
+ onSelected: (_) async {
+ await _cancelAllTask();
+ },
+ itemBuilder: (BuildContext context) => >[
+ const PopupMenuItem(
+ value: POPUP_MENU_FIRST_ITEM, child: Text('Cancel all')),
+ ],
+ ),
+ ],
+ leading: BackButton(
+ onPressed: () => Navigator.pop(context),
+ ),
+ ),
+ body: RefreshIndicator(
+ onRefresh: () async {
+ setState(() {});
+ },
+ child: Center(
+ child: FutureBuilder>(
+ future: _fetchListItems(),
+ builder:
+ (BuildContext context, AsyncSnapshot> snapshot) {
+ if (snapshot.connectionState == ConnectionState.waiting) {
+ return const Center(
+ child: CircularProgressIndicator.adaptive(),
+ );
+ }
+ if (snapshot.data!.isEmpty) {
+ return const EmptyScreen();
+ } else {
+ return ListView.builder(
+ itemCount: snapshot.data!.length,
+ itemBuilder: (BuildContext context, int index) {
+ if (snapshot.data!.isEmpty) {
+ return const Center(
+ child: Text(
+ 'No data',
+ style: TextStyle(color: Colors.white, fontSize: 30),
+ ),
+ );
+ }
+ return TaskListTile(
+ index,
+ snapshot.data![index].uniqueId,
+ snapshot.data![index].data!['processName'].toString(),
+ snapshot.data![index].data!['barcode'].toString(),
+ );
+ },
+ );
+ }
+ },
+ ),
+ ),
+ ),
+ );
+ }
+
+ Future _cancelAllTask() async {
+ String status = 'All tasks Cancelled';
+ try {
+ await TaskManager().cancelTasks();
+ } catch (e) {
+ status = 'Something went wrong';
+ }
+ setState(() {});
+ final SnackBar snackBar = SnackBar(
+ content: Text(
+ status,
+ ),
+ duration: SnackBarDuration.medium,
+ );
+ setState(() {});
+ if (!mounted) {
+ return;
+ }
+ ScaffoldMessenger.of(context).showSnackBar(snackBar);
+ }
+}
+
+class TaskListTile extends StatefulWidget {
+ const TaskListTile(
+ this.index,
+ this.uniqueId,
+ this.processName,
+ this.barcode,
+ ) : assert(index >= 0),
+ assert(uniqueId.length > 0),
+ assert(barcode.length > 0),
+ assert(processName.length > 0);
+
+ final int index;
+ final String uniqueId;
+ final String processName;
+ final String barcode;
+
+ @override
+ State createState() => _TaskListTileState();
+}
+
+class _TaskListTileState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return ListTile(
+ leading: Text((widget.index + 1).toString()),
+ title: Text(widget.barcode),
+ subtitle: Text(widget.processName),
+ trailing: Wrap(
+ children: [
+ IconButton(
+ onPressed: () {
+ String status = 'Retrying';
+ try {
+ TaskManager().runTask(widget.uniqueId);
+ } catch (e) {
+ status = 'Error: $e';
+ }
+ final SnackBar snackBar = SnackBar(
+ content: Text(status),
+ duration: SnackBarDuration.medium,
+ );
+ if (!mounted) {
+ return;
+ }
+ ScaffoldMessenger.of(context).showSnackBar(snackBar);
+ setState(() {});
+ },
+ icon: const Icon(Icons.refresh)),
+ IconButton(
+ onPressed: () async {
+ await _cancelTask(widget.uniqueId);
+
+ setState(() {});
+ },
+ icon: const Icon(Icons.cancel))
+ ],
+ ),
+ );
+ }
+
+ Future _cancelTask(String uniqueId) async {
+ try {
+ await TaskManager().cancelTask(uniqueId);
+ const SnackBar snackBar = SnackBar(
+ content: Text('Cancelled'),
+ duration: SnackBarDuration.medium,
+ );
+ if (!mounted) {
+ return;
+ }
+ ScaffoldMessenger.of(context).showSnackBar(snackBar);
+ } catch (e) {
+ final SnackBar snackBar = SnackBar(
+ content: Text('Error: $e'),
+ duration: SnackBarDuration.medium,
+ );
+ if (!mounted) {
+ return;
+ }
+ ScaffoldMessenger.of(context).showSnackBar(snackBar);
+ }
+ }
+}
+
+class EmptyScreen extends StatelessWidget {
+ const EmptyScreen({Key? key}) : super(key: key);
+ @override
+ Widget build(BuildContext context) {
+ return const Center(child: Text('No Pending Tasks'));
+ }
+}
diff --git a/packages/smooth_app/lib/pages/personalized_ranking_page.dart b/packages/smooth_app/lib/pages/personalized_ranking_page.dart
index 9577a450d9..b25ac86713 100644
--- a/packages/smooth_app/lib/pages/personalized_ranking_page.dart
+++ b/packages/smooth_app/lib/pages/personalized_ranking_page.dart
@@ -10,6 +10,7 @@ import 'package:smooth_app/data_models/up_to_date_product_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/design_constants.dart';
+import 'package:smooth_app/generic_lib/duration_constants.dart';
import 'package:smooth_app/helpers/analytics_helper.dart';
import 'package:smooth_app/helpers/product_compatibility_helper.dart';
import 'package:smooth_app/pages/product/common/product_list_item_simple.dart';
@@ -191,7 +192,7 @@ class _PersonalizedRankingPageState extends State
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(appLocalizations.product_removed_comparison),
- duration: const Duration(seconds: 3),
+ duration: SnackBarDuration.medium,
),
);
},
diff --git a/packages/smooth_app/lib/pages/preferences/user_preferences_dev_mode.dart b/packages/smooth_app/lib/pages/preferences/user_preferences_dev_mode.dart
index cac11f9088..254a0ce725 100644
--- a/packages/smooth_app/lib/pages/preferences/user_preferences_dev_mode.dart
+++ b/packages/smooth_app/lib/pages/preferences/user_preferences_dev_mode.dart
@@ -13,6 +13,7 @@ import 'package:smooth_app/generic_lib/dialogs/smooth_alert_dialog.dart';
import 'package:smooth_app/generic_lib/loading_dialog.dart';
import 'package:smooth_app/helpers/data_importer/product_list_import_export.dart';
import 'package:smooth_app/helpers/data_importer/smooth_app_data_importer.dart';
+import 'package:smooth_app/pages/offline_tasks_page.dart';
import 'package:smooth_app/pages/onboarding/onboarding_flow_navigator.dart';
import 'package:smooth_app/pages/preferences/abstract_user_preferences.dart';
import 'package:smooth_app/pages/preferences/user_preferences_dialog_editor.dart';
@@ -269,6 +270,17 @@ class UserPreferencesDevMode extends AbstractUserPreferences {
},
),
_dataImporterTile(),
+ ListTile(
+ title: const Text('Pending Tasks'),
+ onTap: () {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (BuildContext context) => const OfflineTaskPage(),
+ ),
+ );
+ },
+ ),
ListTile(
title: Text(
appLocalizations.dev_preferences_import_history_title,
diff --git a/packages/smooth_app/lib/pages/product/add_basic_details_page.dart b/packages/smooth_app/lib/pages/product/add_basic_details_page.dart
index 5af93b40a7..fe9295e9f2 100644
--- a/packages/smooth_app/lib/pages/product/add_basic_details_page.dart
+++ b/packages/smooth_app/lib/pages/product/add_basic_details_page.dart
@@ -1,14 +1,20 @@
+import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
+import 'package:openfoodfacts/utils/CountryHelper.dart';
import 'package:provider/provider.dart';
import 'package:smooth_app/cards/product_cards/product_image_carousel.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/duration_constants.dart';
import 'package:smooth_app/generic_lib/widgets/smooth_text_form_field.dart';
-import 'package:smooth_app/pages/product/common/product_refresher.dart';
+import 'package:smooth_app/helpers/background_task_helper.dart';
+import 'package:smooth_app/query/product_query.dart';
import 'package:smooth_app/widgets/smooth_scaffold.dart';
+import 'package:task_manager/task_manager.dart';
class AddBasicDetailsPage extends StatefulWidget {
const AddBasicDetailsPage(
@@ -129,31 +135,67 @@ class _AddBasicDetailsPageState extends State {
if (!_formKey.currentState!.validate()) {
return;
}
- final bool savedAndRefreshed =
- await ProductRefresher().saveAndRefresh(
- context: context,
- localDatabase: localDatabase,
- product: Product(
- productName: _productNameController.text,
- quantity: _weightController.text,
- brands: _brandNameController.text,
- barcode: _product.barcode,
- ),
- isLoggedInMandatory: widget.isLoggedInMandatory,
+ final Product inputProduct = Product(
+ productName: _productNameController.text,
+ quantity: _weightController.text,
+ brands: _brandNameController.text,
+ barcode: _product.barcode,
);
- if (!savedAndRefreshed) {
- return;
+ final String uniqueId = UniqueIdGenerator.generateUniqueId(
+ _product.barcode!, BASIC_DETAILS);
+ final BackgroundOtherDetailsInput
+ backgroundBasicDetailsInput =
+ BackgroundOtherDetailsInput(
+ processName: PRODUCT_EDIT_TASK,
+ uniqueId: uniqueId,
+ barcode: _product.barcode!,
+ inputMap: jsonEncode(inputProduct.toJson()),
+ languageCode: ProductQuery.getLanguage().code,
+ user: jsonEncode(ProductQuery.getUser().toJson()),
+ country: ProductQuery.getCountry()!.iso2Code,
+ );
+ await TaskManager().addTask(
+ Task(
+ data: backgroundBasicDetailsInput.toJson(),
+ uniqueId: uniqueId,
+ ),
+ );
+
+ final DaoProduct daoProduct = DaoProduct(localDatabase);
+ final Product? product = await daoProduct.get(
+ _product.barcode!,
+ );
+ // We go and chek in the local database if the product is
+ // already in the database. If it is, we update the fields of the product.
+ //And if it is not, we create a new product with the fields of the _product
+ // and we insert it in the database. (Giving the user an immediate feedback)
+ if (product == null) {
+ daoProduct.put(Product(
+ barcode: _product.barcode,
+ productName: _productNameController.text,
+ brands: _brandNameController.text,
+ quantity: _weightController.text,
+ lang: ProductQuery.getLanguage(),
+ ));
+ } else {
+ product.productName = _productNameController.text;
+ product.brands = _brandNameController.text;
+ product.quantity = _weightController.text;
+ daoProduct.put(product);
}
+ localDatabase.notifyListeners();
if (!mounted) {
return;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
- content:
- Text(appLocalizations.basic_details_add_success),
+ content: Text(
+ appLocalizations.basic_details_add_success,
+ ),
+ duration: SnackBarDuration.medium,
),
);
- Navigator.pop(context);
+ Navigator.pop(context, product);
},
),
),
diff --git a/packages/smooth_app/lib/pages/product/add_new_product_page.dart b/packages/smooth_app/lib/pages/product/add_new_product_page.dart
index 58989f8b0a..1b9b4d7bfb 100644
--- a/packages/smooth_app/lib/pages/product/add_new_product_page.dart
+++ b/packages/smooth_app/lib/pages/product/add_new_product_page.dart
@@ -4,6 +4,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:openfoodfacts/model/Product.dart';
import 'package:openfoodfacts/model/ProductImage.dart';
+import 'package:provider/provider.dart';
+import 'package:smooth_app/database/dao_product.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';
@@ -82,8 +85,22 @@ class _AddNewProductPageState extends State {
action: SmoothActionButton(
text: appLocalizations.finish,
onPressed: () async {
- await Navigator.maybePop(
- context, _isProductLoaded ? widget.barcode : null);
+ final LocalDatabase localDatabase =
+ context.read();
+ final DaoProduct daoProduct = DaoProduct(localDatabase);
+ final Product? product =
+ await daoProduct.get(widget.barcode);
+ if (product == null) {
+ final Product product = Product(
+ barcode: widget.barcode,
+ );
+ daoProduct.put(product);
+ localDatabase.notifyListeners();
+ }
+ if (mounted) {
+ await Navigator.maybePop(
+ context, _isProductLoaded ? widget.barcode : null);
+ }
},
),
),
@@ -159,7 +176,6 @@ class _AddNewProductPageState extends State {
_isProductLoaded = true;
});
}
- initialPhoto.delete();
},
),
);
@@ -167,16 +183,23 @@ class _AddNewProductPageState extends State {
Widget _buildImageUploadedRow(
BuildContext context, ImageField imageType, File image) {
+ final ThemeData themeData = Theme.of(context);
return Padding(
padding: _ROW_PADDING_TOP,
child: Row(
- mainAxisAlignment: MainAxisAlignment.start,
+ mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(height: 50, child: Image.file(image, fit: BoxFit.cover)),
Expanded(
- child: Center(
- child: Text(_getPhotoUploadedLabelText(context, imageType),
- style: Theme.of(context).textTheme.bodyText1))),
+ child: Center(
+ child: Text(_getAddPhotoButtonText(context, imageType),
+ style: themeData.textTheme.bodyText1),
+ ),
+ ),
+ Icon(
+ Icons.check_box,
+ color: themeData.bottomNavigationBarTheme.selectedItemColor,
+ )
],
),
);
@@ -198,23 +221,6 @@ class _AddNewProductPageState extends State {
}
}
- String _getPhotoUploadedLabelText(
- BuildContext context, ImageField imageType) {
- final AppLocalizations appLocalizations = AppLocalizations.of(context);
- switch (imageType) {
- case ImageField.FRONT:
- return appLocalizations.front_photo_uploaded;
- case ImageField.INGREDIENTS:
- return appLocalizations.ingredients_photo_uploaded;
- case ImageField.NUTRITION:
- return appLocalizations.nutritional_facts_photo_uploaded;
- case ImageField.PACKAGING:
- return appLocalizations.recycling_photo_uploaded;
- case ImageField.OTHER:
- return appLocalizations.other_photo_uploaded;
- }
- }
-
bool _isImageUploadedForType(ImageField imageType) {
return (_uploadedImages[imageType] ?? []).isNotEmpty;
}
@@ -222,27 +228,30 @@ class _AddNewProductPageState extends State {
Widget _buildNutritionInputButton() {
if (_nutritionFactsAdded) {
return Padding(
- padding: _ROW_PADDING_TOP,
- child: Row(
- mainAxisSize: MainAxisSize.max,
- mainAxisAlignment: MainAxisAlignment.start,
- children: [
- const SizedBox(
- width: 50.0,
- child: Icon(
- Icons.check,
- color: Colors.greenAccent,
- ),
+ padding: _ROW_PADDING_TOP,
+ child: Row(
+ mainAxisSize: MainAxisSize.max,
+ mainAxisAlignment: MainAxisAlignment.start,
+ children: [
+ SizedBox(
+ width: 50.0,
+ child: Icon(
+ Icons.check,
+ color: Theme.of(context)
+ .bottomNavigationBarTheme
+ .selectedItemColor,
),
- Expanded(
- child: Center(
- child: Text(
- AppLocalizations.of(context).nutritional_facts_added,
- style: Theme.of(context).textTheme.bodyText1),
- ),
+ ),
+ Expanded(
+ child: Center(
+ child: Text(
+ AppLocalizations.of(context).nutritional_facts_added,
+ style: Theme.of(context).textTheme.bodyText1),
),
- ],
- ));
+ ),
+ ],
+ ),
+ );
}
return Padding(
@@ -293,17 +302,18 @@ class _AddNewProductPageState extends State {
Widget _buildaddInputDetailsButton() {
if (_basicDetailsAdded) {
+ final ThemeData themeData = Theme.of(context);
return Padding(
padding: _ROW_PADDING_TOP,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
children: [
- const SizedBox(
+ SizedBox(
width: 50.0,
child: Icon(
Icons.check,
- color: Colors.greenAccent,
+ color: themeData.bottomNavigationBarTheme.selectedItemColor,
),
),
Expanded(
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 3edf28edec..8dfcba636d 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
@@ -12,6 +12,7 @@ import 'package:smooth_app/database/dao_product_list.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/duration_constants.dart';
import 'package:smooth_app/generic_lib/loading_dialog.dart';
import 'package:smooth_app/helpers/robotoff_insight_helper.dart';
import 'package:smooth_app/pages/inherited_data_manager.dart';
@@ -308,7 +309,7 @@ class _ProductListPageState extends State
? appLocalizations.product_removed_history
: appLocalizations.product_could_not_remove,
),
- duration: const Duration(seconds: 3),
+ duration: SnackBarDuration.medium,
),
);
// TODO(monsieurtanuki): add a snackbar ("put back the food")
@@ -349,7 +350,7 @@ class _ProductListPageState extends State
products.length,
),
),
- duration: const Duration(seconds: 2),
+ duration: SnackBarDuration.short,
),
);
setState(() {});
diff --git a/packages/smooth_app/lib/pages/product/common/product_query_page.dart b/packages/smooth_app/lib/pages/product/common/product_query_page.dart
index 373ca823dd..e599bbd21e 100644
--- a/packages/smooth_app/lib/pages/product/common/product_query_page.dart
+++ b/packages/smooth_app/lib/pages/product/common/product_query_page.dart
@@ -428,7 +428,7 @@ class _ProductQueryPageState extends State
void _scrollToTop() {
_scrollController.animateTo(
0,
- duration: const Duration(seconds: 3),
+ duration: SnackBarDuration.medium,
curve: Curves.linear,
);
}
diff --git a/packages/smooth_app/lib/pages/product/confirm_and_upload_picture.dart b/packages/smooth_app/lib/pages/product/confirm_and_upload_picture.dart
index 1acfbf71c6..3a9d2a0598 100644
--- a/packages/smooth_app/lib/pages/product/confirm_and_upload_picture.dart
+++ b/packages/smooth_app/lib/pages/product/confirm_and_upload_picture.dart
@@ -176,8 +176,7 @@ class _ConfirmAndUploadPictureState extends State {
),
),
onPressed: () async {
- final bool isPhotoUploaded =
- await uploadCapturedPicture(
+ uploadCapturedPicture(
context,
barcode: widget.barcode,
imageField: widget.imageType,
@@ -186,10 +185,9 @@ class _ConfirmAndUploadPictureState extends State {
if (!mounted) {
return;
}
- retakenPhoto?.delete();
Navigator.pop(
context,
- isPhotoUploaded ? photo : null,
+ photo,
);
},
label: Text(
diff --git a/packages/smooth_app/lib/pages/product/edit_ingredients_page.dart b/packages/smooth_app/lib/pages/product/edit_ingredients_page.dart
index c0327ecbe3..cf5c3fa046 100644
--- a/packages/smooth_app/lib/pages/product/edit_ingredients_page.dart
+++ b/packages/smooth_app/lib/pages/product/edit_ingredients_page.dart
@@ -1,20 +1,26 @@
+import 'dart:convert';
import 'dart:io';
import 'dart:ui' show ImageFilter;
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
+import 'package:openfoodfacts/utils/CountryHelper.dart';
import 'package:provider/provider.dart';
import 'package:smooth_app/data_models/up_to_date_product_provider.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/duration_constants.dart';
+import 'package:smooth_app/helpers/background_task_helper.dart';
import 'package:smooth_app/helpers/picture_capture_helper.dart';
import 'package:smooth_app/pages/image_crop_page.dart';
-import 'package:smooth_app/pages/product/common/product_refresher.dart';
import 'package:smooth_app/pages/product/explanation_widget.dart';
import 'package:smooth_app/pages/product/ocr_helper.dart';
+import 'package:smooth_app/query/product_query.dart';
import 'package:smooth_app/widgets/smooth_scaffold.dart';
+import 'package:task_manager/task_manager.dart';
/// Editing with OCR a product field and the corresponding image.
///
@@ -79,7 +85,7 @@ class _EditOcrPageState extends State {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(error),
- duration: const Duration(seconds: 3),
+ duration: SnackBarDuration.medium,
),
);
}
@@ -90,7 +96,6 @@ class _EditOcrPageState extends State {
// Returns a Future that resolves successfully only if everything succeeds,
// otherwise it will resolve with the relevant error.
Future _getImage(bool isNewImage) async {
- bool isUploaded = true;
if (isNewImage) {
final File? croppedImageFile =
await startImageCropping(context, showOptionDialog: true);
@@ -105,18 +110,12 @@ class _EditOcrPageState extends State {
if (!mounted) {
return;
}
- isUploaded = await uploadCapturedPicture(
+ await uploadCapturedPicture(
context,
barcode: _product.barcode!,
imageField: _helper.getImageField(),
imageUri: croppedImageFile.uri,
);
-
- croppedImageFile.delete();
- }
-
- if (!isUploaded) {
- throw Exception('Image could not be uploaded.');
}
final String? extractedText = await _helper.getExtractedText(_product);
@@ -131,12 +130,56 @@ class _EditOcrPageState extends State {
}
Future _updateText(final String text) async {
- final LocalDatabase localDatabase = context.read();
- return ProductRefresher().saveAndRefresh(
- context: context,
- localDatabase: localDatabase,
- product: _helper.getMinimalistProduct(_product, text),
+ final Product minimalistProduct =
+ _helper.getMinimalistProduct(_product, text);
+ final String uniqueId =
+ UniqueIdGenerator.generateUniqueId(_product.barcode!, INGREDIENT_EDIT);
+ final BackgroundOtherDetailsInput backgroundOtherDetailsInput =
+ BackgroundOtherDetailsInput(
+ processName: PRODUCT_EDIT_TASK,
+ uniqueId: uniqueId,
+ barcode: minimalistProduct.barcode!,
+ languageCode: ProductQuery.getLanguage().code,
+ inputMap: jsonEncode(minimalistProduct.toJson()),
+ user: jsonEncode(ProductQuery.getUser().toJson()),
+ country: ProductQuery.getCountry()!.iso2Code,
);
+ await TaskManager().addTask(
+ Task(
+ data: backgroundOtherDetailsInput.toJson(),
+ uniqueId: uniqueId,
+ ),
+ );
+
+ // ignore: use_build_context_synchronously
+ final LocalDatabase localDatabase = context.read();
+ final DaoProduct daoProduct = DaoProduct(localDatabase);
+ final Product? localProduct =
+ await daoProduct.get(minimalistProduct.barcode!);
+ // We go and chek in the local database if the product is
+ // already in the database. If it is, we update the fields of the product.
+ //And if it is not, we create a new product with the fields of the minimalistProduct
+ // and we insert it in the database. (Giving the user an immediate feedback)
+ if (localProduct != null) {
+ localProduct.ingredientsText = minimalistProduct.ingredientsText;
+ await daoProduct.put(localProduct);
+ } else {
+ await daoProduct.put(minimalistProduct);
+ }
+
+ localDatabase.notifyListeners();
+ if (!mounted) {
+ return false;
+ }
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(
+ AppLocalizations.of(context).product_task_background_schedule,
+ ),
+ duration: SnackBarDuration.medium,
+ ),
+ );
+ return true;
}
@override
diff --git a/packages/smooth_app/lib/pages/product/edit_product_page.dart b/packages/smooth_app/lib/pages/product/edit_product_page.dart
index 2d96eb45fe..eabf60a515 100644
--- a/packages/smooth_app/lib/pages/product/edit_product_page.dart
+++ b/packages/smooth_app/lib/pages/product/edit_product_page.dart
@@ -10,6 +10,7 @@ import 'package:smooth_app/data_models/product_image_data.dart';
import 'package:smooth_app/data_models/up_to_date_product_provider.dart';
import 'package:smooth_app/database/local_database.dart';
import 'package:smooth_app/generic_lib/design_constants.dart';
+import 'package:smooth_app/generic_lib/duration_constants.dart';
import 'package:smooth_app/generic_lib/widgets/smooth_card.dart';
import 'package:smooth_app/helpers/product_cards_helper.dart';
import 'package:smooth_app/pages/product/add_basic_details_page.dart';
@@ -328,7 +329,7 @@ class _EditProductPageState extends State {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(appLocalizations.product_refreshed),
- duration: const Duration(seconds: 2),
+ duration: SnackBarDuration.short,
),
);
}
diff --git a/packages/smooth_app/lib/pages/product/new_product_page.dart b/packages/smooth_app/lib/pages/product/new_product_page.dart
index 79f8ed6465..efcd63c9b0 100644
--- a/packages/smooth_app/lib/pages/product/new_product_page.dart
+++ b/packages/smooth_app/lib/pages/product/new_product_page.dart
@@ -169,7 +169,7 @@ class _ProductPageState extends State with TraceableClientMixin {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(appLocalizations.product_refreshed),
- duration: const Duration(seconds: 2),
+ duration: SnackBarDuration.short,
),
);
}
diff --git a/packages/smooth_app/lib/pages/product/nutrition_page_loaded.dart b/packages/smooth_app/lib/pages/product/nutrition_page_loaded.dart
index b4c158dc21..3e6051f853 100644
--- a/packages/smooth_app/lib/pages/product/nutrition_page_loaded.dart
+++ b/packages/smooth_app/lib/pages/product/nutrition_page_loaded.dart
@@ -1,3 +1,5 @@
+import 'dart:convert';
+
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -6,17 +8,20 @@ import 'package:intl/intl.dart';
import 'package:openfoodfacts/model/OrderedNutrient.dart';
import 'package:openfoodfacts/model/OrderedNutrients.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
+import 'package:openfoodfacts/utils/CountryHelper.dart';
import 'package:openfoodfacts/utils/UnitHelper.dart';
import 'package:provider/provider.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/widgets/smooth_card.dart';
+import 'package:smooth_app/helpers/background_task_helper.dart';
import 'package:smooth_app/helpers/text_input_formatters_helper.dart';
-import 'package:smooth_app/pages/product/common/product_refresher.dart';
import 'package:smooth_app/pages/product/nutrition_container.dart';
import 'package:smooth_app/query/product_query.dart';
import 'package:smooth_app/widgets/smooth_scaffold.dart';
+import 'package:task_manager/task_manager.dart';
/// Actual nutrition page, with data already loaded.
class NutritionPageLoaded extends StatefulWidget {
@@ -526,11 +531,45 @@ class _NutritionPageLoadedState extends State {
return false;
}
// if it fails, we stay on the same page
- return ProductRefresher().saveAndRefresh(
- context: context,
- localDatabase: localDatabase,
- product: changedProduct,
- isLoggedInMandatory: widget.isLoggedInMandatory,
+ final String uniqueId =
+ UniqueIdGenerator.generateUniqueId(_product.barcode!, NUTRITION_EDIT);
+ final BackgroundOtherDetailsInput nutritonInputData =
+ BackgroundOtherDetailsInput(
+ processName: PRODUCT_EDIT_TASK,
+ uniqueId: uniqueId,
+ barcode: _product.barcode!,
+ languageCode: ProductQuery.getLanguage().code,
+ inputMap: jsonEncode(changedProduct.toJson()),
+ user: jsonEncode(ProductQuery.getUser().toJson()),
+ country: ProductQuery.getCountry()!.iso2Code,
);
+ await TaskManager().addTask(
+ Task(
+ data: nutritonInputData.toJson(),
+ uniqueId: uniqueId,
+ ),
+ );
+ final DaoProduct daoProduct = DaoProduct(localDatabase);
+ final Product? product = await daoProduct.get(
+ _product.barcode!,
+ );
+ // We go and chek in the local database if the product is
+ // already in the database. If it is, we update the fields of the product.
+ //And if it is not, we create a new product with the fields of the _product
+ // and we insert it in the database. (Giving the user an immediate feedback)
+ if (product != null) {
+ product.servingSize = changedProduct.servingSize;
+ product.nutriments = changedProduct.nutriments;
+ await daoProduct.put(product);
+ }
+ localDatabase.notifyListeners();
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(appLocalizations.product_task_background_schedule),
+ ),
+ );
+ }
+ return true;
}
}
diff --git a/packages/smooth_app/lib/pages/product/product_image_gallery_view.dart b/packages/smooth_app/lib/pages/product/product_image_gallery_view.dart
index cbddded424..ba62cdbac0 100644
--- a/packages/smooth_app/lib/pages/product/product_image_gallery_view.dart
+++ b/packages/smooth_app/lib/pages/product/product_image_gallery_view.dart
@@ -10,6 +10,7 @@ import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';
import 'package:smooth_app/data_models/product_image_data.dart';
import 'package:smooth_app/generic_lib/design_constants.dart';
+import 'package:smooth_app/generic_lib/duration_constants.dart';
import 'package:smooth_app/generic_lib/loading_dialog.dart';
import 'package:smooth_app/generic_lib/widgets/smooth_gauge.dart';
import 'package:smooth_app/helpers/picture_capture_helper.dart';
@@ -42,7 +43,6 @@ class _ProductImageGalleryViewState extends State {
final List allProductImageProviders = [];
late String title;
bool _hasPhoto = true;
- bool _isRefreshed = false;
late ProductImageData _productImageDataCurrent;
int _currentIndex = 0;
@@ -98,7 +98,7 @@ class _ProductImageGalleryViewState extends State {
leading: IconButton(
icon: Icon(ConstantIcons.instance.getBackIcon()),
onPressed: () {
- Navigator.maybePop(context, _isRefreshed);
+ Navigator.maybePop(context);
},
)),
backgroundColor: Colors.black,
@@ -139,30 +139,14 @@ class _ProductImageGalleryViewState extends State {
if (!mounted) {
return;
}
- final bool isUploaded =
- await uploadCapturedPicture(
+ await uploadCapturedPicture(
context,
barcode: widget.barcode!,
imageField: _productImageDataCurrent.imageField,
imageUri: croppedImageFile.uri,
);
-
- if (isUploaded) {
- _isRefreshed = true;
- if (!mounted) {
- return;
- }
- final AppLocalizations appLocalizations =
- AppLocalizations.of(context);
- final String message = getImageUploadedMessage(
- _productImageDataCurrent.imageField,
- appLocalizations);
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text(message),
- duration: const Duration(seconds: 3),
- ),
- );
+ if (!mounted) {
+ return;
}
}
}
@@ -275,29 +259,28 @@ class _ProductImageGalleryViewState extends State {
if (!mounted) {
return;
}
- final bool isUploaded = await uploadCapturedPicture(
+ await uploadCapturedPicture(
context,
barcode: widget.barcode!,
imageField: _productImageDataCurrent.imageField,
imageUri: croppedImageFile.uri,
);
- if (isUploaded) {
- _isRefreshed = true;
- if (!mounted) {
- return;
- }
- final AppLocalizations appLocalizations =
- AppLocalizations.of(context);
- final String message = getImageUploadedMessage(
- _productImageDataCurrent.imageField, appLocalizations);
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text(message),
- duration: const Duration(seconds: 3),
- ),
- );
+ if (!mounted) {
+ return;
}
+ final AppLocalizations appLocalizations =
+ AppLocalizations.of(context);
+ final String message = appLocalizations.image_upload_queued;
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(message),
+ duration: SnackBarDuration.medium,
+ ),
+ );
+ Navigator.pop(
+ context,
+ );
}
}
},
@@ -344,7 +327,6 @@ class _ProductImageGalleryViewState extends State {
),
);
if (photoUploaded != null) {
- _isRefreshed = true;
if (!mounted) {
return;
}
diff --git a/packages/smooth_app/lib/pages/product/simple_input_page.dart b/packages/smooth_app/lib/pages/product/simple_input_page.dart
index cabb5f4726..521e2b2886 100644
--- a/packages/smooth_app/lib/pages/product/simple_input_page.dart
+++ b/packages/smooth_app/lib/pages/product/simple_input_page.dart
@@ -1,18 +1,23 @@
+import 'dart:convert';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
+import 'package:openfoodfacts/utils/CountryHelper.dart';
import 'package:provider/provider.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/duration_constants.dart';
import 'package:smooth_app/generic_lib/widgets/smooth_card.dart';
+import 'package:smooth_app/helpers/background_task_helper.dart';
import 'package:smooth_app/helpers/collections_helper.dart';
import 'package:smooth_app/helpers/product_cards_helper.dart';
-import 'package:smooth_app/pages/product/common/product_refresher.dart';
import 'package:smooth_app/pages/product/simple_input_page_helpers.dart';
import 'package:smooth_app/pages/product/simple_input_widget.dart';
+import 'package:smooth_app/query/product_query.dart';
import 'package:smooth_app/widgets/smooth_scaffold.dart';
+import 'package:task_manager/task_manager.dart';
/// Simple input page: we have a list of terms, we add, we remove, we save.
class SimpleInputPage extends StatefulWidget {
@@ -141,6 +146,7 @@ class _SimpleInputPageState extends State {
final Product changedProduct = Product(barcode: widget.product.barcode);
bool changed = false;
bool added = false;
+ String pageName = '';
for (int i = 0; i < widget.helpers.length; i++) {
if (widget.helpers[i].addItemsFromController(_controllers[i])) {
added = true;
@@ -148,6 +154,7 @@ class _SimpleInputPageState extends State {
if (widget.helpers[i].getChangedProduct(changedProduct)) {
changed = true;
}
+ pageName = widget.helpers[i].getTitle(AppLocalizations.of(context));
}
if (added) {
setState(() {});
@@ -183,12 +190,37 @@ class _SimpleInputPageState extends State {
return true;
}
}
- // if it fails, we stay on the same page
- return ProductRefresher().saveAndRefresh(
- context: context,
- localDatabase: localDatabase,
- product: changedProduct,
+ final String uniqueId =
+ UniqueIdGenerator.generateUniqueId(changedProduct.barcode!, pageName);
+ final BackgroundOtherDetailsInput backgroundOtherDetailsInput =
+ BackgroundOtherDetailsInput(
+ processName: PRODUCT_EDIT_TASK,
+ uniqueId: uniqueId,
+ barcode: changedProduct.barcode!,
+ languageCode: ProductQuery.getLanguage().code,
+ inputMap: jsonEncode(changedProduct.toJson()),
+ user: jsonEncode(ProductQuery.getUser().toJson()),
+ country: ProductQuery.getCountry()!.iso2Code,
);
+ await TaskManager().addTask(
+ Task(
+ data: backgroundOtherDetailsInput.toJson(),
+ uniqueId: uniqueId,
+ ),
+ );
+ localDatabase.notifyListeners();
+ if (!mounted) {
+ return false;
+ }
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(
+ appLocalizations.product_task_background_schedule,
+ ),
+ duration: SnackBarDuration.medium,
+ ),
+ );
+ return true;
}
@override
diff --git a/packages/smooth_app/pubspec.lock b/packages/smooth_app/pubspec.lock
index 78744e884d..7d4cd3b837 100644
--- a/packages/smooth_app/pubspec.lock
+++ b/packages/smooth_app/pubspec.lock
@@ -1388,4 +1388,4 @@ packages:
version: "3.1.1"
sdks:
dart: ">=2.17.0 <3.0.0"
- flutter: ">=3.0.0"
+ flutter: ">=3.0.0"
\ No newline at end of file
diff --git a/packages/smooth_app/pubspec.yaml b/packages/smooth_app/pubspec.yaml
index b8091dc273..684383faa8 100644
--- a/packages/smooth_app/pubspec.yaml
+++ b/packages/smooth_app/pubspec.yaml
@@ -47,15 +47,18 @@ dependencies:
# Camera (custom implementation for Android)
camera:
git:
- url: 'https://github.com/g123k/plugins.git'
- ref: 'smooth_camera'
- path: 'packages/camera/camera'
+ url: "https://github.com/g123k/plugins.git"
+ ref: "smooth_camera"
+ path: "packages/camera/camera"
camera_platform_interface:
git:
- url: 'https://github.com/g123k/plugins.git'
- ref: 'smooth_camera'
- path: 'packages/camera/camera_platform_interface'
-
+ url: "https://github.com/g123k/plugins.git"
+ ref: "smooth_camera"
+ path: "packages/camera/camera_platform_interface"
+ task_manager:
+ git:
+ url: "https://github.com/g123k/flutter_task_manager.git"
+
audioplayers: ^1.0.0
percent_indicator: ^4.2.2
flutter_email_sender: ^5.1.0
@@ -122,8 +125,6 @@ flutter_native_splash:
branding: assets/app/splashscreen_branding.png
branding_dark: assets/app/splashscreen_branding_dark.png
-
-
# https://developer.android.com/guide/topics/ui/splash-screen
android_12:
# Please note that the splash screen will be clipped to a circle on the center of the screen.