diff --git a/packages/smooth_app/lib/background/abstract_background_task.dart b/packages/smooth_app/lib/background/abstract_background_task.dart index a497d0b0efb..107d3894fcf 100644 --- a/packages/smooth_app/lib/background/abstract_background_task.dart +++ b/packages/smooth_app/lib/background/abstract_background_task.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; +import 'package:smooth_app/background/background_task_crop.dart'; import 'package:smooth_app/background/background_task_details.dart'; import 'package:smooth_app/background/background_task_image.dart'; import 'package:smooth_app/background/background_task_manager.dart'; @@ -47,6 +48,7 @@ abstract class AbstractBackgroundTask { BackgroundTaskDetails.fromJson(map) ?? BackgroundTaskImage.fromJson(map) ?? BackgroundTaskUnselect.fromJson(map) ?? + BackgroundTaskCrop.fromJson(map) ?? BackgroundTaskRefreshLater.fromJson(map); /// Executes the background task: upload, download, update locally. diff --git a/packages/smooth_app/lib/background/background_task_crop.dart b/packages/smooth_app/lib/background/background_task_crop.dart new file mode 100644 index 00000000000..cceaa2c9bee --- /dev/null +++ b/packages/smooth_app/lib/background/background_task_crop.dart @@ -0,0 +1,228 @@ +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:provider/provider.dart'; +import 'package:smooth_app/background/abstract_background_task.dart'; +import 'package:smooth_app/background/background_task_image.dart'; +import 'package:smooth_app/background/background_task_refresh_later.dart'; +import 'package:smooth_app/data_models/operation_type.dart'; +import 'package:smooth_app/database/local_database.dart'; +import 'package:smooth_app/database/transient_file.dart'; +import 'package:smooth_app/query/product_query.dart'; + +/// Background task about product image crop from existing file. +class BackgroundTaskCrop extends AbstractBackgroundTask { + const BackgroundTaskCrop._({ + required super.processName, + required super.uniqueId, + required super.barcode, + required super.languageCode, + required super.user, + required super.country, + required super.stamp, + required this.imageId, + required this.imageField, + required this.croppedPath, + required this.rotationDegrees, + required this.cropX1, + required this.cropY1, + required this.cropX2, + required this.cropY2, + }); + + BackgroundTaskCrop._fromJson(Map json) + : this._( + processName: json['processName'] as String, + uniqueId: json['uniqueId'] as String, + barcode: json['barcode'] as String, + languageCode: json['languageCode'] as String, + user: json['user'] as String, + country: json['country'] as String, + imageId: json['imageId'] as int, + imageField: json['imageField'] as String, + croppedPath: json['croppedPath'] as String, + rotationDegrees: json['rotation'] as int, + cropX1: json['x1'] as int? ?? 0, + cropY1: json['y1'] as int? ?? 0, + cropX2: json['x2'] as int? ?? 0, + cropY2: json['y2'] as int? ?? 0, + stamp: json['stamp'] as String, + ); + + /// Task ID. + static const String _PROCESS_NAME = 'IMAGE_CROP'; + + static const OperationType _operationType = OperationType.crop; + + final int imageId; + final String imageField; + final String croppedPath; + final int rotationDegrees; + final int cropX1; + final int cropY1; + final int cropX2; + final int cropY2; + + @override + Map toJson() => { + 'processName': processName, + 'uniqueId': uniqueId, + 'barcode': barcode, + 'languageCode': languageCode, + 'user': user, + 'country': country, + 'imageId': imageId, + 'imageField': imageField, + 'croppedPath': croppedPath, + 'stamp': stamp, + 'rotation': rotationDegrees, + 'x1': cropX1, + 'y1': cropY1, + 'x2': cropX2, + 'y2': cropY2, + }; + + /// Returns the deserialized background task if possible, or null. + static AbstractBackgroundTask? fromJson(final Map map) { + try { + final AbstractBackgroundTask result = BackgroundTaskCrop._fromJson(map); + if (result.processName == _PROCESS_NAME) { + return result; + } + } catch (e) { + // + } + return null; + } + + /// Adds the background task about uploading a product image. + static Future addTask( + final String barcode, { + required final int imageId, + required final ImageField imageField, + required final File croppedFile, + required final int rotation, + required final int x1, + required final int y1, + required final int x2, + required final int y2, + required final State widget, + }) async { + final LocalDatabase localDatabase = widget.context.read(); + final String uniqueId = await _operationType.getNewKey( + localDatabase, + barcode, + ); + final AbstractBackgroundTask task = _getNewTask( + barcode, + imageId, + imageField, + croppedFile, + uniqueId, + rotation, + x1, + y1, + x2, + y2, + ); + await task.addToManager(localDatabase, widget: widget); + } + + @override + String? getSnackBarMessage(final AppLocalizations appLocalizations) => + appLocalizations.product_task_background_schedule; + + /// Returns a new background task about cropping an existing image. + static BackgroundTaskCrop _getNewTask( + final String barcode, + final int imageId, + final ImageField imageField, + final File croppedFile, + final String uniqueId, + final int rotationDegrees, + final int cropX1, + final int cropY1, + final int cropX2, + final int cropY2, + ) => + BackgroundTaskCrop._( + uniqueId: uniqueId, + barcode: barcode, + processName: _PROCESS_NAME, + imageId: imageId, + imageField: imageField.offTag, + croppedPath: croppedFile.path, + rotationDegrees: rotationDegrees, + cropX1: cropX1, + cropY1: cropY1, + cropX2: cropX2, + cropY2: cropY2, + languageCode: ProductQuery.getLanguage().code, + user: jsonEncode(ProductQuery.getUser().toJson()), + country: ProductQuery.getCountry()!.offTag, + stamp: BackgroundTaskImage.getStamp( + barcode, + imageField.offTag, + ProductQuery.getLanguage().code, + ), + ); + + @override + Future preExecute(final LocalDatabase localDatabase) async => + TransientFile.putImage( + ImageField.fromOffTag(imageField)!, + barcode, + localDatabase, + File(croppedPath), + ); + + @override + Future postExecute( + final LocalDatabase localDatabase, + final bool success, + ) async { + try { + File(croppedPath).deleteSync(); + } catch (e) { + // not likely, but let's not spoil the task for that either. + } + TransientFile.removeImage( + ImageField.fromOffTag(imageField)!, + barcode, + localDatabase, + ); + localDatabase.notifyListeners(); + if (success) { + await BackgroundTaskRefreshLater.addTask( + barcode, + localDatabase: localDatabase, + ); + } + } + + /// Uploads the product image. + @override + Future upload() async { + final ImageField imageField = ImageField.fromOffTag(this.imageField)!; + final OpenFoodFactsLanguage language = getLanguage(); + final User user = getUser(); + final String? imageUrl = await OpenFoodAPIClient.setProductImageCrop( + barcode: barcode, + imageField: imageField, + language: language, + imgid: '$imageId', + angle: ImageAngleExtension.fromInt(rotationDegrees)!, + x1: cropX1, + y1: cropY1, + x2: cropX2, + y2: cropY2, + user: user, + ); + if (imageUrl == null) { + throw Exception('Could not select picture'); + } + } +} diff --git a/packages/smooth_app/lib/data_models/operation_type.dart b/packages/smooth_app/lib/data_models/operation_type.dart index a45b13e596c..313365fd409 100644 --- a/packages/smooth_app/lib/data_models/operation_type.dart +++ b/packages/smooth_app/lib/data_models/operation_type.dart @@ -13,6 +13,7 @@ import 'package:smooth_app/helpers/database_helper.dart'; /// * possibly, which barcode (not useful yet) enum OperationType { image('I'), + crop('C'), unselect('U'), refreshLater('R'), details('D'); @@ -47,6 +48,8 @@ enum OperationType { return appLocalizations.background_task_operation_image; case OperationType.unselect: return 'Unselect a product image'; + case OperationType.crop: + return 'Crop an existing image'; case OperationType.refreshLater: return 'Waiting 10 min before refreshing product to get all automatic edits'; } diff --git a/packages/smooth_app/lib/generic_lib/widgets/images/smooth_images_sliver_grid.dart b/packages/smooth_app/lib/generic_lib/widgets/images/smooth_images_sliver_grid.dart deleted file mode 100644 index 2c0217fff1a..00000000000 --- a/packages/smooth_app/lib/generic_lib/widgets/images/smooth_images_sliver_grid.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:shimmer/shimmer.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/loading_sliver.dart'; -import 'package:smooth_app/generic_lib/widgets/images/smooth_image.dart'; -import 'package:smooth_app/generic_lib/widgets/images/smooth_images_view.dart'; -import 'package:smooth_app/generic_lib/widgets/picture_not_found.dart'; - -/// Displays a [SliverGrid] with tiles showing the images passed -/// via [imagesData] -class SmoothImagesSliverGrid extends SmoothImagesView { - const SmoothImagesSliverGrid({ - required super.imagesData, - super.onTap, - super.loading = false, - this.loadingCount = 6, - this.maxTileWidth = VERY_LARGE_SPACE * 7, - this.childAspectRatio = 1.5, - }); - - /// The number of shimmering tiles to display while [loading] is true - final int loadingCount; - - /// The maximum width of a tile - final double maxTileWidth; - - final double childAspectRatio; - - @override - Widget build(BuildContext context) { - final List> imageList = - imagesData.entries.toList(); - - return SliverPadding( - padding: const EdgeInsets.all(MEDIUM_SPACE), - sliver: SliverGrid( - delegate: LoadingSliverChildBuilderDelegate( - loading: loading, - childCount: imageList.length, - loadingWidget: _buildShimmer(), - loadingCount: loadingCount, - childBuilder: (BuildContext context, int index) { - final MapEntry?> entry = - imageList[index]; - final ImageProvider? imageProvider = entry.value; - final String? imageUrl = entry.key.imageUrl; - - return imageProvider == null || imageUrl == null - ? const PictureNotFound() - : Hero( - tag: imageUrl, - child: _ImageTile( - image: imageProvider, - onTap: onTap == null - ? null - : () => onTap!( - entry.key, - entry.value, - null, - ), - ), - ); - }), - gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: maxTileWidth, - childAspectRatio: childAspectRatio, - mainAxisSpacing: MEDIUM_SPACE, - crossAxisSpacing: MEDIUM_SPACE, - ), - ), - ); - } - - Widget _buildShimmer() => Shimmer.fromColors( - baseColor: WHITE_COLOR, - highlightColor: GREY_COLOR, - child: const SmoothImage( - width: VERY_LARGE_SPACE * 5, - height: MEDIUM_SPACE * 5, - color: WHITE_COLOR, - ), - ); -} - -class _ImageTile extends StatelessWidget { - const _ImageTile({ - required this.image, - this.onTap, - }); - - final ImageProvider image; - final void Function()? onTap; - - @override - Widget build(BuildContext context) => Ink( - decoration: BoxDecoration( - borderRadius: ROUNDED_BORDER_RADIUS, - image: DecorationImage( - image: image, - fit: BoxFit.cover, - )), - child: InkWell( - borderRadius: ROUNDED_BORDER_RADIUS, - onTap: onTap, - ), - ); -} diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index cd3600c3e11..70a8b36642e 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -1526,6 +1526,22 @@ "@edit_photo_unselect_button_label": { "description": "Edit 'unselect photo' button label" }, + "edit_photo_select_existing_button_label": "Select an existing image", + "@edit_photo_select_existing_button_label": { + "description": "Edit 'select existing image' button label" + }, + "edit_photo_select_existing_all_label": "Existing images", + "@edit_photo_select_existing_all_label": { + "description": "Page title" + }, + "edit_photo_select_existing_download_label": "Retrieving existing images...", + "@edit_photo_select_existing_download_label": { + "description": "Dialog label" + }, + "edit_photo_select_existing_downloaded_none": "There are no images previously uploaded related to this product.", + "@edit_photo_select_existing_downloaded_none": { + "description": "Error message" + }, "category_picker_screen_title": "Categories", "@category_picker_screen_title": { "description": "Categories picker screen title" diff --git a/packages/smooth_app/lib/pages/image/uploaded_image_gallery.dart b/packages/smooth_app/lib/pages/image/uploaded_image_gallery.dart new file mode 100644 index 00000000000..37d30dfb033 --- /dev/null +++ b/packages/smooth_app/lib/pages/image/uploaded_image_gallery.dart @@ -0,0 +1,104 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; +import 'package:provider/provider.dart'; +import 'package:smooth_app/database/dao_int.dart'; +import 'package:smooth_app/database/local_database.dart'; +import 'package:smooth_app/generic_lib/design_constants.dart'; +import 'package:smooth_app/generic_lib/widgets/images/smooth_image.dart'; +import 'package:smooth_app/pages/image_crop_page.dart'; +import 'package:smooth_app/tmp_crop_image/new_crop_page.dart'; +import 'package:smooth_app/widgets/smooth_scaffold.dart'; + +/// Gallery of all images already uploaded, about a given product. +class UploadedImageGallery extends StatelessWidget { + const UploadedImageGallery({ + required this.barcode, + required this.imageIds, + required this.imageField, + }); + + final String barcode; + final List imageIds; + final ImageField imageField; + + @override + Widget build(BuildContext context) { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + final MediaQueryData mediaQueryData = MediaQuery.of(context); + final double columnWidth = mediaQueryData.size.width * .45; + return SmoothScaffold( + backgroundColor: Colors.black, + appBar: AppBar( + title: Text(appLocalizations.edit_photo_select_existing_all_label), + backgroundColor: Colors.black, + foregroundColor: WHITE_COLOR, + elevation: 0, + ), + body: GridView.builder( + itemCount: imageIds.length, + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: mediaQueryData.size.width / 2, + childAspectRatio: 1, + mainAxisSpacing: MEDIUM_SPACE, + crossAxisSpacing: MEDIUM_SPACE, + ), + itemBuilder: (final BuildContext context, final int index) { + // order by descending ids + final int imageId = imageIds[imageIds.length - 1 - index]; + final String url = ImageHelper.getUploadedImageUrl( + barcode, + imageId, + ImageSize.DISPLAY, + ); + return GestureDetector( + onTap: () async { + final LocalDatabase localDatabase = context.read(); + final NavigatorState navigatorState = Navigator.of(context); + final File? imageFile = await downloadImageUrl( + context, + ImageHelper.getUploadedImageUrl( + barcode, + imageId, + ImageSize.ORIGINAL, + ), + DaoInt(localDatabase), + ); + if (imageFile == null) { + return; + } + await navigatorState.push( + MaterialPageRoute( + builder: (BuildContext context) => CropPage( + barcode: barcode, + imageField: imageField, + inputFile: imageFile, + imageId: imageId, + brandNewPicture: false, + ), + fullscreenDialog: true, + ), + ); + }, + child: ClipRRect( + borderRadius: ROUNDED_BORDER_RADIUS, + child: Container( + width: columnWidth, + height: columnWidth, + color: Colors.grey[900], + child: SmoothImage( + width: columnWidth, + height: columnWidth, + imageProvider: NetworkImage(url), + fit: BoxFit.contain, + ), + ), + ), + ); + }, + ), + ); + } +} diff --git a/packages/smooth_app/lib/pages/image_crop_page.dart b/packages/smooth_app/lib/pages/image_crop_page.dart index cc628e4efe2..fe6d11eed09 100644 --- a/packages/smooth_app/lib/pages/image_crop_page.dart +++ b/packages/smooth_app/lib/pages/image_crop_page.dart @@ -2,12 +2,17 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:http/http.dart' as http; import 'package:image_picker/image_picker.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import 'package:smooth_app/data_models/user_preferences.dart'; +import 'package:smooth_app/database/dao_int.dart'; import 'package:smooth_app/generic_lib/dialogs/smooth_alert_dialog.dart'; +import 'package:smooth_app/generic_lib/loading_dialog.dart'; import 'package:smooth_app/helpers/camera_helper.dart'; +import 'package:smooth_app/helpers/database_helper.dart'; import 'package:smooth_app/tmp_crop_image/new_crop_page.dart'; /// Picks an image file from gallery or camera. @@ -105,3 +110,53 @@ Future confirmAndUploadNewPicture( ), ); } + +/// Downloads an image URL into a file, with a dialog. +Future downloadImageUrl( + final BuildContext context, + final String? imageUrl, + final DaoInt daoInt, +) async { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + if (imageUrl == null) { + await LoadingDialog.error( + context: context, + title: appLocalizations.image_edit_url_error, + ); + return null; + } + + final File? imageFile = await LoadingDialog.run( + context: context, + future: _downloadImageFile(daoInt, imageUrl), + ); + + if (imageFile == null) { + await LoadingDialog.error( + context: context, + title: appLocalizations.image_download_error, + ); + } + return imageFile; +} + +/// Downloads an image from the server and stores it locally in temp folder. +Future _downloadImageFile(DaoInt daoInt, String url) async { + final Uri uri = Uri.parse(url); + final http.Response response = await http.get(uri); + final int code = response.statusCode; + if (code != 200) { + throw NetworkImageLoadException(statusCode: code, uri: uri); + } + + final Directory tempDirectory = await getTemporaryDirectory(); + + const String CROP_IMAGE_SEQUENCE_KEY = 'crop_image_sequence'; + + final int sequenceNumber = + await getNextSequenceNumber(daoInt, CROP_IMAGE_SEQUENCE_KEY); + + final File file = File('${tempDirectory.path}/editing_image_$sequenceNumber'); + + return file.writeAsBytes(response.bodyBytes); +} diff --git a/packages/smooth_app/lib/pages/product/product_image_swipeable_view.dart b/packages/smooth_app/lib/pages/product/product_image_swipeable_view.dart index f0a83a13b5a..1fbe5e700d4 100644 --- a/packages/smooth_app/lib/pages/product/product_image_swipeable_view.dart +++ b/packages/smooth_app/lib/pages/product/product_image_swipeable_view.dart @@ -29,8 +29,8 @@ class ProductImageSwipeableView extends StatefulWidget { class _ProductImageSwipeableViewState extends State { late final LocalDatabase _localDatabase; //Making use of [ValueNotifier] such that to avoid performance issues - //while swipping between pages by making sure only [Text] widget for product title is rebuilt - final ValueNotifier _currentImageDataIndex = ValueNotifier(0); + //while swiping between pages by making sure only [Text] widget for product title is rebuilt + late final ValueNotifier _currentImageDataIndex; late Map _selectedImages; late List _imageDataList; late PageController _controller; @@ -51,6 +51,7 @@ class _ProductImageSwipeableViewState extends State { _controller = PageController( initialPage: widget.initialImageIndex, ); + _currentImageDataIndex = ValueNotifier(widget.initialImageIndex); } @override diff --git a/packages/smooth_app/lib/pages/product/product_image_viewer.dart b/packages/smooth_app/lib/pages/product/product_image_viewer.dart index c2207ffd7b0..ecbff0d36a2 100644 --- a/packages/smooth_app/lib/pages/product/product_image_viewer.dart +++ b/packages/smooth_app/lib/pages/product/product_image_viewer.dart @@ -2,9 +2,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:http/http.dart' as http; import 'package:openfoodfacts/openfoodfacts.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:photo_view/photo_view.dart'; import 'package:provider/provider.dart'; import 'package:smooth_app/background/background_task_unselect.dart'; @@ -13,12 +11,14 @@ import 'package:smooth_app/database/dao_int.dart'; import 'package:smooth_app/database/local_database.dart'; import 'package:smooth_app/database/transient_file.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; +import 'package:smooth_app/generic_lib/dialogs/smooth_alert_dialog.dart'; import 'package:smooth_app/generic_lib/loading_dialog.dart'; import 'package:smooth_app/generic_lib/widgets/picture_not_found.dart'; -import 'package:smooth_app/helpers/database_helper.dart'; import 'package:smooth_app/helpers/product_cards_helper.dart'; +import 'package:smooth_app/pages/image/uploaded_image_gallery.dart'; import 'package:smooth_app/pages/image_crop_page.dart'; import 'package:smooth_app/pages/product/edit_image_button.dart'; +import 'package:smooth_app/query/product_query.dart'; import 'package:smooth_app/tmp_crop_image/new_crop_page.dart'; import 'package:smooth_app/widgets/smooth_scaffold.dart'; @@ -92,6 +92,68 @@ class _ProductImageViewerState extends State { ), ), ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Expanded(child: Container()), // would be "take another picture" + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: SMALL_SPACE), + child: EditImageButton( + iconData: Icons.image_search, + label: appLocalizations + .edit_photo_select_existing_button_label, + onPressed: () async { + final List? result = + await LoadingDialog.run>( + future: OpenFoodAPIClient.getProductImageIds( + _barcode, + user: ProductQuery.getUser(), + ), + context: context, + title: appLocalizations + .edit_photo_select_existing_download_label, + ); + if (result == null) { + return; + } + if (!mounted) { + return; + } + if (result.isEmpty) { + await showDialog( + context: context, + builder: (BuildContext context) => SmoothAlertDialog( + body: Text(appLocalizations + .edit_photo_select_existing_downloaded_none), + actionsAxis: Axis.vertical, + positiveAction: SmoothActionButton( + text: appLocalizations.okay, + onPressed: () => Navigator.of(context).pop(), + ), + ), + ); + return; + } + await Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => + UploadedImageGallery( + barcode: _barcode, + imageIds: result, + imageField: widget.imageField, + ), + ), + ); + }, + ), + ), + ), + ], + ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, @@ -158,27 +220,13 @@ class _ProductImageViewerState extends State { // but if not possible, get the best picture from the server. if (imageFile == null) { - final AppLocalizations appLocalizations = AppLocalizations.of(context); final String? imageUrl = _imageData.getImageUrl(ImageSize.ORIGINAL); - if (imageUrl == null) { - await LoadingDialog.error( - context: context, - title: appLocalizations.image_edit_url_error, - ); - return; - } - - final DaoInt daoInt = DaoInt(_localDatabase); - imageFile = await LoadingDialog.run( - context: context, - future: _downloadImageFile(daoInt, imageUrl), + imageFile = await downloadImageUrl( + context, + imageUrl, + DaoInt(_localDatabase), ); - if (imageFile == null) { - await LoadingDialog.error( - context: context, - title: appLocalizations.image_download_error, - ); return; } } @@ -200,26 +248,4 @@ class _ProductImageViewerState extends State { ), ); } - - static const String _CROP_IMAGE_SEQUENCE_KEY = 'crop_image_sequence'; - - /// Downloads an image from the server and stores it locally in temp folder. - Future _downloadImageFile(DaoInt daoInt, String url) async { - final Uri uri = Uri.parse(url); - final http.Response response = await http.get(uri); - final int code = response.statusCode; - if (code != 200) { - throw NetworkImageLoadException(statusCode: code, uri: uri); - } - - final Directory tempDirectory = await getTemporaryDirectory(); - - final int sequenceNumber = - await getNextSequenceNumber(daoInt, _CROP_IMAGE_SEQUENCE_KEY); - - final File file = - File('${tempDirectory.path}/editing_image_$sequenceNumber'); - - return file.writeAsBytes(response.bodyBytes); - } } diff --git a/packages/smooth_app/lib/tmp_crop_image/new_crop_page.dart b/packages/smooth_app/lib/tmp_crop_image/new_crop_page.dart index 2745220fc0d..93f04723f11 100644 --- a/packages/smooth_app/lib/tmp_crop_image/new_crop_page.dart +++ b/packages/smooth_app/lib/tmp_crop_image/new_crop_page.dart @@ -10,6 +10,7 @@ import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import 'package:scanner_shared/scanner_shared.dart'; +import 'package:smooth_app/background/background_task_crop.dart'; import 'package:smooth_app/background/background_task_image.dart'; import 'package:smooth_app/data_models/continuous_scan_model.dart'; import 'package:smooth_app/database/dao_int.dart'; @@ -33,6 +34,7 @@ class CropPage extends StatefulWidget { required this.barcode, required this.imageField, required this.brandNewPicture, + this.imageId, }); /// The initial input file we start with. @@ -44,6 +46,9 @@ class CropPage extends StatefulWidget { /// Is that a new picture we crop, or an existing picture? final bool brandNewPicture; + /// Only makes sense when we deal with an "already existing" image. + final int? imageId; + @override State createState() => _CropPageState(); } @@ -218,24 +223,39 @@ class _CropPageState extends State { if (croppedFile == null) { return true; } - final File fullFile = await _getFullImageFile( - directory, - sequenceNumber, - ); final Rect cropRect = _getCropRect(); - await BackgroundTaskImage.addTask( - widget.barcode, - imageField: widget.imageField, - fullFile: fullFile, - croppedFile: croppedFile, - rotation: _controller.degrees, - x1: cropRect.left.ceil(), - y1: cropRect.top.ceil(), - x2: cropRect.right.floor(), - y2: cropRect.bottom.floor(), - widget: this, - ); + if (widget.imageId == null) { + final File fullFile = await _getFullImageFile( + directory, + sequenceNumber, + ); + await BackgroundTaskImage.addTask( + widget.barcode, + imageField: widget.imageField, + fullFile: fullFile, + croppedFile: croppedFile, + rotation: _controller.degrees, + x1: cropRect.left.ceil(), + y1: cropRect.top.ceil(), + x2: cropRect.right.floor(), + y2: cropRect.bottom.floor(), + widget: this, + ); + } else { + await BackgroundTaskCrop.addTask( + widget.barcode, + imageField: widget.imageField, + imageId: widget.imageId!, + croppedFile: croppedFile, + rotation: _controller.degrees, + x1: cropRect.left.ceil(), + y1: cropRect.top.ceil(), + x2: cropRect.right.floor(), + y2: cropRect.bottom.floor(), + widget: this, + ); + } localDatabase.notifyListeners(); if (!mounted) { return true; diff --git a/packages/smooth_app/pubspec.lock b/packages/smooth_app/pubspec.lock index 48c62c4e8ef..3df17efb458 100644 --- a/packages/smooth_app/pubspec.lock +++ b/packages/smooth_app/pubspec.lock @@ -811,7 +811,7 @@ packages: name: openfoodfacts url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" + version: "2.2.0" openfoodfacts_flutter_lints: dependency: "direct dev" description: @@ -1100,7 +1100,7 @@ packages: name: sentry url: "https://pub.dartlang.org" source: hosted - version: "6.13.1" + version: "6.18.3" sentry_flutter: dependency: "direct main" description: diff --git a/packages/smooth_app/pubspec.yaml b/packages/smooth_app/pubspec.yaml index 7d3a2b27dee..6a5fb45ec06 100644 --- a/packages/smooth_app/pubspec.yaml +++ b/packages/smooth_app/pubspec.yaml @@ -31,9 +31,9 @@ dependencies: latlong2: 0.8.1 matomo_tracker: 1.5.0 modal_bottom_sheet: 2.1.2 - openfoodfacts: 2.1.2 - # openfoodfacts: - # path: ../../../openfoodfacts-dart + openfoodfacts: 2.2.0 + # openfoodfacts: + # path: ../../../openfoodfacts-dart package_info_plus: 1.4.3+1 device_info_plus: 4.1.2 permission_handler: 9.2.0