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

feat: image list gallery #2724

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d748bc6
feat: image list gallery
VaiTon Aug 3, 2022
27e780b
feat: added arrows and styling to tiles
VaiTon Aug 3, 2022
be68b27
Merge branch 'develop' into feat/image_gallery
VaiTon Aug 4, 2022
50f1e1d
refactor: extract SmoothListTileCard widget
VaiTon Aug 5, 2022
7b70d61
docs: add documentation to `product_image_gallery_view.dart`
VaiTon Aug 5, 2022
3e3968c
chore: cleaned code
VaiTon Aug 5, 2022
5d53934
refactor: rename `getAllProductImagesData` to `getProductMainImagesDa…
VaiTon Aug 6, 2022
8339953
Merge branch 'develop' into feat/image_gallery
VaiTon Aug 6, 2022
86af408
feat: display every image in gallery view
VaiTon Aug 6, 2022
ae461e4
Merge branch 'develop' into feat/image_gallery
VaiTon Aug 7, 2022
cb34353
fix: make onTap work again
VaiTon Aug 7, 2022
5a1a91c
Merge branch 'develop' into feat/image_gallery
VaiTon Aug 9, 2022
2318810
Merge branch 'develop' into feat/image_gallery
VaiTon Aug 10, 2022
5236428
Merge branch 'develop' into feat/image_gallery
VaiTon Aug 13, 2022
3693d95
feat: shimmering effect in `smooth_list_view.dart`
VaiTon Aug 13, 2022
dfcd7b0
Update packages/smooth_app/lib/generic_lib/widgets/smooth_image_list.…
VaiTon Aug 16, 2022
862241a
refactor: extract SmoothBackButton
VaiTon Aug 16, 2022
efa58c2
refactor: extract methods and add docs in `product_image_viewer.dart`
VaiTon Aug 16, 2022
dbcd334
Merge branch 'develop' into feat/image_gallery
VaiTon Aug 16, 2022
0b580a3
refactor: use _ListTileItem in `edit_product_page.dart`
VaiTon Aug 16, 2022
c21aafc
refactor: allow better reviews
VaiTon Aug 16, 2022
a57a485
fix: use relative size instead of arbitrary ones in `smooth_list_tile…
VaiTon Aug 16, 2022
7a25112
fix: use black for `product_image_viewer.dart` AppBar
VaiTon Aug 16, 2022
d44439e
fix: use Flutter TODO format
VaiTon Aug 16, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ class _ImageUploadCardState extends State<ImageUploadCard> {
}
final bool isUploaded = 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
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,
);
Expand Down Expand Up @@ -131,9 +131,7 @@ class _ImageUploadCardState extends State<ImageUploadCard> {
context,
MaterialPageRoute<bool>(
builder: (BuildContext context) => ProductImageGalleryView(
productImageData: widget.productImageData,
allProductImagesData: widget.allProductImagesData,
title: widget.productImageData.title,
imagesData: widget.allProductImagesData,
barcode: widget.product.barcode,
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class ProductImageCarousel extends StatelessWidget {
Widget build(BuildContext context) {
final AppLocalizations appLocalizations = AppLocalizations.of(context);
final List<ProductImageData> allProductImagesData =
getAllProductImagesData(product, appLocalizations);
getProductMainImagesData(product, appLocalizations);

return SizedBox(
height: height,
Expand Down
14 changes: 14 additions & 0 deletions packages/smooth_app/lib/generic_lib/widgets/picture_not_found.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_svg/svg.dart';

class PictureNotFound extends StatelessWidget {
const PictureNotFound();

@override
Widget build(BuildContext context) {
return SvgPicture.asset(
'assets/product/product_not_found.svg',
fit: BoxFit.cover,
);
}
}
45 changes: 45 additions & 0 deletions packages/smooth_app/lib/generic_lib/widgets/smooth_image_list.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import 'package:flutter/material.dart';
import 'package:smooth_app/data_models/product_image_data.dart';
import 'package:smooth_app/generic_lib/widgets/smooth_list_tile_card.dart';
import 'package:smooth_app/generic_lib/widgets/smooth_list_view.dart';

/// Represents a list of [ProductImageData]
VaiTon marked this conversation as resolved.
Show resolved Hide resolved
class SmoothImageList extends StatelessWidget {
const SmoothImageList({
required this.imagesData,
this.onTap,
this.loading = false,
});

final Map<ProductImageData, ImageProvider?> imagesData;
final void Function(ProductImageData, ImageProvider?)? onTap;
final bool loading;

@override
Widget build(BuildContext context) {
final ThemeData themeData = Theme.of(context);
final List<MapEntry<ProductImageData, ImageProvider?>> imageList =
imagesData.entries.toList();
final int count = imageList.length;

return Scrollbar(
child: SmoothListView.builder(
loading: loading,
itemCount: count,
loadingWidget: (_, __) => SmoothListTileCard.loading(),
itemBuilder: (_, int index) => SmoothListTileCard.image(
imageProvider: imageList[index].value,
title: Text(
imageList[index].key.title,
style: themeData.textTheme.headline4,
),
onTap: () {
if (onTap != null) {
onTap!(imageList[index].key, imageList[index].value);
}
},
),
),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import 'package:smooth_app/generic_lib/design_constants.dart';
import 'package:smooth_app/generic_lib/widgets/picture_not_found.dart';
import 'package:smooth_app/generic_lib/widgets/smooth_card.dart';
import 'package:smooth_app/generic_lib/widgets/smooth_product_image_container.dart';
import 'package:smooth_app/themes/constant_icons.dart';

class SmoothListTileCard extends StatelessWidget {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused: is that a list card, like a Card(child: ListTile(...)), or is that for pictures?
You would probably be better off with 2 distinct classes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is like a Card(child: ListTile(...)) but I added some constructor to facilitate the creation of an inner ListTile with a leading image or a leading icon. Should I create different classes? I don't really know when to create a different class vs just create a delegating constructor.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The big misunderstanding is that I never imagined that you would display an image inside a "normal" ListTile. I expected something similar to the website: tons of pictures in a grid.

I don't know if there were explicit expectations regarding UX/UI, but I must say I'm not a big fan of:

  • all the waste space with no added value (the "title")
  • the cropped image
  • the image ratio

We should start from there: how is it supposed to look? @teolemon Are you OK with the latest screenshots?

const SmoothListTileCard({
required final this.title,
this.subtitle,
this.onTap,
this.leading,
Key? key,
}) : super(key: key);

SmoothListTileCard.image({
Widget? title,
required ImageProvider? imageProvider,
GestureTapCallback? onTap,
}) : this(
title: title,
leading: SmoothProductImageContainer(
width: 100,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does that come from? Why not 103?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. I didn't know where to take this from and 100 seems to have a good visual effect. I'm updating the images

child: imageProvider != null
? Image(
image: imageProvider,
fit: BoxFit.cover,
)
: const PictureNotFound(),
),
onTap: onTap,
);

SmoothListTileCard.loading()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand: is that only a "loading" effect for "pictures"? If so call it something like "imageLoading".

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a widget that displays the shimmer effect

: this(
title: Shimmer.fromColors(
baseColor: GREY_COLOR,
highlightColor: WHITE_COLOR,
child: Container(
width: 200,
height: 10,
decoration: const BoxDecoration(
color: GREY_COLOR,
borderRadius: CIRCULAR_BORDER_RADIUS,
)),
),
leading: Shimmer.fromColors(
baseColor: GREY_COLOR,
highlightColor: WHITE_COLOR,
child: const SmoothProductImageContainer(
width: 100,
height: 50,
VaiTon marked this conversation as resolved.
Show resolved Hide resolved
color: GREY_COLOR,
),
),
);

SmoothListTileCard.icon({
Widget? icon,
Widget? title,
Widget? subtitle,
GestureTapCallback? onTap,
Key? key,
}) : this(
title: title,
subtitle: subtitle,
// we use a Column to have the icon centered vertically
leading: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[icon ?? const Icon(Icons.edit)],
),
key: key,
onTap: onTap,
);

final Widget? title;
final Widget? subtitle;
final Widget? leading;
final GestureTapCallback? onTap;

@override
Widget build(BuildContext context) => SmoothCard(
child: ListTile(
onTap: onTap,
title: title,
subtitle: subtitle,
leading: leading,
trailing: Icon(ConstantIcons.instance.getForwardIcon()),
),
);
}
24 changes: 24 additions & 0 deletions packages/smooth_app/lib/generic_lib/widgets/smooth_list_view.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'package:flutter/widgets.dart';

class SmoothListView extends ListView {
/// Represents a ListView that can be show progress by displaying a
/// [loadingWidget] at the end of the list.
///
/// [loading] controls the display of the [loadingWidget] at the end of
/// the list.
///
SmoothListView.builder({
required IndexedWidgetBuilder itemBuilder,
required int itemCount,
IndexedWidgetBuilder? loadingWidget,
bool loading = false,
}) : assert(loading == false || loadingWidget != null),
super.builder(
itemCount: loading ? itemCount + 1 : itemCount,
itemBuilder: (BuildContext context, int index) =>
loading && (index == itemCount)
// Render a loading card as the last card
? loadingWidget!(context, index)
: itemBuilder(context, index),
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:openfoodfacts/model/Product.dart';
import 'package:smooth_app/generic_lib/widgets/picture_not_found.dart';
import 'package:smooth_app/generic_lib/widgets/smooth_product_image_container.dart';

/// Main product image on a product card.
Expand All @@ -17,49 +17,35 @@ class SmoothProductImage extends StatelessWidget {

@override
Widget build(BuildContext context) {
Widget? result;
result = _buildFromUrl(product.imageFrontSmallUrl);
if (result != null) {
return result;
}
result = _buildFromUrl(product.imageFrontUrl);
if (result != null) {
return result;
}
final Widget child = _buildFromUrl(product.imageFrontSmallUrl) ??
_buildFromUrl(product.imageFrontUrl) ??
const Center(child: PictureNotFound());

return SmoothProductImageContainer(
width: width,
height: height,
child: Center(
child: SvgPicture.asset(
'assets/product/product_not_found.svg',
fit: BoxFit.cover,
),
),
child: child,
);
}

Widget? _buildFromUrl(final String? url) => url == null || url.isEmpty
Image? _buildFromUrl(final String? url) => url == null || url.isEmpty
? null
: SmoothProductImageContainer(
width: width,
height: height,
child: Image.network(
url,
fit: BoxFit.contain,
loadingBuilder: (BuildContext context, Widget child,
ImageChunkEvent? progress) =>
progress == null
? child
: Center(
child: CircularProgressIndicator(
strokeWidth: 2.5,
valueColor: const AlwaysStoppedAnimation<Color>(
Colors.white,
: Image.network(
url,
fit: BoxFit.contain,
loadingBuilder:
(BuildContext _, Widget child, ImageChunkEvent? progress) =>
progress == null
? child
: Center(
child: CircularProgressIndicator(
strokeWidth: 2.5,
valueColor: const AlwaysStoppedAnimation<Color>(
Colors.white,
),
value: progress.cumulativeBytesLoaded /
progress.expectedTotalBytes!,
),
value: progress.cumulativeBytesLoaded /
progress.expectedTotalBytes!,
),
),
),
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,24 @@ import 'package:smooth_app/generic_lib/design_constants.dart';
/// Container to display the main product image on a product card.
class SmoothProductImageContainer extends StatelessWidget {
const SmoothProductImageContainer({
required this.height,
required this.width,
required this.child,
this.child,
this.height,
this.width,
this.color,
});

final double height;
final double width;
final Widget child;
final Widget? child;
final double? height;
final double? width;
final Color? color;

@override
Widget build(BuildContext context) => ClipRRect(
borderRadius: ROUNDED_BORDER_RADIUS,
child: SizedBox(
child: Container(
width: width,
height: height,
color: color,
child: child,
),
);
Expand Down
11 changes: 0 additions & 11 deletions packages/smooth_app/lib/helpers/collections_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -77,17 +77,6 @@ extension SetExtensions<T> on Set<T> {
}
}

extension IterableExtensions<T> on Iterable<T> {
T? firstWhereOrNull(bool Function(T element) test) {
for (final T element in this) {
if (test(element)) {
return element;
}
}
return null;
}
}

extension MapStringKeyExtensions<V> on Map<String, V> {
String? keyStartingWith(String key, {bool ignoreCase = false}) {
final String searchKey;
Expand Down
9 changes: 5 additions & 4 deletions packages/smooth_app/lib/helpers/product_cards_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,11 @@ Widget addPanelButton(
),
);

List<ProductImageData> getAllProductImagesData(
Product product, AppLocalizations appLocalizations) {
final List<ProductImageData> allProductImagesData = <ProductImageData>[
List<ProductImageData> getProductMainImagesData(
Product product,
AppLocalizations appLocalizations,
) {
return <ProductImageData>[
ProductImageData(
imageField: ImageField.FRONT,
imageUrl: product.imageFrontUrl,
Expand Down Expand Up @@ -123,5 +125,4 @@ List<ProductImageData> getAllProductImagesData(
buttonText: appLocalizations.more_photos,
),
];
return allProductImagesData;
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ class ProductRefresher {
await showDialog<void>(
context: context,
builder: (BuildContext context) => SmoothAlertDialog(
body: Text(appLocalizations
.nutrition_page_update_done), // TODO(monsieurtanuki): title as method parameter
body: Text(appLocalizations.nutrition_page_update_done),
// TODO(monsieurtanuki): title as method parameter
positiveAction: SmoothActionButton(
text: appLocalizations.okay,
onPressed: () => Navigator.of(context).pop(),
Expand Down Expand Up @@ -128,6 +128,7 @@ class ProductRefresher {
return const _MetaProductRefresher.error(null);
}

/// Returns `true` if the fetch is successful.
Future<bool> fetchAndRefresh({
required final BuildContext context,
required final LocalDatabase localDatabase,
Expand Down Expand Up @@ -177,6 +178,7 @@ class ProductRefresher {

class _MetaProductRefresher {
const _MetaProductRefresher.error(this.error) : product = null;

const _MetaProductRefresher.product(this.product) : error = null;

final String? error;
Expand Down
Loading