Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: ProductImageData to contain all image links #3088

Merged
merged 12 commits into from
Oct 12, 2022
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,11 @@ class _ImageUploadCardState extends State<ImageUploadCard> {
// we need to load the full resolution image

if (_imageFullProvider == null) {
final String imageFullUrl =
widget.productImageData.imageUrl!.replaceAll('.400.', '.full.');
_imageFullProvider = NetworkImage(imageFullUrl);
final String? imageFullUrl =
widget.productImageData.getImageUrl(ImageSize.ORIGINAL);
if (imageFullUrl != null) {
_imageFullProvider = NetworkImage(imageFullUrl);
}
}

// TODO(monsieurtanuki): careful, waiting for pop'ed value
Expand Down
39 changes: 38 additions & 1 deletion packages/smooth_app/lib/data_models/product_image_data.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'package:openfoodfacts/model/ProductImage.dart';
import 'package:openfoodfacts/openfoodfacts.dart';

class ProductImageData {
const ProductImageData({
Expand All @@ -8,8 +8,45 @@ class ProductImageData {
this.imageUrl,
});

factory ProductImageData.from(ProductImage image, String barcode) {
return ProductImageData(
imageField: image.field,
// TODO(VaiTon): i18n
title: image.imgid ?? '',
buttonText: image.imgid ?? '',
imageUrl: ImageHelper.buildUrl(barcode, image),
);
}

final ImageField imageField;
final String title;
final String buttonText;
final String? imageUrl;

/// Try to convert [imageUrl] to specified [size].
/// Note that url for specified [size] might not exist on API.
String? getImageUrl(ImageSize size) {
final String? imageUrl = this.imageUrl;
if (imageUrl == null) {
return null;
}

const String SEPARATOR = '.';

final int extensionIndex = imageUrl.lastIndexOf(SEPARATOR);
if (extensionIndex < 0) {
return null;
}

final int sizeIndex = imageUrl.lastIndexOf(SEPARATOR, extensionIndex - 1);
if (sizeIndex < 0) {
return null;
}

final String baseUrl = imageUrl.substring(0, sizeIndex + 1);
final String number = size.toNumber();
final String extension =
imageUrl.substring(extensionIndex, imageUrl.length);
return baseUrl + number + extension;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,12 @@ class SmoothImagesSliverGrid extends SmoothImagesView {
final MapEntry<ProductImageData, ImageProvider<Object>?> entry =
imageList[index];
final ImageProvider? imageProvider = entry.value;
final String? imageUrl = entry.key.imageUrl;

return imageProvider == null
return imageProvider == null || imageUrl == null
? const PictureNotFound()
: Hero(
tag: entry.key.imageUrl!,
tag: imageUrl,
child: _ImageTile(
image: imageProvider,
onTap: onTap == null
Expand Down
8 changes: 8 additions & 0 deletions packages/smooth_app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1698,5 +1698,13 @@
"word_separator": ", ",
"@word_separator": {
"description": "Word separator string. In English, this is a comma followed by a space: ', '"
},
"image_download_error": "Failed to download image",
"@image_download_error": {
"description": "Error message, when image download fails"
},
"image_edit_url_error": "Failed to edit image, image url not set.",
"@image_edit_url_error": {
"description": "Error message, when editing image fails, due to missing url."
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,13 @@ class _ProductImageGalleryViewState extends State<ProductImageGalleryView> {
}

Future<Iterable<ProductImageData>?> _getProductImages() async {
final String? barcode = widget.product.barcode;
if (barcode == null) {
return null;
}

final ProductQueryConfiguration configuration = ProductQueryConfiguration(
widget.product.barcode!,
barcode,
fields: <ProductField>[ProductField.IMAGES],
language: ProductQuery.getLanguage(),
country: ProductQuery.getCountry(),
Expand All @@ -250,7 +255,8 @@ class _ProductImageGalleryViewState extends State<ProductImageGalleryView> {
return null;
}

return _deduplicateImages(resultProduct.images!).map(_getProductImageData);
return _deduplicateImages(resultProduct.images!)
.map((ProductImage image) => ProductImageData.from(image, barcode));
}

/// Groups the list of [ProductImage] by [ProductImage.imgid]
Expand All @@ -259,15 +265,16 @@ class _ProductImageGalleryViewState extends State<ProductImageGalleryView> {
images
.groupListsBy((ProductImage element) => element.imgid)
.values
.map((List<ProductImage> sameIdImages) => sameIdImages.firstOrNull)
.map(_findBestProductImage)
.whereNotNull();

/// Created a [ProductImageData] from a [ProductImage]
ProductImageData _getProductImageData(ProductImage image) => ProductImageData(
imageField: image.field,
// TODO(VaiTon): i18n
title: image.imgid ?? '',
buttonText: image.imgid ?? '',
imageUrl: ImageHelper.buildUrl(widget.product.barcode, image),
);
ProductImage? _findBestProductImage(Iterable<ProductImage> images) {
final Map<ImageSize?, ProductImage> map = images
.groupListsBy((ProductImage image) => image.size)
.map((ImageSize? key, List<ProductImage> value) =>
MapEntry<ImageSize?, ProductImage>(key, value.first));
return map[ImageSize.DISPLAY] ??
map[ImageSize.SMALL] ??
map[ImageSize.THUMB];
}
}
30 changes: 23 additions & 7 deletions packages/smooth_app/lib/pages/product/product_image_viewer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,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/model/ProductImage.dart';
import 'package:path_provider/path_provider.dart';
import 'package:photo_view/photo_view.dart';
import 'package:provider/provider.dart';
Expand Down Expand Up @@ -32,6 +33,7 @@ class ProductImageViewer extends StatefulWidget {

class _ProductImageViewerState extends State<ProductImageViewer> {
late final ProductImageData imageData;
late final AppLocalizations appLocalizations = AppLocalizations.of(context);

/// When the image is edited, this is the new image
late ImageProvider imageProvider;
Expand All @@ -50,7 +52,7 @@ class _ProductImageViewerState extends State<ProductImageViewer> {
extendBodyBehindAppBar: true,
backgroundColor: Colors.black,
floatingActionButton: FloatingActionButton.extended(
label: Text(AppLocalizations.of(context).edit_photo_button_label),
label: Text(appLocalizations.edit_photo_button_label),
icon: const Icon(Icons.edit),
backgroundColor: Theme.of(context).colorScheme.primary,
onPressed: () {
Expand Down Expand Up @@ -90,12 +92,17 @@ class _ProductImageViewerState extends State<ProductImageViewer> {
);

Future<void> _editImage(final DaoInt daoInt) async {
final File? imageFile = await LoadingDialog.run<File>(
context: context,
future: _downloadImageFile(daoInt, imageData.imageUrl!),
);
final String? imageUrl = imageData.getImageUrl(ImageSize.ORIGINAL);
if (imageUrl == null) {
await _showDownloadFailedDialog(appLocalizations.image_edit_url_error);
return;
}

final File? imageFile = await LoadingDialog.run<File?>(
context: context, future: _downloadImageFile(daoInt, imageUrl));

if (imageFile == null) {
await _showDownloadFailedDialog(appLocalizations.image_download_error);
return;
}

Expand Down Expand Up @@ -125,10 +132,19 @@ class _ProductImageViewerState extends State<ProductImageViewer> {
}
}

Future<void> _showDownloadFailedDialog(String? title) =>
LoadingDialog.error(context: context, title: title);

static const String _CROP_IMAGE_SEQUENCE_KEY = 'crop_image_sequence';

Future<File> _downloadImageFile(DaoInt daoInt, String url) async {
final http.Response response = await http.get(Uri.parse(url));
Future<File?> _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 =
Expand Down