mirror of
https://github.com/openfoodfacts/smooth-app.git
synced 2025-08-26 11:16:45 +08:00
fix: Background image upload (#2433)
* work manager used for bg schedule * removed swith case * remove unwanted contrains and randomize the taskid as image.others might fail * added translations * new product image upload working * removed debug for work manager * delete photo when uploaded * use await keywords while initializing * refactor the code * fix show image after queed * serialize and deserialze input data seperately * add an instant product in the loacl db to let the user not confuse * added smooth random class * use smooth random to generate 8 digits * remove unneccesary await * update the local db with a product if not yet uploaded * update local databse after sucessful update into server * don't refresh automatically after upload queed * supply language code to work manager * import app localizations * small refactoring * refactor the code for work manager * iOS Config for WorkManager * formatting * suggestion by tolemon Co-authored-by: Pierre Slamich <pierre.slamich@gmail.com> * suggestion by tolemon Co-authored-by: Pierre Slamich <pierre.slamich@gmail.com> * suggestion by tolemon Co-authored-by: Pierre Slamich <pierre.slamich@gmail.com> * small fix * add swith case to the workmanager * efficient switch case * Update packages/smooth_app/lib/l10n/app_en.arb Suggested by tolemon Co-authored-by: Pierre Slamich <pierre.slamich@gmail.com> * minor fix * Basic Input details add are now run in background * basic details are now updated immediatly even without internet * nutrient edit task are now background and also conflict when recheduling task is managed * menu screen for background tasks and nutrition edits are now backgrounded * button for retry now works in the bg tasks screen * formatting * genaralised function for non image tasks * should retry set default to false * better mechanism for retries and refactor * refactor and consts duration of 3s for snackbars * now language country and user are properly dropped * Update packages/smooth_app/ios/Runner/Info.plist * Update packages/smooth_app/ios/Runner/AppDelegate.swift * Error messages fixed * custom workmanager dependencies added * migrated basic detail task from work manager to taskmanager * chanages to do the product edit changes in bg * dart format * cleaned the function calls when doing bg task * dao_task edited * unnecessary methods removed * use int as id instead of string * added comments and made fields final * used const variable instead of hardcoded ones * get pending tasks from taskManager class * refactor * handled exceptions * string instead of int * remove unused plugin * removed unnessary files * update to localdatabase working without islotes * suggestions left as comments on PR * deleted unncessary Random class * doc added * Remove WorkManager iOS code * reset publock * reset publock * remove harcoded color * used duration constant class instead of harcoded * make methods to generate taskId * dart frmt * changes as per the code review * string buffer , var documentation and rename * error fix * refactor and spelling corrections * /// instead of // * dart ff formater * more refactorings * Update background_task_helper.dart * dart ff fix Co-authored-by: monsieurtanuki <fabrice_fontaine@hotmail.com> Co-authored-by: Edouard Marquez <g123k@users.noreply.github.com> Co-authored-by: Pierre Slamich <pierre.slamich@gmail.com> Co-authored-by: Pierre Slamich <pierre@openfoodfacts.org>
This commit is contained in:
@ -1,7 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
@ -22,12 +24,26 @@
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>https</string>
|
||||
<string>http</string>
|
||||
<string>mailto</string>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app needs Camera Usage for scanning barcodes and cropping photos</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>This app needs to access photo library for product photos uploads</string>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiresFullScreen</key>
|
||||
<true/>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
@ -39,21 +55,5 @@
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>This app needs to access photo library for product photos uploads</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app needs Camera Usage for scanning barcodes and cropping photos</string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>https</string>
|
||||
<string>http</string>
|
||||
<string>mailto</string>
|
||||
</array>
|
||||
<key>UIRequiresFullScreen</key>
|
||||
<true/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -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<ImageUploadCard> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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<Product> {
|
||||
language: ProductQuery.getLanguage(),
|
||||
country: ProductQuery.getCountry(),
|
||||
),
|
||||
).timeout(const Duration(seconds: 5));
|
||||
).timeout(SnackBarDuration.long);
|
||||
|
||||
@override
|
||||
String getAssetPath() => assetPath;
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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);
|
||||
}
|
||||
|
249
packages/smooth_app/lib/helpers/background_task_helper.dart
Normal file
249
packages/smooth_app/lib/helpers/background_task_helper.dart
Normal file
@ -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<TaskResult> 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<TaskResult> otherDetails(
|
||||
Map<String, dynamic> inputData,
|
||||
LocalDatabase localDatabase,
|
||||
) async {
|
||||
final BackgroundOtherDetailsInput inputTask =
|
||||
BackgroundOtherDetailsInput.fromJson(inputData);
|
||||
final Map<String, dynamic> productMap =
|
||||
json.decode(inputTask.inputMap) as Map<String, dynamic>;
|
||||
final User user =
|
||||
User.fromJson(jsonDecode(inputTask.user) as Map<String, dynamic>);
|
||||
|
||||
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<TaskResult> uploadImage(
|
||||
Map<String, dynamic> inputData,
|
||||
LocalDatabase localDatabase,
|
||||
) async {
|
||||
final BackgroundImageInputData inputTask =
|
||||
BackgroundImageInputData.fromJson(inputData);
|
||||
final User user =
|
||||
User.fromJson(jsonDecode(inputTask.user) as Map<String, dynamic>);
|
||||
|
||||
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<String, dynamic> 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<String, dynamic> toJson() => <String, dynamic>{
|
||||
'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<String, dynamic> 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<String, dynamic> toJson() => <String, dynamic>{
|
||||
'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();
|
||||
}
|
||||
}
|
@ -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<bool> uploadCapturedPicture(
|
||||
BuildContext context, {
|
||||
@ -13,32 +19,68 @@ Future<bool> uploadCapturedPicture(
|
||||
required Uri imageUri,
|
||||
}) async {
|
||||
final AppLocalizations appLocalizations = AppLocalizations.of(context);
|
||||
final SendImage image = SendImage(
|
||||
lang: ProductQuery.getLanguage(),
|
||||
final LocalDatabase localDatabase = context.read<LocalDatabase>();
|
||||
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<Status>(
|
||||
context: context,
|
||||
future: OpenFoodAPIClient.addProductImage(
|
||||
ProductQuery.getUser(),
|
||||
image,
|
||||
await TaskManager().addTask(
|
||||
Task(
|
||||
data: backgroundImageInputData.toJson(),
|
||||
uniqueId: uniqueId,
|
||||
),
|
||||
title: appLocalizations.uploading_image,
|
||||
);
|
||||
if (result == null || result.error != null || result.status != 'status ok') {
|
||||
await LoadingDialog.error(
|
||||
context: context,
|
||||
title: appLocalizations.error_occurred,
|
||||
|
||||
localDatabase.notifyListeners();
|
||||
// ignore: use_build_context_synchronously
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
appLocalizations.image_upload_queued,
|
||||
),
|
||||
duration: SnackBarDuration.medium,
|
||||
),
|
||||
);
|
||||
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<void> _updateContinuousScanModel(
|
||||
BuildContext context, String barcode) async {
|
||||
final ContinuousScanModel model = context.read<ContinuousScanModel>();
|
||||
|
@ -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<KnowledgePanelPage>
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(appLocalizations.product_refreshed),
|
||||
duration: const Duration(seconds: 2),
|
||||
duration: SnackBarDuration.short,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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<void> 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<bool> _init1() async {
|
||||
),
|
||||
daoString: DaoString(_localDatabase),
|
||||
);
|
||||
await callbackDispatcher(_localDatabase);
|
||||
|
||||
AnalyticsHelper.setCrashReports(_userPreferences.crashReports);
|
||||
ProductQuery.setCountry(_userPreferences.userCountryCode);
|
||||
|
202
packages/smooth_app/lib/pages/offline_tasks_page.dart
Normal file
202
packages/smooth_app/lib/pages/offline_tasks_page.dart
Normal file
@ -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<OfflineTaskPage> createState() => _OfflineTaskState();
|
||||
}
|
||||
|
||||
class _OfflineTaskState extends State<OfflineTaskPage> {
|
||||
Future<List<Task>> _fetchListItems() async {
|
||||
return TaskManager().listPendingTasks().then(
|
||||
(Iterable<Task> value) => value.toList(
|
||||
growable: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Pending Background Tasks'),
|
||||
actions: <Widget>[
|
||||
PopupMenuButton<int>(
|
||||
onSelected: (_) async {
|
||||
await _cancelAllTask();
|
||||
},
|
||||
itemBuilder: (BuildContext context) => <PopupMenuItem<int>>[
|
||||
const PopupMenuItem<int>(
|
||||
value: POPUP_MENU_FIRST_ITEM, child: Text('Cancel all')),
|
||||
],
|
||||
),
|
||||
],
|
||||
leading: BackButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
setState(() {});
|
||||
},
|
||||
child: Center(
|
||||
child: FutureBuilder<List<Task>>(
|
||||
future: _fetchListItems(),
|
||||
builder:
|
||||
(BuildContext context, AsyncSnapshot<List<Task>> 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<void> _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<TaskListTile> createState() => _TaskListTileState();
|
||||
}
|
||||
|
||||
class _TaskListTileState extends State<TaskListTile> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: Text((widget.index + 1).toString()),
|
||||
title: Text(widget.barcode),
|
||||
subtitle: Text(widget.processName),
|
||||
trailing: Wrap(
|
||||
children: <Widget>[
|
||||
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<void> _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'));
|
||||
}
|
||||
}
|
@ -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<PersonalizedRankingPage>
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(appLocalizations.product_removed_comparison),
|
||||
duration: const Duration(seconds: 3),
|
||||
duration: SnackBarDuration.medium,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -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<void>(
|
||||
context,
|
||||
MaterialPageRoute<void>(
|
||||
builder: (BuildContext context) => const OfflineTaskPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(
|
||||
appLocalizations.dev_preferences_import_history_title,
|
||||
|
@ -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<AddBasicDetailsPage> {
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
final bool savedAndRefreshed =
|
||||
await ProductRefresher().saveAndRefresh(
|
||||
context: context,
|
||||
localDatabase: localDatabase,
|
||||
product: Product(
|
||||
final Product inputProduct = Product(
|
||||
productName: _productNameController.text,
|
||||
quantity: _weightController.text,
|
||||
brands: _brandNameController.text,
|
||||
barcode: _product.barcode,
|
||||
),
|
||||
isLoggedInMandatory: widget.isLoggedInMandatory,
|
||||
);
|
||||
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);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@ -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<AddNewProductPage> {
|
||||
action: SmoothActionButton(
|
||||
text: appLocalizations.finish,
|
||||
onPressed: () async {
|
||||
final LocalDatabase localDatabase =
|
||||
context.read<LocalDatabase>();
|
||||
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<AddNewProductPage> {
|
||||
_isProductLoaded = true;
|
||||
});
|
||||
}
|
||||
initialPhoto.delete();
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -167,16 +183,23 @@ class _AddNewProductPageState extends State<AddNewProductPage> {
|
||||
|
||||
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: <Widget>[
|
||||
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: Text(_getAddPhotoButtonText(context, imageType),
|
||||
style: themeData.textTheme.bodyText1),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.check_box,
|
||||
color: themeData.bottomNavigationBarTheme.selectedItemColor,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -198,23 +221,6 @@ class _AddNewProductPageState extends State<AddNewProductPage> {
|
||||
}
|
||||
}
|
||||
|
||||
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] ?? <File>[]).isNotEmpty;
|
||||
}
|
||||
@ -227,11 +233,13 @@ class _AddNewProductPageState extends State<AddNewProductPage> {
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const SizedBox(
|
||||
SizedBox(
|
||||
width: 50.0,
|
||||
child: Icon(
|
||||
Icons.check,
|
||||
color: Colors.greenAccent,
|
||||
color: Theme.of(context)
|
||||
.bottomNavigationBarTheme
|
||||
.selectedItemColor,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
@ -242,7 +250,8 @@ class _AddNewProductPageState extends State<AddNewProductPage> {
|
||||
),
|
||||
),
|
||||
],
|
||||
));
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
@ -293,17 +302,18 @@ class _AddNewProductPageState extends State<AddNewProductPage> {
|
||||
|
||||
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: <Widget>[
|
||||
const SizedBox(
|
||||
SizedBox(
|
||||
width: 50.0,
|
||||
child: Icon(
|
||||
Icons.check,
|
||||
color: Colors.greenAccent,
|
||||
color: themeData.bottomNavigationBarTheme.selectedItemColor,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
|
@ -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<ProductListPage>
|
||||
? 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<ProductListPage>
|
||||
products.length,
|
||||
),
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
duration: SnackBarDuration.short,
|
||||
),
|
||||
);
|
||||
setState(() {});
|
||||
|
@ -428,7 +428,7 @@ class _ProductQueryPageState extends State<ProductQueryPage>
|
||||
void _scrollToTop() {
|
||||
_scrollController.animateTo(
|
||||
0,
|
||||
duration: const Duration(seconds: 3),
|
||||
duration: SnackBarDuration.medium,
|
||||
curve: Curves.linear,
|
||||
);
|
||||
}
|
||||
|
@ -176,8 +176,7 @@ class _ConfirmAndUploadPictureState extends State<ConfirmAndUploadPicture> {
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
final bool isPhotoUploaded =
|
||||
await uploadCapturedPicture(
|
||||
uploadCapturedPicture(
|
||||
context,
|
||||
barcode: widget.barcode,
|
||||
imageField: widget.imageType,
|
||||
@ -186,10 +185,9 @@ class _ConfirmAndUploadPictureState extends State<ConfirmAndUploadPicture> {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
retakenPhoto?.delete();
|
||||
Navigator.pop(
|
||||
context,
|
||||
isPhotoUploaded ? photo : null,
|
||||
photo,
|
||||
);
|
||||
},
|
||||
label: Text(
|
||||
|
@ -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<EditOcrPage> {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(error),
|
||||
duration: const Duration(seconds: 3),
|
||||
duration: SnackBarDuration.medium,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -90,7 +96,6 @@ class _EditOcrPageState extends State<EditOcrPage> {
|
||||
// Returns a Future that resolves successfully only if everything succeeds,
|
||||
// otherwise it will resolve with the relevant error.
|
||||
Future<void> _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<EditOcrPage> {
|
||||
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<EditOcrPage> {
|
||||
}
|
||||
|
||||
Future<bool> _updateText(final String text) async {
|
||||
final LocalDatabase localDatabase = context.read<LocalDatabase>();
|
||||
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<LocalDatabase>();
|
||||
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
|
||||
|
@ -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<EditProductPage> {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(appLocalizations.product_refreshed),
|
||||
duration: const Duration(seconds: 2),
|
||||
duration: SnackBarDuration.short,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -169,7 +169,7 @@ class _ProductPageState extends State<ProductPage> with TraceableClientMixin {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(appLocalizations.product_refreshed),
|
||||
duration: const Duration(seconds: 2),
|
||||
duration: SnackBarDuration.short,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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<NutritionPageLoaded> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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<ProductImageGalleryView> {
|
||||
final List<ImageProvider?> allProductImageProviders = <ImageProvider?>[];
|
||||
late String title;
|
||||
bool _hasPhoto = true;
|
||||
bool _isRefreshed = false;
|
||||
late ProductImageData _productImageDataCurrent;
|
||||
int _currentIndex = 0;
|
||||
|
||||
@ -98,7 +98,7 @@ class _ProductImageGalleryViewState extends State<ProductImageGalleryView> {
|
||||
leading: IconButton(
|
||||
icon: Icon(ConstantIcons.instance.getBackIcon()),
|
||||
onPressed: () {
|
||||
Navigator.maybePop(context, _isRefreshed);
|
||||
Navigator.maybePop(context);
|
||||
},
|
||||
)),
|
||||
backgroundColor: Colors.black,
|
||||
@ -139,31 +139,15 @@ class _ProductImageGalleryViewState extends State<ProductImageGalleryView> {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
final bool isUploaded =
|
||||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -275,29 +259,28 @@ class _ProductImageGalleryViewState extends State<ProductImageGalleryView> {
|
||||
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);
|
||||
final String message = appLocalizations.image_upload_queued;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
duration: const Duration(seconds: 3),
|
||||
duration: SnackBarDuration.medium,
|
||||
),
|
||||
);
|
||||
}
|
||||
Navigator.pop(
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -344,7 +327,6 @@ class _ProductImageGalleryViewState extends State<ProductImageGalleryView> {
|
||||
),
|
||||
);
|
||||
if (photoUploaded != null) {
|
||||
_isRefreshed = true;
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
@ -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<SimpleInputPage> {
|
||||
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<SimpleInputPage> {
|
||||
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<SimpleInputPage> {
|
||||
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
|
||||
|
@ -47,14 +47,17 @@ 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
|
||||
@ -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.
|
||||
|
Reference in New Issue
Block a user