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:
Aman Raj
2022-09-01 15:45:09 +05:30
committed by GitHub
parent 0c7e236a38
commit cf4fa6aebe
28 changed files with 903 additions and 245 deletions

View File

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<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>

View File

@ -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);
}
}
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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,
),

View File

@ -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);
}

View 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();
}
}

View File

@ -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>();

View File

@ -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,
),
);
}

View File

@ -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"

View File

@ -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);

View 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'));
}
}

View File

@ -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,
),
);
},

View File

@ -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,

View File

@ -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);
},
),
),

View File

@ -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(

View File

@ -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(() {});

View File

@ -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,
);
}

View File

@ -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(

View File

@ -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

View File

@ -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,
),
);
}

View File

@ -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,
),
);
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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

View File

@ -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.