Skip to content

Commit

Permalink
feat: Add a gallery of the images selected and uploaded for a product (
Browse files Browse the repository at this point in the history
…#2801)

* feat: image list gallery
* feat: display every image in gallery view
* feat: shimmering effect in `smooth_list_view.dart`
* docs: add documentation to `product_image_gallery_view.dart`
* refactor: extract SmoothBackButton

Co-authored-by: monsieurtanuki <fabrice_fontaine@hotmail.com>
  • Loading branch information
VaiTon and monsieurtanuki committed Sep 4, 2022
1 parent d1b9020 commit c706839
Show file tree
Hide file tree
Showing 25 changed files with 818 additions and 485 deletions.
15 changes: 7 additions & 8 deletions packages/smooth_app/lib/cards/data_cards/image_upload_card.dart
Expand Up @@ -25,9 +25,11 @@ class ImageUploadCard extends StatefulWidget {
}

class _ImageUploadCardState extends State<ImageUploadCard> {
ImageProvider? _imageProvider; // Normal size image to display in carousel
ImageProvider?
_imageFullProvider; // Full resolution image to display in image page
/// Normal size image to display in carousel
ImageProvider? _imageProvider;

/// Full resolution image to display in image page
ImageProvider? _imageFullProvider;

Future<void> _getImage() async {
final File? croppedImageFile =
Expand All @@ -47,8 +49,7 @@ class _ImageUploadCardState extends State<ImageUploadCard> {
}
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!,
imageField: widget.productImageData.imageField,
imageUri: croppedImageFile.uri,
);
Expand Down Expand Up @@ -113,9 +114,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
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
Expand Up @@ -86,7 +86,7 @@ class SmoothProductCardFound extends StatelessWidget {
padding: const EdgeInsets.all(VERY_SMALL_SPACE),
child: Row(
children: <Widget>[
SmoothProductImage(
SmoothMainProductImage(
product: product,
width: screenSize.width * 0.20,
height: screenSize.width * 0.20,
Expand Down
Expand Up @@ -2,8 +2,8 @@ import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:smooth_app/cards/product_cards/smooth_product_card_found.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/generic_lib/widgets/smooth_card.dart';
import 'package:smooth_app/generic_lib/widgets/smooth_product_image_container.dart';
import 'package:smooth_app/helpers/ui_helpers.dart';

/// Empty template for a product card display.
Expand Down Expand Up @@ -51,10 +51,10 @@ class SmoothProductCardTemplate extends StatelessWidget {
padding: const EdgeInsets.all(VERY_SMALL_SPACE),
child: Row(
children: <Widget>[
SmoothProductImageContainer(
SmoothImage(
width: screenSize.width * 0.20,
height: screenSize.width * 0.20,
child: Container(color: itemColor),
color: itemColor,
),
const Padding(padding: EdgeInsets.only(left: VERY_SMALL_SPACE)),
Expanded(
Expand Down
21 changes: 21 additions & 0 deletions packages/smooth_app/lib/generic_lib/loading_sliver.dart
@@ -0,0 +1,21 @@
import 'package:flutter/widgets.dart';

/// A [SliverChildBuilderDelegate] that can show progress by displaying
/// [loadingWidget]s.
///
/// When [loading] is `true`, [loadingCount] of [loadingWidget] will be
/// displayed.
class LoadingSliverChildBuilderDelegate extends SliverChildBuilderDelegate {
LoadingSliverChildBuilderDelegate({
required IndexedWidgetBuilder childBuilder,
required int childCount,
Widget? loadingWidget,
int loadingCount = 4,
bool loading = false,
}) : assert(loading == false || loadingWidget != null),
super(
(BuildContext context, int index) =>
loading ? loadingWidget : childBuilder(context, index),
childCount: loading ? loadingCount : childCount,
);
}
@@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:smooth_app/generic_lib/design_constants.dart';
import 'package:smooth_app/generic_lib/widgets/picture_not_found.dart';

/// Container to display a product image on a product card.
///
/// If [imageProvider] is null, [PictureNotFound] is displayed.
class SmoothImage extends StatelessWidget {
const SmoothImage({
this.imageProvider,
this.height,
this.width,
this.color,
this.decoration,
this.fit,
});

final ImageProvider? imageProvider;
final double? height;
final double? width;
final Color? color;
final Decoration? decoration;
final BoxFit? fit;

@override
Widget build(BuildContext context) => ClipRRect(
borderRadius: ROUNDED_BORDER_RADIUS,
child: Container(
decoration: decoration,
width: width,
height: height,
color: color,
child: imageProvider == null
? const PictureNotFound()
: Image(
image: imageProvider!,
fit: fit ?? BoxFit.cover,
loadingBuilder: _loadingBuilder,
),
),
);

Widget _loadingBuilder(
BuildContext _,
Widget child,
ImageChunkEvent? progress,
) {
if (progress == null) {
return child;
}

final double progressValue =
progress.cumulativeBytesLoaded / progress.expectedTotalBytes!;

return Center(
child: CircularProgressIndicator(
strokeWidth: 2.5,
valueColor: const AlwaysStoppedAnimation<Color>(
Colors.white,
),
value: progressValue,
),
);
}
}
@@ -0,0 +1,103 @@
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<MapEntry<ProductImageData, ImageProvider?>> 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<ProductImageData, ImageProvider<Object>?> entry =
imageList[index];
final ImageProvider? imageProvider = entry.value;

return imageProvider == null
? const PictureNotFound()
: Hero(
tag: entry.key.imageUrl!,
child: _ImageTile(
image: imageProvider,
onTap: onTap == null
? null
: () => onTap!(entry.key, entry.value),
),
);
}),
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,
),
);
}
@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:smooth_app/data_models/product_image_data.dart';
import 'package:smooth_app/generic_lib/loading_sliver.dart';
import 'package:smooth_app/generic_lib/widgets/images/smooth_images_view.dart';
import 'package:smooth_app/generic_lib/widgets/smooth_list_tile_card.dart';

/// Displays a [SliverList] by using [SmoothListTileCard] for showing images
/// passed via [imagesData].
///
/// If [loading] is set to `true`, the list shows instead
/// loading [SmoothListTileCard]s.
class SmoothImagesSliverList extends SmoothImagesView {
const SmoothImagesSliverList({
required super.imagesData,
super.onTap,
super.loading = false,
});

@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 SliverList(
delegate: LoadingSliverChildBuilderDelegate(
loading: loading,
childCount: count,
loadingWidget: SmoothListTileCard.loading(),
childBuilder: (_, int index) => SmoothListTileCard.image(
imageProvider: imageList[index].value,
title: Text(
imageList[index].key.title,
style: themeData.textTheme.headline4,
),
onTap: onTap == null
? null
: () => onTap!(imageList[index].key, imageList[index].value),
),
),
);
}
}
@@ -0,0 +1,15 @@
import 'package:flutter/widgets.dart';
import 'package:smooth_app/data_models/product_image_data.dart';

/// Base class for classes that display a collection of images.
abstract class SmoothImagesView extends StatelessWidget {
const SmoothImagesView({
required this.imagesData,
this.onTap,
this.loading = false,
});

final Map<ProductImageData, ImageProvider?> imagesData;
final void Function(ProductImageData, ImageProvider?)? onTap;
final bool loading;
}
15 changes: 15 additions & 0 deletions packages/smooth_app/lib/generic_lib/widgets/picture_not_found.dart
@@ -0,0 +1,15 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_svg/svg.dart';

/// Displays a default asset as a _picture not found_ image.
class PictureNotFound extends StatelessWidget {
const PictureNotFound();

static const String NOT_FOUND_ASSET = 'assets/product/product_not_found.svg';

@override
Widget build(BuildContext context) => SvgPicture.asset(
NOT_FOUND_ASSET,
fit: BoxFit.cover,
);
}
@@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import 'package:smooth_app/themes/constant_icons.dart';

/// Displays an [IconButton] containing the platform-specific default
/// back button icon.
class SmoothBackButton extends StatelessWidget {
const SmoothBackButton({
this.onPressed,
});

final void Function()? onPressed;

@override
Widget build(BuildContext context) => IconButton(
icon: Icon(ConstantIcons.instance.getBackIcon()),
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
onPressed: onPressed ?? () => Navigator.maybePop(context),
);
}

0 comments on commit c706839

Please sign in to comment.